ATLAS日本基礎ネットワーク C++トレーニングコース
ここでの目標
目次
C++で扱う基本的な型には次のものがあります。
- 論理型
- 論理状態、真・偽を表すのみ。bool
- 文字型
- アルファベットなどの文字を表すもの。char
- 整数型
- 整数を表すもの。intなど
- 浮動小数点数型
- 実数を表すもの。doubleなど
また、それぞれの型に割り当てる記憶域の大きさによっていろいろなバリエーションがあります。
プログラム文脈の上からは、これらの値を表す表現としての定数(リテラルと呼ばれます)と、その型の値を格納するために用意された記憶域である変数があります。変数は必ず定義されなければ使うことが出来ません。
false
、真の時はtrue
になります。true
とfalse
は論理型リテラルです。
bool a = true;
論理型変数a
が初期値true
で初期化されて定義されました。
bool b;
今度は論理型変数b
が初期化子なしで定義されました。省略時標準解釈(デフォルト)でfalse
として初期化されます。
整数型や浮動小数点型など他の型のデータが論理型へ変換される場合、0がfalse、非0がtrueとして扱われます。また逆に、論理型の値が整数型などへ変換される場合falseが0に、trueが1に変換されます。
アルファベットや数字、記号や制御コードなど文字を表すものです。文字型リテラルは'a'
や'1'
など、シングルクォーテーションで挟んだ文字で表現されます。
char ca = 'a';
文字型変数ca
が'a'
で初期化されて定義されました。
文字の種類としてはASCIIコードと呼ばれるものでは128個あることから、文字型変数は8ビットの記憶域を取ります。整数であると考えると、符号付きか符号なしかによって-128から127まで、もしくは0から255までを表現することが出来ます。このいずれを使おうとしているかによってsigned char
とunsigned char
の二つのバリエーションがあります。
unsigned char cp; signed char cn = -1;
符号がついているかいないかを指定せずにchar
として定義するとunsigned char
として扱われます。
ここで扱っているのは文字一つ一つです。文字列は文字型データを並べた物で、特別な扱いが必要です。それは後ほど解説します。
文字型リテラルは''
(ダブルクォートではなくシングルクォート二個)で囲まれた印刷可能文字で表されますが、それ以外の文字も表現可能です。例えば改行文字は'\n'
と表現されます。水平タブは'\t'
、ベルは'\a'
、バックスペースは'\b'
など。\
は日本語端末では円記号ですがASCII端末ではバックスラッシュになります。その文字自身を表すリテラルは'\\'
と記述します。では'シングルクォートは?、'\''
となります。任意の制御文字を表す別の方法として'\000'
と三桁の8進数を与える方法や'\x0FF'
というように16進数で与える方法もあります。
整数型int
はその記憶域の寸法によりshort int
(16ビット)、long int
(32ビット)、long long int
(64ビット)があり、かつ、符号つきsigned int
と符号無しunsigned int
がそれぞれの記憶サイズにあります。
何も修飾をつけずに単にint
と書くとsigned long int
という扱いになります。
符号無しの場合は2進数表現で0から最大数までが表現できます。
符号つきの場合は2の補数表現で表せる範囲になります。
作業場所を用意しましょう。
cd
cd tutorial/cplusplus
mkdir l2
cd l2
どの範囲の数値が扱えるかを調べるために、次の内容のファイルをint.cxxとして書いてみましょう。
#define __STDC_LIMIT_MACROS 1 #include <iostream> #include <stdint.h> main( ) { std::cout << "short int min. = " << INT16_MIN << " max. = " << INT16_MAX << std::endl << "long int min. = " << INT32_MIN << " max. = " << INT32_MAX << std::endl << "long long int min. = " << INT64_MIN << " max. = " << INT64_MAX << std::endl; std::cout << "unsigned short int max. = " << UINT16_MAX << std::endl << "unsigned long int max. = " << UINT32_MAX << std::endl << "unsigned long long int max. = " << UINT64_MAX << std::endl; }
stdint.h
というヘッダーファイルの中で整数型の最大値と最小値(符号無しの場合最小値は0なのでマクロはありませんが)が定義されています。C++から呼び出すときは混乱の原因となりうるのでデフォルトでスイッチオフされていますが、そのスイッチが__STDC_LIMIT_MACROS
です。それをオンにします。(正確に言うとこれが定義されていればオンになるので必ずしも1にセットする必要はないのですが)
プログラムとしてはほとんどHello, World!
と変わりませんが、名前空間を個別に指定しています。std::cout
とstd::endl
です。それぞれstd::という名前空間に属する物であることを表します。Hello, World!
ではusing namespace std;
として、関数内に出てきたcout
やendl
がstd
に属することを一括して宣言していました。
stdint.h
はC++のインクルードファイルではなく/usr/include
にある標準のヘッダーです。C言語からも呼べる物でありすべてが匿名名前空間(名前のついていない名前空間)で定義されています。それ故.h
がついています。
ではこれをコンパイルして実行してみましょう。
g++ int.cxx
./a.out
short int min. = -32768 max. = 32767
long int min. = -2147483648 max. = 2147483647
long long int min. = -9223372036854775808 max. = 9223372036854775807
unsigned short int max. = 65535
unsigned long int max. = 4294967295
unsigned long long int max. = 18446744073709551615
short
では3万強、long
では21億強までの数字が扱えます。アプリケーションによってはこれでは不十分ということもあり得ます。国家予算はlong int
では扱えません。演算の結果がその記憶サイズで収容できない大きい数字になった場合「桁あふれ」overflow
例外が生じます。
整数リテラルは10進数、8進数、16進数で表現できます。10進数の場合は数字を1
から9
で始めます。0
で始まる数字は8進数と考えます。0
から7
の数字で構成する必要があります。16進数の場合は先頭を0x
で始めます。その後に0-9a-f
の数字を使います。
整数リテラルの精度を表すためにlong int
の場合、末尾にLをつけます。0L
は32ビット長の0
を意味します。また、符号無し数を表すためにU
をつけます。0U
は符号無しの0
です。これらを組み合わせて0UL
でunsigned long int
であることを示します。
通常、数値計算には浮動小数点数型のデータが使われます。これは、符号、指数部(2のN乗のN)、仮数部(W×2のN乗のWの部分)で数値を表現します。記憶領域の大きさにより、単精度(float
、32ビット)、倍精度(double
、64ビット)、四倍精度(long double
、128ビット)の物があります。
型 | 語長 | 符号 | 指数部 | 仮数部 | 正規化最小数(正) | 正規化最大数(正) |
float |
32 | 1 | 8 | 23 | 1.17549435e-38 | 3.40282347e+38 |
double |
64 | 1 | 11 | 52 | 2.225073858507201d-308 | 1.797693134862316d+308 |
long double |
128 | 1 | 15 | 112 | 3.3621031431120935062626778173217526q-4932 | 1.1897314953572317650857593266280070q+4932 |
それぞれ表現できる数値の範囲と、有効数字の桁数が違います。10進法で言うと、単精度で約7桁、倍精度で約15桁、四倍精度で約33桁の有効桁数があると言えます。
指数部は例えば単精度の場合8ビットあり、2の256の範囲の数字を表しますが、非常に小さい数から非常に大きい数まで扱うために、真ん中の指数、この場合128が0乗を表すようにオフセットをつけます。
指数部は例えば単精度の場合23ビットですが、実は最上位に1が立っているとしてその下の23ビットを示していると考えます。指数部は1.0以上2.0未満の数値を表す訳で、必ず最上位は1です。このビットのことを隠れビット(hidden bit)と呼びます。
浮動小数点の演算は例えば足し算をする場合、指数が異なる場合、片方の仮数部を、指数部が等しくなるまでシフトした上で加算します。シフトされる部分の仮数部はシフトされた分だけ精度が落とされることになります。
例えば、ある単精度の数に、それより7桁小さい単精度の数を足しても、1.0000000+0.00000001のような計算をすることになり、有効数字のものすごく乏しい、もしくは意味をなさないくらい不正確な計算をすることになります。何も考えずに積分アルゴリズムを書くと実際にこういうことをしてしまいます。注意が必要です。
次をやってみましょう。float.cxxという名前で作ります。
#include <iostream> main( ){ using namespace std; float x = 1.0; float y = 1.0e-8; float z; z = x + y; cout.precision( 16 ); cout.setf( ios_base::fixed, ios_base::floatfield ); cout << " x = " << x << " y = " << y << " x + y = " << z << endl; }
同様にコンパイル実行をやりましょう。
g++ float.cxx
./a.out
x = 1.0000000000000000 y = 0.0000000099999999 x + y = 1.0000000000000000
ソースコードでcout.precision(16)
は有効桁数16桁で表示しなさいという意味です。出力ストリームオブジェクトstd::cout
のprecision
メソッドを使っています。その次のcout.setf( ios_base::fixed, ios_base::floatfield)
は固定形式で表示するよう書式設定しています。std::cout
のsetf
メソッドは引数にios_base
名前空間で定義されたオブジェクトを受け付けます。
1に1億分の1を足しても1のままということです。上述したように、単精度では7桁までしか有効桁数がありませんから、このようになってしまいます。
上述のyの値をy=1.0e-7
でやってみましょう。
x = 1.0000000000000000 y = 0.0000001000000012 x + y = 1.0000001192092896
1に1千万分の一を足しました。すごくいい加減な足し算をしていることがわかります。
上述のfloat
と書かれたところをすべてdouble
にしてやってみましょう。
x = 1.0000000000000000 y = 0.0000000100000000 x + y = 1.0000000099999999
今度は(ほぼ)期待していたような結果に見えます。有効桁数が15桁あるdouble
を使った結果です。この場合も、1千兆分の一(10の-15乗)を足すと、同様に有効桁数が問題になってきます。
プログラムの中では、様々な型のデータは変数として記憶されます。その変数は必ず定義されなければなりません。それにより、記憶域が確保され、その変数に名前が与えられ、プログラムの中で参照できるようになります。
変数の定義は 型 変数名 = 初期値;
で与えます。
char c = 'A'; int l = 1; double x = 1.23;
=以降は初期化子です。省略が可能で、その変数が大域的もしくは静的staticな場合は0で初期化されます。動的に割り当てられた(関数の内部で定義された、もしくはnewで生成された)ものは初期化されず、不定な値を持ちます。注意してください。
int i;
一つの定義文の中で、同じ型の変数を複数定義することが可能ですが、ATLASのルールとしては一つの定義文中では一つの変数を定義することを求めています。プログラムをより読みやすくし、間違いを減らすためのルールです。
int i,j,k;
variables.cxx
を書いてみましょう。
#include <iostream> main( ) { using namespace std; char c = 'A'; int l = 1; double x = 1.23; cout << "c = " << c << " l = " << l << " x = " << x << endl; }
で、走らせます。
g++ variables.cxx
./a.out
c = A l = 1 x = 1.23
ある特定の型の変数が連続して割り付けられた領域を配列と呼びます。
配列の定義は、
int iarray[ 10 ]; int ja[] = { 1, 2, 3, 4, 5 };
最初の例は寸法を指定して定義するもので、初期値としてはデフォルトの0が与えられます。二番目の例は初期値の並びで定義するもので、初期値を収容できるだけのサイズで定義されます。
配列の個別の要素を参照するときは[]で配列要素を取り出します。
ja[ 2 ] iarray[ i ]
などです。
ここで使われているja
やiarray
は実は配列の置かれたアドレスに対するポインターです。ポインターに対して要素取り出し演算[]
を施すことで、配列要素の値を得ることが出来るわけです。
定義された最初の要素は0番目にあります。オフセットが0
で、iarray[0]
です。文法上はオフセットが実際の配列の寸法を超えたり(iarray[10]
)、マイナスであっても(iarray[-1]
)許されます。(文法上はチェックできません) オフセットを変数で与える場合、プログラマーの意図に反してそういうことが起こりえます。
array.cxx
を書いてみましょう。
#include <iostream> main( ) { using namespace std; int iarray[ 10 ]; int ia[ ] = { 1,2,3,4,5 }; cout << iarray << endl; cout << iarray[ 3 ] << endl; cout << ia[ 2 ] << endl; }
走らせてみます。
g++ array.cxx
./a.out
0x7fbffff0f0
0
3
変なものが表示されましたか。最初のcout << iarray << endl;
は配列のあるメモリーのアドレスをiarray
が持っているのでこういう結果になります。cout << iarray[ 3 ] << endl;
では明示的に初期化されていない配列要素iarray[3]
の内容を表示しています。
文字列を扱う場合、C言語では文字型の配列を使います。C++では文字列を扱うクラスが用意されており、それを使う方が便利で安全です。それについてはずっと後に解説します。
文字型の配列は整数型の配列などと同様に定義します。
char carray1[ 10 ]; char carray2[] = { 'H', 'e','l','l','o' }; char carray3[] = "Hello";
最初の例、carray1
はサイズ10で定義され、省略時標準解釈で初期化されます。次の例、carray2
は5つの要素を持ち、それぞれの文字で初期化されます。最後の例carray3
は6つの要素を持ちます。最初の5つの要素はcarray2
と同じHello
ですが、その後に0
が追加されます。ここで出てきた"
(ダブルクォート)で囲まれた文字の並びは文字列リテラルと呼ばれます。C言語からの歴史で、文字列は0
(ヌル文字)で終端されている必要があります。
この例で
carray2
をcout
するとなにかHello
の後にゴミが付きます。cout
はcarray2
という文字型ポインターで渡されたデータを文字型と考え、ヌル文字が出てくるまで表示し続けるからです。うまく動かすためにはcarray2
の初期化子を{'H','e','l','l','o','\000'}
等とし、終端文字(ヌル文字)を加えます。でも、こう書くよりはcarray3
のように文字列リテラルを使って初期化した方がスマートです。
試してみましょう。carray.cxx
を書きます。
#include <iostream> main( ) { using namespace std; char carray2[] = { 'H', 'e','l','l','o' }; char carray3[] = "Hello"; cout << carray2 << endl; cout << carray3 << endl; }
走らせます。
g++ carray.cxx
./a.out
Hello
Hello
先ほどの整数配列の時はアドレスが16進数で表示されたのに今回はその内容がHello
と表示されました。文字型配列へのポインターは特別扱いされています。
ポインターについては次で説明します。その最後に再度文字列ポインターについて考えます。
この例題で、最初のHelloの後に何か変なものが表示されませんでしたか。その理由は上に述べたとおりです。carray2がちゃんとヌル文字で終端されていないためですね。
ポインターは変数のアドレスを記憶する変数です。
ポインターはそれが指し示す変数の型により文字型へのポインター、整数型へのポインター、浮動小数点数型へのポインターなどがあります。有効な型を持たないポインターとしてvoid
ポインターもあります。ポインターへのポインターも可能です。
ポインターであることは*
を型宣言に付け加えて示します。
int* ip;
この例はip
が整数型データへのポインターであることを表します。注意すべきは、*
がその直後の変数宣言にのみ有効であるということです。
int* ip, jp;
この場合、*
はip
にのみ有効です。ip
はポインターですが、jp
は整数型変数であるという宣言になります。
名前からわかるように、このプログラマは
ip
もjp
もポインターのつもりで定義しているかもしれません。こういった誤解を避けるため、ATLASでは一つの定義文で同種の変数だけ定義するよう求めています(CL4)。定義の中でも、
int*i,int* i, int *i, int * i
という風に、空白を入れても入れなくても良いのですが、*
が直後の変数定義にのみかかると言うことはint *i
やint * i
のように、int
の後に空白を入れる方が読みやすいでしょう。
ポインターもデフォルトの初期値は0又は不定(大域的・局所的・動的により)です。それ故、正しく初期化せずに使用すると有害です。初期値としては変数へのアドレスを渡す必要があります。
int i=1; int * ip = & i;
この例ではまず整数型変数i
を定義します。これは1
で初期化されます。1
という値を持っています。次に整数型へのポインターip
を定義します。初期化としてi
という定義済み変数のアドレスを与えます。 &
はアドレスを取り出す演算子です。&i
で変数i
のアドレスを取り出しています。
ポインターは記憶域のアドレスを保持しています。それを通常の変数のように使うと、それが保持するアドレスへの演算になってしまいます。それ自身は意味がある場合があります。そのポインターが指し示す記憶域(変数)の中身を取り出すには*という演算子を前につけて表します。
int iarray[ 10 ]; int * ip = iarray;
最初に整数型配列を定義しました。先にも述べましたが、配列名はそれが割り当てられた記憶域へのポインターとして使えます。ですので、二行目はip
を整数型のポインター(int *
)として定義し、その初期値として配列iarray
を指すようにしました。
int j = * ip;
この例はip
が現在指している整数記憶域の内容、つまりiarray[0]
の内容を使って整数型変数jを初期化するというものです。さらに続けて、
ip += 2; k = * ip;
この最初の行はip
という整数型ポインターに2
を足しています。実際には整数という単位で2つ先の整数を指すことになるので、加えられる値は8(sizeof( int )
が4なので、つまり整数は4バイト長)です。二行目の例はそれ故、iarray[2]
の内容をk
に代入していることになります。
const int ARRAYSIZE = 10; int iarray[ ARRAYSIZE ]; ... //ここでiarrayの中身を満たす int i; int isum = 0; int * ip = iarray; for( i = 0; i < ARRAYSIZE; i++ ) isum += * ip ++;
この例は、iarray
という配列の中身の和を計算しています。最後の文、isum += * ip ++;
は、単項演算子が右結合で、*
と++
は優先順位が等しいので、まずip ++
が評価されます。ip
が持つポインターの値が残り、++
演算によりip
の内容が一つ(4バイト)進められます。式の値として整数型へのポインターの値が残っています。そこに*
(間接参照。ポインターが指している先のデータの取り出し)が作用してip
が指しているiarray
のi番目の要素の値が結果として残ります。その値を最後に+=
でisum
に加えます。この演算の詳細については次回説明します。
for( i = 0; i < ARRAYSIZE; i ++ ) isum += iarray[ i ];
としても結果的には同じですが、厳密に追いかけると、この場合、i
が変化するごとにiarray
の先頭からのオフセットを計算し直し、その値をiarray
に加えた上で得られたポインターを使って値を取り出し、その値をisum
に加えています。どちらの方が効率的でしょうか。ほとんど差はありませんが。
pointer.cxx
を書いてみましょう。
#include <iostream> main( ) { using namespace std; int iarray[] = {1,2,3,4,5,6}; int isize = sizeof( iarray ) / sizeof( int ); int isum = 0; int * ip = iarray; for( int i = 0; i < isize; i ++ ) isum += *ip++; cout << "Size = " << isize << " Sum = " << isum << endl; }
sizeof
関数は引数が表す記憶域のバイト数を返します。6つの配列要素を持つiarray
のサイズは24バイト。それをint
型の記憶域のサイズ4で割りますので配列要素数6が得られます。走らせてみましょう。
g++ pointer.cxx
./a.out
Size = 6 Sum = 21
文字型配列を指すポインターは文字列を扱うためによく使われます。前節で文字型配列でも説明したように文字型配列を定義し、そのアドレスをポインターに与えることで文字列を扱えます。先ほどの例に続けるとしたら、
char * cp = carray3; std::cout << cp << endl;
などと出来ます。また、cp
は定数ではない配列を指していますので、
carray3[ 0 ] = 'h'; //Hの文字をhに書き換えた。 * cp = 'h'; //これも全く同じ。
一方、文字列ポインターを文字列リテラルで初期化することが出来ます。
char * cp = "Hello";
この場合、(const
宣言されていないにもかかわらず)cp
の指す先が文字列リテラルであるために* cp = 'h'
のような代入は禁止されます。
cpointer.cxx
を書いてみましょう。まず
#include <iostream> main( ) { using namespace std; char carray[] = "Hello!"; char * cp1 = "World."; char * cp2 = carray; cout << cp2 << cp1 << endl; * cp2 = 'h'; cout << cp2 << cp1 << endl; * cp1 = 'w'; cout << cp2 << cp1 << endl; }
走らせましょう。
g++ cpointer.cxx
cpointer.cxx: In function ‘int main()’:
./a.out
cpointer.cxx:5: warning: deprecated conversion from string constant to ‘char*’
Hello!World.
hello!World.
Segmentation fault (core dumpt)
コンパイル時に警告メッセージが出ましたが、とりあえずここは無視してください。
最後の行が実行されずSegmentation fault
になってしまいました。これは、書いてはいけないところに書こうとしたことに因ります。原因は* cp1 = 'w';
にあります。cp2がプログラム中に割り当てた配列carrayを指しているのに対しcp1は"World."という文字列リテラルを指しています。この部分は定数であり書き換えが実行時に禁止されます。
ここまでの例でも、ポインターに数値を足したり、インクリメントしたりしました。そこでは、例えばポインターの内容を1増やすと言うことは、それが扱うデータ型の寸法を単位として計算すると言うことでした。整数型の場合、1足すと言うことは4バイト分(sizeof(int)
)先を指すように、アドレスの値としては4を加えると言うことになっています。
例えばポインターの間の引き算をします。そうすると、それぞれが指している場所の相対距離が得られます。
int * ip1 = & iarray[ 2 ]; int * ip2 = & iarray[ 4 ]; int idiff = ip2 - ip1;
この結果、ip2
はip1
より8バイト先を指していて、cout << ip2
とやるとip1
より8だけ大きい値を出力するにもかかわらず、idiff
には2
が代入されます。それはip1
やip2
が整数型へのポインターだからsizeof(int)
を単位で計算した差を代入するからです。
pcalc.cxx
を書いてみましょう。
#include <iostream> main( ) { using namespace std; int iarray[] = {1,2,3,4,5,6}; int * ip1 = & iarray[ 2 ]; int * ip2 = & iarray[ 4 ]; cout << "ip1 = " << ip1 << endl; cout << "ip2 = " << ip2 << endl; cout << "Difference = " << ip2 - ip1 << endl; }
走らせましょう。
g++ pcalc.cxx
./a.out
ip1 = 0x7fbffff108
ip2 = 0x7fbffff110
Difference = 2
ip1
とip2
の値は0x
で始まっており16進数で表現されています。最後の二桁が0x08
と0x10
なので10進数で表現すると8と16、つまり8違うのですが差は2
と表現されます。8/sizeof(int)
が表示されたことになります。
プログラムの中では、変更されることのないデータの表現(定数)が必要になります。定数を実現するのに、constという修飾を使う方法とenum(列挙)があります。
const
は定数を定義するために用いられる仕組みです。const
として定義された変数は(定数として)初期化時以外に値を設定することが出来ません。
const int ARRAY_SIZE = 10; int iarray[ ARRAY_SIZE ];
浮動小数点数でも、配列でも可能です。
const double pi = 3.1415926; const char * month[ ] = { "January", "February", "March",...
pi
の値を書き換えることは出来ません。また、month
の要素が他の文字列を指すように書き換えることは出来ません。
プログラム中で使われる変数等にconst
を与えて読み出し専用にすることにより意図しないデータの改変の検出などより信頼性の高いプログラムを書くことが出来ます。それは特にポインター関係で起こります。
int i = 0; const int *ip = &i;i=2; // OK *jp = 3; // NG. jpはconst intへのポインター
i
は定数ではないので変更が可能ですがip
はconst int
へのポインターなのでそこへの代入は禁止されます。関数の引数などにconst
ポインターを渡しておけば、間違った変更を防ぐことが可能です。
int * const jp = &i;
const
の出現する順番が型宣言より後になりました。この場合、jp
自身がconst
なので、それが持つアドレスを書き換えることは出来ませんが、指し示す先はint
ですので*jp=3
のように指し示す先の値の変更が可能です。
先ほど作ったcpointer.cxx
ですが、中程
char * cp1 = "World.";
を
const char * cp1 = "World.";
に置き換えましょう。char
の前にconst
をつけます。コンパイルすると
cpointer.cxx: In function `int main()':
cpointer.cxx:11: error: assignment of read-only location '* cp1'
とエラーになります。const char
で、指している先が文字型の定数であることがわかるのでコンパイラーはエラーを検出できます。先ほどは気がつかずに実行してしまってSegmentation fault
でした。この違いは大変重要です。できるだけコンパイル時に問題を摘出しておくことです。
ひとまとまりと考えられる一連の整数型定数は次に説明するenum
(列挙)を使用することをお薦めします。
状態遷移モデルでは、いくつかの有限のステートを考え、そのステートの間を移動することでタスクを実現します。プログラミング上では整数型の変数にそのステートを記憶させ、それぞれのコンテキストでステートを解読して処理をおこなうという書き方になります。この場合、プログラム中にリテラルを直接書き込むのではなくて、ステートを表すシンボルを使って記述する方がずっとわかりやすくなります。(例で用いられているswitch
文については次回説明します。)
switch( istate ) { case 0: //do something here for state = 0 break; case 1: ...
というよりも
switch( istate ) { case INITIAL_STATE: //do something break; case STANDBAY_STATE: ...
という方がプログラマーの意図が明確に出来ます。このような場合、const int
でこれらのシンボルを定義することも、マクロで#define
を使って定義することも可能なのですが、enum
(列挙)を用いて記述するのがもっともスマートです。
enum MACHINESTATE { INITIAL_STATE, STANDBY_STATE, RUNNING_STATE };
enum
は一つの型です。関数の引数に用いる場合、int istate
として与えるより、MACHINESTATE istate
として与えた方が、読みやすくなり、かつ、コンパイラが値の範囲をチェックできるメリットがあります。列挙では先頭から順番に0
、1
、2
と昇順で番号を振っていきます。列挙において任意の数字を与えることも可能です。
enum ERROR_CODE { INVALID_ARGUMENT = -1, ARGUMENT_MISSING = -2, .... };
この場合も一連の定数を集めて一つの型として与えることの意味が理解できます。
C言語では当たり前であった
#define
によるシンボル定義は唯一の例外を除いてC++では使う必要はありません。#define
はプリプロセッサの段階でシンボルをリテラルに変換してしまうので、コンパイラがチェックをするチャンスを奪ってしまうことになります。#define
は(唯一の例外を除いて)使用しないようにしましょう。
enum
は、整数型の定数を宣言しているのではなく、列挙による新しい型を定義しています。上述の例ですと、MACHINESTATE
やERROR_CODE
が型の名前になります。その方が取り得る値は{}
に列挙されたシンボルです。}
の後に;
(セミコロン)がつけられているのは、enum
文が型の定義であることから、}
に続けてその方に属する変数を定義することが出来るからです。}
に続けて直ちに;
を与えることで、ここでは変数を定義しないという解釈がされます。
型名を与えないenum
も可能です。
enum { ARRAY_SIZE = 10 };
列挙は,
(コンマ)で区切っていくつでも並べられますが、空白のみの列挙は許されていません。
enum { ARRAY_SIZE = 10, };
気持ちとしては続けて何か定義したかったのですが、今のところARRAY_SIZE
だけが列挙されています。この場合その後の,
と}
の間には空白しかありません。これはエラーになります。,
を取る必要があります。
enum
を実際に使ってみましょう。enum.cxx
を作ります。
#includemain() { using namespace std; enum MACHINE_STATE { INITIAL_STATE, STANDBY_STATE, RUNNING_STATE, EXIT_STATE }; int state = INITIAL_STATE; while( EXIT_STATE != state ) { cout << "state code = " << state << ": "; switch( state ) { case INITIAL_STATE: cout << "INITIAL_STATE" << endl; state = STANDBY_STATE; break; case STANDBY_STATE: cout << "STANDBY_STATE" << endl; state = RUNNING_STATE; break; case RUNNING_STATE: cout << "RUNNING_STATE" << endl; state = EXIT_STATE; break; } } }
コンパイルして実行します。
g++ enum.cxx
./a.out
state code = 0: INITIAL_STATE
state code = 1: STANDBY_STATE
state code = 2: RUNNING_STATE
参照とは記憶域(変数)に別名をつける方法として導入されました。
参照は必ずそれが指し示す相手の変数で初期化することでしか定義できません。
int i; int & ir = i;
第一行目で整数型変数i
を定義します。二行目がi
への参照ir
の定義です。整数型への参照を示すint &
で定義します。
参照はそれが結びつけられた変数と全く同じ使われ方をします。上記の例ではi
が使われるところをすべてir
で置き換えることが可能です。ポインターではそれが指す相手の内容を取り出すために毎回*をつける必要がありました。参照ではその必要はなく、そのまま使えばよいのです。これにより、プログラムはより読みやすくなります。
参照がよく使われるのは関数の戻り値や引数です。元々C++に参照が導入されたのは、演算子オーバーロードを導入するにあたり、ポインターの煩わしさ(*
や&
を毎度つける)を避けるためです。(「C++の設計と進化」BJより)
ですので、詳細はクラスの解説の後に説明します。
次回は手続きと関数について説明します。