ATLAS日本基礎ネットワーク C++トレーニングコース

第二回 データの扱い

ここでの目標

目次

1.データ型

C++で扱う基本的な型には次のものがあります。

論理型
論理状態、真・偽を表すのみ。bool
文字型
アルファベットなどの文字を表すもの。char
整数型
整数を表すもの。intなど
浮動小数点数型
実数を表すもの。doubleなど

また、それぞれの型に割り当てる記憶域の大きさによっていろいろなバリエーションがあります。

プログラム文脈の上からは、これらの値を表す表現としての定数(リテラルと呼ばれます)と、その型の値を格納するために用意された記憶域である変数があります。変数は必ず定義されなければ使うことが出来ません。

1.1.論理型

真と偽の二つの値を取ります。偽の時はfalse、真の時はtrueになります。truefalseは論理型リテラルです。
bool a = true;

論理型変数aが初期値trueで初期化されて定義されました。

bool b;

今度は論理型変数bが初期化子なしで定義されました。省略時標準解釈(デフォルト)でfalseとして初期化されます。

整数型や浮動小数点型など他の型のデータが論理型へ変換される場合、0がfalse、非0がtrueとして扱われます。また逆に、論理型の値が整数型などへ変換される場合falseが0に、trueが1に変換されます。

1.2.文字型

アルファベットや数字、記号や制御コードなど文字を表すものです。文字型リテラルは'a''1'など、シングルクォーテーションで挟んだ文字で表現されます。

char ca = 'a';

文字型変数ca'a'で初期化されて定義されました。

文字の種類としてはASCIIコードと呼ばれるものでは128個あることから、文字型変数は8ビットの記憶域を取ります。整数であると考えると、符号付きか符号なしかによって-128から127まで、もしくは0から255までを表現することが出来ます。このいずれを使おうとしているかによってsigned charunsigned charの二つのバリエーションがあります。

unsigned char cp;
signed char cn = -1;

符号がついているかいないかを指定せずにcharとして定義するとunsigned charとして扱われます。

ここで扱っているのは文字一つ一つです。文字列は文字型データを並べた物で、特別な扱いが必要です。それは後ほど解説します。

文字型リテラルは''(ダブルクォートではなくシングルクォート二個)で囲まれた印刷可能文字で表されますが、それ以外の文字も表現可能です。例えば改行文字は'\n'と表現されます。水平タブは'\t'、ベルは'\a'、バックスペースは'\b'など。\は日本語端末では円記号ですがASCII端末ではバックスラッシュになります。その文字自身を表すリテラルは'\\'と記述します。では'シングルクォートは?、'\''となります。任意の制御文字を表す別の方法として'\000'と三桁の8進数を与える方法や'\x0FF'というように16進数で与える方法もあります。

1.3.整数型

整数型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::coutstd::endlです。それぞれstd::という名前空間に属する物であることを表します。Hello, World!ではusing namespace std;として、関数内に出てきたcoutendlstdに属することを一括して宣言していました。

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です。これらを組み合わせて0ULunsigned long intであることを示します。

1.4.浮動小数点数型

通常、数値計算には浮動小数点数型のデータが使われます。これは、符号、指数部(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のような計算をすることになり、有効数字のものすごく乏しい、もしくは意味をなさないくらい不正確な計算をすることになります。何も考えずに積分アルゴリズムを書くと実際にこういうことをしてしまいます。注意が必要です。

実習1.

次をやってみましょう。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::coutprecisionメソッドを使っています。その次のcout.setf( ios_base::fixed, ios_base::floatfield)は固定形式で表示するよう書式設定しています。std::coutsetfメソッドは引数に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乗)を足すと、同様に有効桁数が問題になってきます。

2.変数の定義と初期化

プログラムの中では、様々な型のデータは変数として記憶されます。その変数は必ず定義されなければなりません。それにより、記憶域が確保され、その変数に名前が与えられ、プログラムの中で参照できるようになります。

変数の定義は 型 変数名 = 初期値; で与えます。

char c = 'A';
int l = 1;
double x = 1.23;

=以降は初期化子です。省略が可能で、その変数が大域的もしくは静的staticな場合は0で初期化されます。動的に割り当てられた(関数の内部で定義された、もしくはnewで生成された)ものは初期化されず、不定な値を持ちます。注意してください。

int i;

一つの定義文の中で、同じ型の変数を複数定義することが可能ですが、ATLASのルールとしては一つの定義文中では一つの変数を定義することを求めています。プログラムをより読みやすくし、間違いを減らすためのルールです。

int i,j,k;
実習2.

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

3.配列

ある特定の型の変数が連続して割り付けられた領域を配列と呼びます。

3.1.配列の定義

配列の定義は、

int iarray[ 10 ];
int ja[] = { 1, 2, 3, 4, 5 };

最初の例は寸法を指定して定義するもので、初期値としてはデフォルトの0が与えられます。二番目の例は初期値の並びで定義するもので、初期値を収容できるだけのサイズで定義されます。

3.2.配列の引用

配列の個別の要素を参照するときは[]で配列要素を取り出します。

ja[ 2 ]
iarray[ i ]

などです。

ここで使われているjaiarrayは実は配列の置かれたアドレスに対するポインターです。ポインターに対して要素取り出し演算[]を施すことで、配列要素の値を得ることが出来るわけです。

定義された最初の要素は0番目にあります。オフセットが0で、iarray[0]です。文法上はオフセットが実際の配列の寸法を超えたり(iarray[10])、マイナスであっても(iarray[-1])許されます。(文法上はチェックできません) オフセットを変数で与える場合、プログラマーの意図に反してそういうことが起こりえます。

実習3.

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]の内容を表示しています。

3.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(ヌル文字)で終端されている必要があります。

この例でcarray2coutするとなにかHelloの後にゴミが付きます。coutcarray2という文字型ポインターで渡されたデータを文字型と考え、ヌル文字が出てくるまで表示し続けるからです。うまく動かすためにはcarray2の初期化子を{'H','e','l','l','o','\000'}等とし、終端文字(ヌル文字)を加えます。でも、こう書くよりはcarray3のように文字列リテラルを使って初期化した方がスマートです。

実習4.

試してみましょう。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がちゃんとヌル文字で終端されていないためですね。

4.ポインター

ポインターは変数のアドレスを記憶する変数です。

4.1.ポインターの定義

ポインターはそれが指し示す変数の型により文字型へのポインター、整数型へのポインター、浮動小数点数型へのポインターなどがあります。有効な型を持たないポインターとしてvoidポインターもあります。ポインターへのポインターも可能です。

ポインターであることは*を型宣言に付け加えて示します。

int* ip;

この例はipが整数型データへのポインターであることを表します。注意すべきは、*がその直後の変数宣言にのみ有効であるということです。

int* ip, jp;

この場合、*ipにのみ有効です。ipはポインターですが、jpは整数型変数であるという宣言になります。

名前からわかるように、このプログラマはipjpもポインターのつもりで定義しているかもしれません。こういった誤解を避けるため、ATLASでは一つの定義文で同種の変数だけ定義するよう求めています(CL4)。

定義の中でも、int*i,int* i, int *i, int * iという風に、空白を入れても入れなくても良いのですが、*が直後の変数定義にのみかかると言うことはint *iint * iのように、intの後に空白を入れる方が読みやすいでしょう。

ポインターもデフォルトの初期値は0又は不定(大域的・局所的・動的により)です。それ故、正しく初期化せずに使用すると有害です。初期値としては変数へのアドレスを渡す必要があります。

int i=1;
int * ip = & i;

この例ではまず整数型変数iを定義します。これは1で初期化されます。1という値を持っています。次に整数型へのポインターipを定義します。初期化としてiという定義済み変数のアドレスを与えます。 &はアドレスを取り出す演算子です。&iで変数iのアドレスを取り出しています。

4.2.ポインターの利用

ポインターは記憶域のアドレスを保持しています。それを通常の変数のように使うと、それが保持するアドレスへの演算になってしまいます。それ自身は意味がある場合があります。そのポインターが指し示す記憶域(変数)の中身を取り出すには*という演算子を前につけて表します。

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に加えています。どちらの方が効率的でしょうか。ほとんど差はありませんが。

実習5.

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

4.3.文字列ポインター

文字型配列を指すポインターは文字列を扱うためによく使われます。前節で文字型配列でも説明したように文字型配列を定義し、そのアドレスをポインターに与えることで文字列を扱えます。先ほどの例に続けるとしたら、

char * cp = carray3;
std::cout << cp << endl;

などと出来ます。また、cpは定数ではない配列を指していますので、

carray3[ 0 ] = 'h';    //Hの文字をhに書き換えた。
* cp = 'h';    //これも全く同じ。

一方、文字列ポインターを文字列リテラルで初期化することが出来ます。

char * cp = "Hello";

この場合、(const宣言されていないにもかかわらず)cpの指す先が文字列リテラルであるために* cp = 'h'のような代入は禁止されます。

実習6.

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()’:
cpointer.cxx:5: warning: deprecated conversion from string constant to ‘char*’
./a.out
Hello!World.
hello!World.
Segmentation fault (core dumpt)

コンパイル時に警告メッセージが出ましたが、とりあえずここは無視してください。

最後の行が実行されずSegmentation faultになってしまいました。これは、書いてはいけないところに書こうとしたことに因ります。原因は* cp1 = 'w';にあります。cp2がプログラム中に割り当てた配列carrayを指しているのに対しcp1は"World."という文字列リテラルを指しています。この部分は定数であり書き換えが実行時に禁止されます。

4.4.ポインター演算

ここまでの例でも、ポインターに数値を足したり、インクリメントしたりしました。そこでは、例えばポインターの内容を1増やすと言うことは、それが扱うデータ型の寸法を単位として計算すると言うことでした。整数型の場合、1足すと言うことは4バイト分(sizeof(int))先を指すように、アドレスの値としては4を加えると言うことになっています。

例えばポインターの間の引き算をします。そうすると、それぞれが指している場所の相対距離が得られます。

int * ip1 = & iarray[ 2 ];
int * ip2 = & iarray[ 4 ];
int idiff = ip2 - ip1;

この結果、ip2ip1より8バイト先を指していて、cout << ip2とやるとip1より8だけ大きい値を出力するにもかかわらず、idiffには2が代入されます。それはip1ip2が整数型へのポインターだからsizeof(int)を単位で計算した差を代入するからです。

実習7.

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

ip1ip2の値は0xで始まっており16進数で表現されています。最後の二桁が0x080x10なので10進数で表現すると8と16、つまり8違うのですが差は2と表現されます。8/sizeof(int)が表示されたことになります。

5.constとenum

プログラムの中では、変更されることのないデータの表現(定数)が必要になります。定数を実現するのに、constという修飾を使う方法とenum(列挙)があります。

5.1.const

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は定数ではないので変更が可能ですがipconst intへのポインターなのでそこへの代入は禁止されます。関数の引数などにconstポインターを渡しておけば、間違った変更を防ぐことが可能です。

int * const jp = &i;

constの出現する順番が型宣言より後になりました。この場合、jp自身がconstなので、それが持つアドレスを書き換えることは出来ませんが、指し示す先はintですので*jp=3のように指し示す先の値の変更が可能です。

実習8.

先ほど作った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(列挙)を使用することをお薦めします。

5.2.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として与えた方が、読みやすくなり、かつ、コンパイラが値の範囲をチェックできるメリットがあります。列挙では先頭から順番に012と昇順で番号を振っていきます。列挙において任意の数字を与えることも可能です。

enum ERROR_CODE { INVALID_ARGUMENT = -1,
        ARGUMENT_MISSING = -2,
        .... };

この場合も一連の定数を集めて一つの型として与えることの意味が理解できます。

C言語では当たり前であった#defineによるシンボル定義は唯一の例外を除いてC++では使う必要はありません。#defineはプリプロセッサの段階でシンボルをリテラルに変換してしまうので、コンパイラがチェックをするチャンスを奪ってしまうことになります。#defineは(唯一の例外を除いて)使用しないようにしましょう。

enumは、整数型の定数を宣言しているのではなく、列挙による新しい型を定義しています。上述の例ですと、MACHINESTATEERROR_CODEが型の名前になります。その方が取り得る値は{}に列挙されたシンボルです。}の後に;(セミコロン)がつけられているのは、enum文が型の定義であることから、}に続けてその方に属する変数を定義することが出来るからです。}に続けて直ちに;を与えることで、ここでは変数を定義しないという解釈がされます。

型名を与えないenumも可能です。

enum { ARRAY_SIZE = 10 };

列挙は,(コンマ)で区切っていくつでも並べられますが、空白のみの列挙は許されていません。

enum { ARRAY_SIZE = 10,
        };

気持ちとしては続けて何か定義したかったのですが、今のところARRAY_SIZEだけが列挙されています。この場合その後の,}の間には空白しかありません。これはエラーになります。,を取る必要があります。

実習9

enumを実際に使ってみましょう。enum.cxxを作ります。

#include 

main() {

    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

6.参照

参照とは記憶域(変数)に別名をつける方法として導入されました。

6.1.参照の定義

参照は必ずそれが指し示す相手の変数で初期化することでしか定義できません。

int i;
int & ir = i;

第一行目で整数型変数iを定義します。二行目がiへの参照irの定義です。整数型への参照を示すint &で定義します。

6.2.参照の利用

参照はそれが結びつけられた変数と全く同じ使われ方をします。上記の例ではiが使われるところをすべてirで置き換えることが可能です。ポインターではそれが指す相手の内容を取り出すために毎回*をつける必要がありました。参照ではその必要はなく、そのまま使えばよいのです。これにより、プログラムはより読みやすくなります。

参照がよく使われるのは関数の戻り値や引数です。元々C++に参照が導入されたのは、演算子オーバーロードを導入するにあたり、ポインターの煩わしさ(*&を毎度つける)を避けるためです。(「C++の設計と進化」BJより)

ですので、詳細はクラスの解説の後に説明します。

次回は手続きと関数について説明します。


前へ 上へ 次へ


2017年7月20日更新