文字配列は,文字変数の配列であるとともに, 文字列を記録するための変数でもある. 文字列データがどのようにメモリに格納されるのかを理解しよう.
まず,List 1 を利用して,文字列のメモリマップを確認してみよう. このプログラムの実験では, 異なる文字列を同じ文字配列に2回入力し, メモリ内容の変化を観察する.
/* 文字配列のメモリマップを表示 p:文字配列の先頭アドレスへのポインタ n:文字配列の要素数 */ void PrintStrMem(char *p, int n) { int i; printf("文字列 = \"%s\"\n\n", p); printf(" アドレス 変数名 文字 数値\n"); for (i = 0; i < n; i++) { printf("0x%0X : 要素[%d] = '%c' = %4d\n", p, i, *p, *p); p++; } } main() { char str[10]; printf("\n[1 回目の入力]\n文字列(9 文字以内,長めに) > "); scanf("%s", str); PrintStrMem(str, 10); printf("\n[2 回目の入力]\n文字列(9 文字以内,短めに) > "); scanf("%s", str); PrintStrMem(str, 10); }
実行例:
$ ./str-1 [1 回目の入力] 文字列(9 文字以内,長めに) > program 文字列 = "program" アドレス 変数名 文字 数値 0xBF9ADECE : 要素[0] = 'p' = 112 # 入力文字列 0xBF9ADECF : 要素[1] = 'r' = 114 0xBF9ADED0 : 要素[2] = 'o' = 111 0xBF9ADED1 : 要素[3] = 'g' = 103 0xBF9ADED2 : 要素[4] = 'r' = 114 0xBF9ADED3 : 要素[5] = 'a' = 97 0xBF9ADED4 : 要素[6] = 'm' = 109 0xBF9ADED5 : 要素[7] = '' = 0 # 終端記号 0xBF9ADED6 : 要素[8] = '�' = -99 # 以下,ゴミ 0xBF9ADED7 : 要素[9] = '�' = -65 [2 回目の入力] 文字列(9 文字以内,短めに) > C 文字列 = "C" アドレス 変数名 文字 数値 0xBF9ADECE : 要素[0] = 'C' = 67 # 変化部分 0xBF9ADECF : 要素[1] = '' = 0 # 終端記号 0xBF9ADED0 : 要素[2] = 'o' = 111 # 以下,1回目から変化なし 0xBF9ADED1 : 要素[3] = 'g' = 103 0xBF9ADED2 : 要素[4] = 'r' = 114 0xBF9ADED3 : 要素[5] = 'a' = 97 0xBF9ADED4 : 要素[6] = 'm' = 109 0xBF9ADED5 : 要素[7] = '' = 0 0xBF9ADED6 : 要素[8] = '�' = -99 # ゴミも変化なし 0xBF9ADED7 : 要素[9] = '�' = -65
この結果から次のことがわかる:
ここで,文字列末尾の数値 0 は 終端記号と呼ばれ, 文字列とゴミとの境界を知るために重要なデータである. この記号1文字が配列要素1個分のスペースを占有することになるので, 文字配列には用意した要素数よりも短い文字列しか記録できないことになるわけだ. (逆に,ある文字列を記録するには, それよりも1文字分以上広いメモリ領域が必要になる.)
なお,終端記号の数値 0 は,文字として「表示」しても, 実行結果の画面上では「非表示」'' であった. 一方,これをソースコードに文字として「記述」したい場合には, エスケープ系列 '\0' を使えばよい. (後で実験する.)
「文字列」と「文字」との違いに注意せよ.
文字列データの場合,たとえば,char str[] = "abc"; とすると, str[0] は 'a',str[1] は 'b',str[2] は 'c' となり, str[3] は自動的に '\0' になる. このように文字列では,終端記号は自動的に追加される.
一方,同じことを文字データで書こうとすると, str[] = {'a', 'b', 'c', '\0'}; のように, 終端記号を手動で明記する必要がある.
詳しくは,次のセクションで確かめる.
List 1 では,文字列データを実行時に入力した. 今度は,文字列データをソースコードに記述して, 文字配列を初期化してみよう. List 2 では,文字配列 str[10] を使い, 文字列 "program" を 別の文字列 "info" に書き換えようとしている.
main() { char str[10] = "program"; // 宣言と同時に初期化 printf("str = \"%s\"\n", str); str[0] = 'i'; // 1文字ずつ書き換え str[1] = 'n'; str[2] = 'f'; str[3] = 'o'; // str[4] = '\0'; // 終端記号の必要性を確認 // str = "info"; // これは無理 printf("str = \"%s\"\n", str); }
実行例:
str = "program"
str = "inforam" # 何か変だ
List 2 のままでは,文字列 "info" の末尾に終端記号が不足している. 変更して再実行しよう:
str = "program" str = "info"
ちなみに,List 2 の配列の宣言では, 次の書き方でも良さそうに思えるが, これは間違い:
char str[10];
str = "program"; // エラー
このエラーの理由については,後日解説予定.
前回の練習問題では, 文字列の長さを調べるために ライブラリ関数 strlen( ) を利用した. ここまでの知識を動員すれば, strlen() の利用方法だけでなく, その内部の仕組みまでも理解できるハズだ. このライブラリ関数のクローンを作ってみよう. List 3 を実行し,ソースコードを解読してみよう.
/* ライブラリ関数 strlen() のクローン 引数 p:文字列の先頭アドレス(文字列へのポインタ) 戻り値:文字数 */ int mystrlen(char *p) { int n = 0; // 文字数 while (*p != '\0') { // 終端記号まで繰り返す n++; // 文字数をカウント p++; // 次の文字へ移行 } return (n); // 文字数を返す } /* メイン関数 */ main() { char str[256]; // 文字列の入力 printf("文字列(255 文字以内) > "); scanf("%s", str); // 文字数の計算・表示 printf("%d 文字\n", mystrlen(str)); // mystrlen() と strlen() の結果は... printf("%d 文字\n", strlen(str)); // 同値のハズ }
前々回にも実験した通り, C言語ではプログラマの不注意によって, メモリ破壊とそれに伴うシステム異常とを簡単に実現できてしまう. 今回は文字列データについて,これを再び体験する.
文字配列に配列要素数よりも長い文字列を入力するとどうなるかを List 4 のプログラムで試してみよう.
main() { char a[4] = "ana"; char b[4] = "jal"; char c[4] = "hac"; // 配列 a, b, c の表示 printf("\n[初期値]\n"); printf("a = %s\n", a); printf("b = %s\n", b); printf("c = %s\n", c); // 配列 b だけの入力 printf("\n[入力]\n"); printf("文字列 b(3 文字以内)> "); scanf("%s", b); // 配列 a, b, c の表示 printf("\n[入力後]\n"); printf("a = %s\n", a); printf("b = %s\n", b); printf("c = %s\n", c); }
入力文字数を3文字以内から3文字以上へと順次増加しながら, 何度も実行し,結果を観察しよう. 文字配列 b に記録されるハズの入力データが他の配列に記録されたり, プログラムが実行時エラー(segmentation fault や bus error) によって強制終了されたりする. この実験結果を理解するには,メモリマップを考える必要がある. (これが本日の課題.)
なお,この例のような配列要素数以上のデータ入力によるメモリ破壊は, バッファオーバーフロー(buffer overflow)とか バッファオーバーラン(buffer overrun) と呼ばれている.
上級者向け参考文献:
バッファオーバーフロー等の不具合の原因となり得ることから, 世間では「scanf を使うのは良くない」と考えられている. 最近の実際的なプログラムでは,他の安全な入力方法を利用しようとしている.
しかし,scanf は簡単なので, この授業では「必要悪」として,使い続けることにする. なお,不完全だが簡単な予防策として, 文字配列を使う場合,要素数を大きめに (例:256 文字とか 1024 文字とかに) 設定しておこう. 必要最小限ギリギリの設定だと,オーバーフローの危険性が高い. または,scanf("%9s", str); とかやれば, 入力文字数を9文字に制限できる.
mm-4.c を改造し, 不具合発生時のメモリマップについて調査し, 実行結果について考察せよ.
配列 b に4文字以上の文字列を入力すると...
なぜなのか?
ヒント:
余裕のある人は, メモリマップに関する各自の疑問について, 実験プログラムを作成・活用して自己解決してみよう. また,scanf の問題点について,調べてみよう: