ロベールのC++教室・第1部を読んだ
「ロベールのC++教室」第1部を読んだ。if文、for文、ポインタの定義の仕方など、自分にとって既知のことは説明を端折りつつ、未知の情報を整理してみる。
目次:
文字列について
string.h をインクルードすれば、文字列を比較したり書き換えたりする関数が使える。
書式 | 説明 |
---|---|
size_t strlen( const char *s ); | 文字列*sの長さ(バイト数)を取得する。終端の'\0'は含まない。 |
char strcpy( char s1, const char *s2); | 文字列s1に文字列s2を'\0'までコピーする。文字列*s1のサイズに注意。 |
char strcat( char s1, const char *s2 ); | 文字列配列s1のうしろにs2を連結する。文字列*s1のサイズに注意 |
int strcmp( const char s1, const char s2 ); | 文字列s1と文字列s2を比較する。等しければ0を返す。文字列を直接==で比較することはできないので注意 |
char strchr( const char s, int c ); | 文字列sの先頭から文字cを探し、最初に見つかった位置をポインタで返す。見つからなければNULLを返す。'\0'も検索可能 |
以下は使用例。半角英数は1文字あたり1バイトである。ひらがなの1文字あたりバイト数は環境により異なるが、つねに2バイト以上である。
// String.cpp #include <iostream> #include <string.h> using namespace std; int main() { char szHello[50] = "こんにちは"; const char* pszCat = "Koneko"; cout << (void *)pszCat << endl; cout << (void *)strchr( pszCat, 'o' ) << endl; cout << strchr( pszCat, 'o' ) << endl; cout << strlen( szHello ) << endl; cout << strcpy( szHello, pszCat ) << endl; cout << szHello << endl; return 0; }
実行結果:
0x5648549f6d25 0x5648549f6d26 oneko 15 Koneko Koneko
ポインタの演算
ポインタに整数を足したり引いたりできる。
// #includeなどは省略 int main(){ char szHello[] = "こんにちは"; int nOdd[] = { 1, 3, 5, }; long int lnPrime[] = { 2, 3, 5, }; cout << szHello << " " << szHello+1 << endl; cout << nOdd << " " << nOdd+1 << endl; cout << lnPrime << " " << lnPrime+1 << endl; }
実行結果
こんにちは ��んにちは 0x7fff70cdbfc4 0x7fff70cdbfc8 0x7fff70cdbfd0 0x7fff70cdbfd8
ポインタp(=&p[0])
に1足すと&p[1]
になる。*(p + 5)
は (p+5)[0]
や p[5]
や (p + 2)[3]
に等しい。
さらにポインタの加減は以下のようにも使える。
// Pointer_plus_minus.cpp #include <iostream> using namespace std; int strlen( const char* s ) { int i; for( i = 0; *(s + i); i++); return i; } void DispLength( const char* s ) { cout << s << "の長さは" << strlen( s ) << "Byteです。" << endl; } int main() { DispLength("やっほー"); DispLength("good morning"); return 0; }
実行結果:
やっほーの長さは12Byteです。 good morningの長さは12Byteです。
for文中の*(s + i)
はs[i]
と同じである。文字列終端のヌルターミネータに着いたらfor文を抜ける。
マクロ
マクロは以下のように定義する。#define マクロ名 差し込むテキスト
マクロを定義しておけば、それ以降のソース中に書いた「マクロ名」は、コンパイル前に「差し込むテキスト」へと置き換えられる。
下のコード Macro1.cpp でマクロの使用例を示した。
// Macro1.cpp #include <iostream> using namespace std; #define NUM 5 #define REP( i, time ) for( i=0; i<time; i++ ) #define FUNC( name ) void name( int x, int y ) FUNC(func); int main() { int i; REPP( i, NUM ) { func( 1, i ); } return 0; } FUNC(func) { cout << x + y << endl; }
実行結果:
1 2 3 4 5
「マクロ名」と「差し込むテキスト」を分けるのは「空白」と「カッコの終わり」である。例えば#define FUNC ( num ) num+1
はマクロ名がFUNCだとみなされるのでエラー。ただしカッコ内の空白は許される。#define FUNC( num ) num+1
次に以下のマクロは何が問題だろうか。#define CAT(name); strcat( name, "さん" );
例えば
if( something == 0 ) CAT( a );
のマクロ展開後のコードは
if( something == 0 ) ; strcat( a );
これでは、if文の条件を満たせばセミコロンだけの白文が実行され、if文とは関係なくstrcat( a )
が実行されてしまう。マクロ名が「カッコの終わり」までとみなされ、「差し込むテキスト」は白文と strcat( a )
の2つであると判断されたのである。
解決するにはセミコロンを消せばよい。#define CAT(name) strcat( name, "さん" )
次に、以下のマクロは何が問題だろうか。#define MUL( n1, n2 ) n1*n2
例えば、このマクロを以下のように使用したとする。
cout << MUL( 1+2, 3+4 );
展開後はこのようになる。
cout << 1+2*3+4;
演算子+
よりも演算子*
が優先される。そのため、値21がほしかったのに値11が出てきてしまう。解決するには「差し込むテキスト」内の変数部分をカッコでくくってあげればよい。#define MUL( n1, n2 ) ((n1)*(n2))
最後に「差し込むテキスト」が複数行にわたる場合。
#define SWAP( n1, n2 ) tmp = n1; \
n1 = n2; \
n2 = tmp;
行末にバックスラッシュを入れると改行ができる。それはよいとして、上のマクロ定義の何が問題だろうか。以下のマクロ使用例を見てみよう。
if( something == 0 ) SWAP( a,b );
これをマクロ展開する。
if( something == 0 ) tmp = a; a = b; b = tmp;
このようになってしまう。では差し込むテキストを波括弧でくくるとどうなるだろうか。
#define SWAP( n1, n2 ) {tmp = n1; \
n1 = n2; \
n2 = tmp;}
この場合、
if( something == 0 ) SWAP( a, b ); else // 何らかの処理
をマクロ展開すると
if( something == 0 ) { tmp = a; a = b; b = tmp; }; else // 何らかの処理
これではセミコロンの手前でif文が終わってしまい、else
の部分でエラーが出てしまう。この問題を解決するには差し込むテキストを do〜while 文でくくればよい。
#define SWAP( n1, n2 ) do{tmp = n1; \
n1 = n2; \
n2 = tmp;}while(1)
するとマクロ展開後のコードは以下のようになる。
if( something == 0 ) do{ tmp = a; a = b; b = tmp; }while(0); else // 何らかの処理
このdo〜while文の中身は1回だけ実行される。
フラグ処理〜bit演算の使用例〜
以下のコードはbit演算を使ってフラグ処理する例。文字列をコピーする関数strcpy_ex
を作った。フラグが
- 00ならそのままコピー
- 01なら1文字毎にスペースを入れてコピー
- 10なら「x」と「X」以外をコピー
- 11なら「x」と「X」以外を、1文字毎にスペースを入れてコピー
#include <iostream> using namespace std; #define BIT(num) ((unsigned int)1<<(num)) #define SCEX_COPY 0 #define SCEX_SPACE BIT(0) #define SCEX_TRIM_X BIT(1) void strcpy_ex( char* pszDest, const char* pszSource, unsigned int flags) { int i,j; for( i=0, j=0; *(pszSource+j); i++, j++ ) { // x,X をトリミングするなら、今指している文字が x,X の時は飛ばして次のループに行く if( (flags & SCEX_TRIM_X) && (*(pszSource+j) == 'x' || *(pszSource+j) == 'X') ) { i--; continue; } // スペース処理をするならスペースを入れて、しないならスペースを入れないでコピーする if( flags & SCEX_SPACE ) { *(pszDest+2*i) = *(pszSource+j); *(pszDest+2*i+1) = ' '; }else *(pszDest+i) = *( pszSource + j ); } } void Disp( char* pszDest, const char* pszSource, unsigned int flags ) { strcpy_ex( pszDest, pszSource, flags ); cout << pszDest << endl; } int main() { const char pszSource1[512] = "hello"; const char pszSource2[512] = "xxXxxXGoXxxodxxMxxoXrning.XX"; char pszDest1[512], pszDest2[512]; Disp( pszDest1, pszSource1, SCEX_SPACE ); Disp( pszDest2, pszSource2, SCEX_TRIM_X | SCEX_SPACE ); return 0; }
実行結果
h e l l o G o o d M o r n i n g .
ファイル
ファイルを開いて読み込んだり、書き込んだりできる。関数fopenでファイルを開いたら、その情報をFILE構造体のポインタに入れておく。ファイルは開いたら関数fcloseで閉じなければならない。
// FileSample.cpp #include <stdio.h> int main() { FILE *pFile; char buffer[128]; // FileSample.cpp と同じディレクトリにあるファイルtest.txtを // 書き込み専用で開く pFile = fopen( "test.txt", "w" ); fputs( "ファイルに\n書き込むよ\n", pFile ); fclose( pFile ); // 読み取り専用で開く pFile = fopen( "test.txt", "r" ); // 1行読み出し fgets( buffer, 128, pFile ); // putsは最後のヌル文字を勝手に改行文字に変えて表示する puts( buffer ); fgets( buffer, 128, pFile ); puts( buffer ); fclose( pFile ); return 0; }
実行結果:
ファイルに 書き込むよ
ファイルを開く時、関数fopenの第二引数にはファイルをどのモードで開くかを指定する。
モード | ファイルがある場合 | ファイルがない場合 |
---|---|---|
r | 読み取りモードで開く。 | エラー。関数fopenはNULLを返す。 |
w | ファイルの内容がなくなり、書き込みモードで開く。 | 新しくファイルを作成して書き込みモードで開く。 |
a | ファイルの終端に書き込むモード(追加モード)で開く。 | 新しくファイルを作成して書き込みモードで開く。 |
r+ | 読み取りと書き込み両方のモードで開く。 | エラー。関数fopenはNULLを返す。 |
w+ | ファイルの内容がなくなり、読み取りと書き込み両方のモードで開く。 | 新しくファイルを作成して読み取りと書き込み両方のモードで開く。 |
a+ | 読み取りと追加両方のモードで開く。データを書き込む前にEOFマーカー(0X1A)が削除され、書き込みが完了すると終端にEOFマーカーが追加される。 | 新しくファイルを作成して読み取りと追加両方のモードで開く。 |
モードの最後にbをつけるとバイナリモードで開く。bをつけないとテキストモードで開く。その違いは、改行文字の扱い方である。改行文字は、Windowsでは'\r\n'
、Macでは'\r'
、UNIX系OSでは'\n'
である。テキストモードでは、それぞれの環境でファイルを扱うために、勝手に改行文字を変換してくれる。例えばfputs( "aaa\n", pFile )
だとWindows環境ならば"aaa\r\n"
と書き込んでくれる。しかし、バイナリモードでは何環境であろうと、そのまま"aaa\n"
と書き込む。
さて、ファイルの入出力に使う関数をまとめてみよう。
〜出力編〜
定義 | 動作 | 返り値 |
---|---|---|
int fputc( int c, FILE* pFile ) | 文字cをunsigned char 型に変換してファイルに書き込む。 |
正常に終了すれば書き込まれた文字を返す。エラーの時はEOFを返す。 |
int fputs( const char str, FILE pFile) | 文字列strをファイルに出力する。関数puts に似ているけれど違う点がある。それは関数puts が最後に改行文字を出力するのに対し、関数fputs はそうしないことだ。 |
出力エラーがあればEOF、正常に終了すれば0以上の値。 |
int fprintf( FILE pFile, const char format, ... ) | 関数printf のようにして、文字列をファイルに書き込む。 |
正常に終了すれば、出力した文字数を返す。エラーの時は負の値を返す。 |
size_t fwrite( const void pArray, size_t size, size_t n, FILE pFile) | ポインタpArrayの指す文字列をsize*nバイトだけファイルに書き込む。 | 正常終了すれば、実際に書き込まれた要素数。エラーの時は引数nよりも小さい値を返す。 |
〜入力編〜
定義 | 動作 | 返り値 |
---|---|---|
int fgetc( int c, FILE* pFile ) | ファイルpFileから1文字読み取る。その際、ファイル位置指示子を進める。getcマクロの関数バージョンである。 | 読み込んだ文字をunsigned charからintに変換して返却する。 |
char fgets( char str, int n FILE* pFile ) | ファイルpFileから最大n-1文字読み取って文字列strに格納する。途中でファイル終端に達するか改行が見つかれば、読み取りを終了する。改行が見つかった場合は、改行文字を文字列strに格納する。n文字目に'\0'を格納する。 | 読み取った文字列strを返す。読み取りエラーや一文字も読み取れなかった時はNULLを返す。 |
int fscanf( FILE pFile, const char format, ... ) | ファイルpFileからscanf 関数のように文字列を読み取る。後述の例を参照。 |
変換を1つも行えないまま入力エラーが起きた場合は、EOFを返す。その他の場合には、正常に代入を行えた個数を返す。 |
size_t fread( void pArray, size_t size, size_t n, FILE pFile) | ファイルpFileからsize*nバイト分の文字列を読み込んで、配列pArrayに格納する。 | 実際に読み込まれたバイト数÷第2引数size。エラーが発生したり、ファイルの末尾まで到達したりした際には、それよりも小さい値が返される。 |
ではいくつか例をやってみよう。
例1:ファイル"animals.txt"から特定の位置にある文字列を読み込んで出力する。
// fscanf.cpp #include <stdio.h> #include <stdlib.h> int main() { FILE* pFile; pFile = fopen( "animals.txt", "r" ); if( pFile == NULL ) return EXIT_FAILURE; char str[128]; while(1) { // %*sとはその文字列を無視するという意味 if( EOF == fscanf( pFile, "%*s %*s %s", str ) ) break; printf( "%s\n", str ); } return 0; }
animals.txtの内容:
I like cat You like dog She likes rabbit He likes bird
出力結果:
cat dog rabbit bird
例2:fread関数で読み出して別ファイルにfwrite関数で書き込む。
// fwrite_and_fread.cpp #include <stdio.h> #include <stdlib.h> int main() { FILE *pFile1, *pFile2; pFile1 = fopen( "sample1.txt", "rb" ); pFile2 = fopen( "sample2.txt", "wb" ); if( pFile1 == NULL ) return EXIT_FAILURE; if( pFile2 == NULL ) return EXIT_FAILURE; char buffer[512]; int nDivisor = 8; // freadが返すのは、読み取った全バイト数(終端のヌル文字含む)を第2引数の数で割ったもの // 掛け算した値が配列サイズを越えさえしなければ第二、第三引数の値はメチャクチャでも良い int nLength = nDivisor * (int)fread( buffer, nDivisor, 512/nDivisor, pFile1 ); fprintf( pFile2, "終端のヌルを入れた文字数は「%d」です\n", nLength ); if( nLength > 512 ) nLength = 512; fwrite( buffer, 1, nLength, pFile2 ); fclose(pFile1); fclose(pFile2); return 0; }
sample1.txt
の内容はあらかじめ用意しておこう。sample1.txt
の内容:
12345 678 9
プログラム実行後のsample2.txt
の内容:
終端のヌルを入れた文字数は「12」です 12345 678 9
sizeof演算子
sizeof 変数
(またはsizeof(型名)
)でその変数が確保しているメモリサイズを、sizeof(型名)
でその型のサイズを
バイト単位で返す。
// うちの環境では、ひらがな1文字3バイト // さらに終端のヌル文字ぶんの1バイトも含めて数字を返す cout << sizeof "あいうえお" << endl; // 16 // sizeof( aiu ) はconst char型ポインタのサイズ。 const char* aiu = "あいうえお"; cout << sizeof aiu << endl; // うちでは8
さて、引数に配列を渡すと、配列の要素数を返す関数がほしいと思ったとする。配列のサイズを型のサイズで割ればよいだろう。しかし、△型配列arrayを関数の引数に渡すと、sizeof(array)は△*型ポインタのサイズになってしまう 。なので配列のサイズが取得できなくなる。よって上記で望んだ関数は作れない。その代わりに、配列の要素数を取得するマクロを定義することはできる。
#include <iostream> using namespace std; // 配列の要素数を取得するマクロ #define ELEM(array) (sizeof(array) / sizeof *(array) ) void Func( long int* array ) { // 8 8 1 // 関数を通してしまうと正しい結果が得られなくなる例 cout << sizeof *(array) << " " << sizeof array << " " << ELEM(array) << endl; // 配列を引数として渡さなければ望んだ結果が得られる例 long int array2[7]; // 8 56 7 cout << sizeof *(array2) << " " << sizeof array2 << " " << ELEM(array2) << endl; } int main() { long int test[7]; // 8 56 7 cout << sizeof *(test) << " " << sizeof test << " " << ELEM(test) << endl; Func(test); return 0; }
構造体
構造体の基本的な使い方を以下のコードで示した。
#include <iostream> using namespace std; #define ELEM(array) (sizeof (array) / sizeof *(array)) struct SHuman { char szName[16]; int nAge; char szHobby[16]; }; /* 構造体は参照渡しやポインタ渡しをする 値渡しすると、どんなに構造体のサイズが巨大であっても メモリ上にコピーされてしまうので注意 */ void DispHuman( const SHuman* phuman ) { // phuman->szNameと(*phuman).szNameは同じ // *phuman.szName とすると優先順位上 *(phuman.szName)になってしまう cout << "私の名前は" << phuman->szName << "です。" "年齢は" << phuman->nAge << "歳で" "趣味は" << phuman->szHobby << "です。"<<endl; } void LastYear( const SHuman& human ) { cout << human.szName << "さんは1年前" << human.nAge - 1 << "歳でした。" << endl; } int main() { SHuman friends[] = { { "Taro", 30, "釣り" }, { "John", 20, "読書" } }; SHuman me = { "naomeo", 1, "散歩" }; int i; for( i = 0; i < ELEM( friends ); i++ ) DispHuman( &friends[i] ); DispHuman( &me ); LastYear( me ); return 0; }
実行結果:
私の名前はTaroです。年齢は30歳で趣味は釣りです。 私の名前はJohnです。年齢は20歳で趣味は読書です。 私の名前はnaomeoです。年齢は1歳で趣味は散歩です。 naomeoさんは1年前0歳でした。
オーバーロード
以下の関数プロトタイプ宣言を見てみよう。
void func(int x, int y, int z); void func(int x, int y); int func(char a);
こんな風に同じ名前の違う関数を作ることをオーバーロードと呼ぶ。
また、仮引数によく使う値を代入しておくこともできる(その引数をデフォルト引数と呼ぶ)。オーバーロードやデフォルト引数にはルールがいくつかある。
1 : オーバーロードを行うには、引数の型が異なっていなければならない。戻り値の型が違うだけではエラーになる。
// エラー void func( int x ); int func( int x );
2 : デフォルト引数はこんな風に使う。
// ここではプロトタイプ宣言しか書いていない // 関数定義するときも同様に書く void func(int x, int y = 0, int z = 0); int func(char a = 'X');
途中の引数を省略することはできない:
// インクルードファイル・関数定義省略 int main() { func( x,, z = 3); // エラー! return 0; }
3 : オーバーロードとデフォルト引数を併用する時は、どの関数を呼び出しているかを区別ができるか確かめよう。
void func( int x, int y = 0, int z = 0 ); void func( int x, int y );
例えば上のような関数があったとして、
int main() { func( 1, 0 ); // どちらのfuncか区別できないのでエラー! func( 3 ); // 区別できるので問題なし func( 1, 0, 0 ); // 区別できるので問題なし return 0; }
呼び出す時の引数のとり方によって、どちらのfunc関数から呼び出したのか分からないことがある。しかもfunc( int x, int y )
の方は、呼び出すことができない。このようなことが起きないように気をつけよう。
4 : デフォルト引数には静的なデータしか使えない。静的なデータとは、定数、もしくはプログラム実行前に位置が決まっているデータのことである。つまり定数、外部変数、関数のことである。
void func( int x, int y = x ); // エラー!
内部変数はデフォルト引数には使えない。
#include <stdio.h> int main() { int a = 10; } // エラー! void func( int x, int y = a ){ }
ただし内部変数の定義にstaticをつけて静的にすれば大丈夫である。
静的内部変数
静的な内部変数を定義してみよう。
static int n; static char c;
普通の内部変数と何が違うのだろうか? 静的変数の特徴を挙げてみる。
- 一度しか初期化されない
- 初期値が与えられなければ0で初期化される
- 関数を抜けても値がそのアドレス上に保持される
- アドレスは常に一定である
それらを確かめてみよう。
// static_variable.cpp #include <iostream> using namespace std; #include <memory.h> // 1.一度しか初期化されない void OnceInit( int x ) { // 変数numが初期化されるのは最初だけ。2回目以降は値を渡しても何もしない static int num = x; num++; cout << num << endl; } // 2. 初期値が与えられなければ0で初期化される void InitZero() { static int num1, num2, num3, num4; /* ここで「代入」をしてしまうと関数が実行される度にnum0は5になる。 なので静的変数であることによる利点がなくなる。 なぜなら静的変数は「オブジェクトを参照のように扱いたいけど、 関数の外からは変更できないようにしたい」時に使うからである。 */ static int num0; num0 = 5; // 代入 cout << "num0 = " << num0 << endl; cout << "num1 = " << num1 << endl; cout << "num2 = " << num2 << endl; cout << "num3 = " << num3 << endl; cout << "num4 = " << num4 << endl; } // 3.関数を抜けても値がそのアドレス上に保持される void HoldValue( int*& ps, int*& p ) { static int a = 13; int b = 13; ps = &a; p = &b; } // 4.アドレスは常に一定である void CheckAddress_Sub1() { static int sa; int a; cout << "&sa = " << &sa << endl; cout << "&a = " << &a << endl; } void CheckAddress_Sub2() { cout << "in Sub2" << endl; CheckAddress_Sub1(); } void CheckAddress() { CheckAddress_Sub1(); CheckAddress_Sub2(); CheckAddress_Sub1(); } int main() { // 1.一度しか初期化されない OnceInit(2); // 3 OnceInit(5); // 4 OnceInit(10); // 5 // 2. 初期値が与えられなければ0で初期化される InitZero(); // 3.関数を抜けても値がそのアドレス上に保持される int *ps, *p; HoldValue( ps, p ); cout << "*ps = " << *ps << endl; cout << "*p = " << *p << endl; // 4.アドレスは常に一定である CheckAddress(); return 0; }
実行結果:
3 4 5 num0 = 5 num1 = 0 num2 = 0 num3 = 0 num4 = 0 *ps = 13 *p = 32765 &sa = 0x55833c02715c &a = 0x7ffd11396004 in Sub2 &sa = 0x55833c02715c &a = 0x7ffd11395ff4 &sa = 0x55833c02715c &a = 0x7ffd11396004
リンケージ
複数ファイルを実行するときに必要になるリンケージという概念について。
変数・関数が他のファイルでも使えるとき、その変数・関数は「外部リンケージを持つ」と言う。他のファイルでは使えないとき、その変数・関数は「内部リンケージを持つ」と言う。
まず外部リンケージについて。グローバル変数・関数は普通に定義しただけで外部リンケージを持つ。1つのファイルXにそれらの実体を書き、他のファイルYから呼び出したいとする。ファイルYには、関数のプロトタイプ宣言やグローバル変数のextern宣言を書いておく必要がある。ここで、externを書かなければグローバル変数の二重定義になってしまうので注意。
次に内部リンケージについて。グローバル変数・関数をstatic宣言するとそれらは内部リンケージを持つ。
// linkage1.cpp #include <iostream> using namespace std; // linkage2.cpp 内のCall関数にはstatic記憶クラス指定子がついているので // 内部リンケージを持っている。よって、 // linkage1.cpp からは呼び出せない。 void Disp( int n ); static void Func1( int n ); extern void Call( int n ); // static記憶クラス指定子がついているので // linkage2.cppでは使えない static int sn = -7; int main() { extern int num; Disp( num ); // 37 num = 36; Disp( num ); // 36 Disp( sn ); // -7 // linkage1.cpp内のFunc1関数が呼ばれる Func1( num ); // 36 // linkage2.cpp内のFunc1関数が(Call関数を通して)呼ばれる Call( num ); // 360 return 0; }6; Disp( num ); // 36 Disp( sn ); // -7 // linkage1.cpp内のFunc1関数が呼ばれる Func1( num ); // 36 // linkage2.cpp内のFunc1関数が(Call関数を通して)呼ばれる Call( num ); // 360 return 0; } static void Func1( int n ) { cout << "extern1.cpp内" << endl; cout << n << endl; }
// linkage2.cpp #include <iostream> using namespace std; int num = 37; // 実体 void Disp( int n ) // 実体 { cout << n << endl; } static void Func1( int n ) // 実体 { cout << "linkage2.cpp内" << endl; cout << n*10 << endl; } void Call( int n ) // 実体 { Func1( n ); }
実行結果:
37 36 -7 linkage1.cpp内 36 linkage2.cpp内 360
複数ファイルをコンパイルするときは、「main関数は1つでなければならない」ことに注意。
インライン関数
関数を呼ぶ時は、関数のアドレスまで移動する。この移動にかかる時間をなくしたい。
例えば下のコードでは、20000回もhello関数に移動している。
// inline_hello.cpp #include <iostream> using namespace std; void hello() { cout << "Hello!" << endl; } int main() { for( i=0; i<20000; i++ ) hello(); return 0; }
そこでhello関数をインラインにする。
inline void hello() { cout << "Hello!" << endl; }
するとhello関数を呼び出している場所に、hello関数の中身が埋め込まれるようになる(これをインライン展開と呼ぶ)。for文の部分を見てみよう。
for( i=0; i<20000; i++ ) cout << "Hello!" << endl;
ただし、展開するとプログラムサイズが肥大する場合はインライン展開されない。
また、hello関数のアドレスを取得する文をプログラム中に書いていてもインライン展開されない。インライン展開したら普通の関数ではなくなってしまうからである。
インライン関数をインライン展開するかどうかの判断はコンパイラが行う。
二重定義防止
例えばこんなファイル達をコンパイルしようとする。
// A.h class CA { int a; };
// B.h #include "A.h" class CB : public CA { int b; };
// main.cpp #include "A.h" #include "B.h" int main {}
うちの環境はコンパイラがgccのver.7.5.0でOSがUbuntuなので、端末でコンパイルの命令をこんな風に打ってみる。
$ g++ -o main main.cpp A.h B.h
するとこんなエラーが出る。
In file included from B.h:2:0,from main.cpp:3:
A.h:2:7:
error:
redefinition of ‘class CA’
class
CA
^~
クラスCA
が2回も定義されていますよというエラー。
これを避けるにはインクルードガード(二重定義防止)を施す。
// A.h #ifndef __A_H__INCLUDED__ #define __A_H__INCLUDED__ class CA { int a; }; #endif
// B.h #ifndef __B_H__INCLUDED__ #define __B_H__INCLUDED__ #include "A.h" class CB : public CA { int b; }; #endif
#ifndef <名前>
は、<名前>
が定義されていなければ#endif
までの文を実行する、という意味。#ifndef <名前>
の直後に#define <名前>
と書いて空の定義をする(<名前>という名前だけが定義をされる)。
ヘッダファイルに上記の処理を施しておけば、初めて呼び出された時だけ<名前>
が定義され、2回目以降は#ifndef
文と#endif
文の間にあるコードは無視される。
ヘッダファイル展開後のmain.cpp
を見てみよう。
// main.cpp // A.h #ifndef __A_H__INCLUDED__ #define __A_H__INCLUDED__ class CA { int a; }; #endif // B.h #ifndef __B_H__INCLUDED__ #define __B_H__INCLUDED__ // A.h #ifndef __A_H__INCLUDED__ // ここにあった処理がスキップされる #endif class CB : public CA { int b; }; #endif int main {}
1回インクルードしたA.h
が2回めではインクルードされていない。
列挙型
// enumerator.cpp #include <iostream> using namespace std; enum ECmp { LESSTHAN = 0, EQUALTO = 1, GREATERTHAN = 2, }; ECmp Cmp( int n ) { return (n>100) ? GREATERTHAN : (n<100) ? LESSTHAN : EQUALTO; } bool Cmp() { int n; cout << "整数を入力せよ" << endl; cin >> n; // −1が入力されたら偽を返す if( n == -1 ) return false; cout << "あなたが入力した数字は100"; switch( Cmp(n) ) { case LESSTHAN: cout << "より小さい" << endl; break; case GREATERTHAN: cout << "より大きい" << endl; break; case EQUALTO: cout << "に等しい" << endl; break; } return true; } int main() { while( Cmp() ); }
上のコードでは、enumを使ってECmpという型を作った。ECmpの波カッコ内にあるLESSTHANなどの名前を「列挙子」と呼ぶ。ECmpの定義は以下のように書き換えても一緒。
enum ECmp
{
LESSTHAN, EQUALTO, GREATERTHAN,
};
列挙子の値を何も書かなければ、最初の列挙子から順に0,1,2……となる。また、例えば
enum X { A = -3, B, C = 10, D, DEFAULT = 11, };
とすればBは-2、Dは11になる。同じ数値を持つ列挙子があってもよいが、区別がつかなくなるので注意。
列挙型オブジェクトに値を代入する時は、列挙子の名前か、キャストしたint型の数を入れる。
int main() { ECmp ec; ec = LESSTHAN; ec = (ECmp)0; //ec = 0; // エラー! ec = (ECmp)10; // エラーにならないので注意 }
ECmpの列挙子の値は0,1,2だがそれ以外の整数も代入できてしまうので注意が必要。