配列とポインターと、そして文字列リテラルとかの話

きっかけ

C/C++のポインタの機能--配列との関係 - builder
「 *s 」と「 s[] 」の違い - IT戦記
C 言語の配列について - IT戦記
なにやらポインターとか配列とかでいろいろ盛り上がっていたようですね。
遅ればせながら、私も参戦してみたいと思います。

というのも、新人教育の副講師としてプログラミング言語 C を新人たち*1に教えたりしていたのですが、まさに amachang さんのエントリー名のとおりの "「 *s 」と「 s[] 」の違い" でつまずいてしまったのです。

新人教育では、このコードと図のようなものをプロジェクターで映し出して説明してみました。

char a[] = "hoge";
char a[] = { 'h', 'o', 'g', 'e', '\0' }; /* ↑と同義! */

char *b = "fuga";
/* const char* b = "fuga"; (C++) */

char *c[] = { "hoge", "fuga", "moga", "fuge" };


10 分ほど説明した結果、いろいろ惨敗してしまったので、反省をこめて以下を書き起こしてみました。

配列とポインターと、そして文字列リテラルとかの話

はじめに

適当なこと言ってる可能性大なので、どしどし突っ込みお願いします!

いきなり結論

突然ですが、ここに例をあげます。

char str1[] = "hoge"; /* (A) */
char *str2 = "hoge"; /* (B) */

printf("%s, %s\n", str1, str2);

(A) と (B) でそれぞれ定義した変数(配列)ですが、printf の引数としてわたす時は同じように名前だけ記述しています。こうすることでどちらとも、"hoge" を出力することができます。
この 2 つの変数(配列)の定義、一見似ていますが実は全然違います。

結論から言ってしまうと、これらは変数(配列)の定義と同時に、(A) は配列の初期化、(B) はポインタの初期化を行っています!!

とまぁ、これだけじゃさっきの図と何も変わらないので少しずつ順を追って説明していきます。

文字リテラル (文字定数)

文字リテラルとは、'A' のように、ASCII 文字 1 文字をシングルクオート "'" で囲ったやつですね。
この式が評価されると、A という文字をあらわす ASCII コードの 0x41 *2が返ってきます。

とまぁ、このようにしてプログラミング言語 C では文字をあらわすわけですが、もちろん文字列をあらわす構文も存在します。

文字列リテラル (文字列定数)

文字列リテラルを用いると、"ABC" のように文字"列"を表すことができます。

プログラムではデータを扱うときに、データをメモリ上に格納します。
もちろん文字列もメモリ上に置かれます。
先ほど述べたように、プログラミング言語 C では ASCII コードで文字をあらわすのですが、文字列 "ABC" は 0x41, 0x42, 0x43, 0x00 というを数値を順に格納する事で表現します。
なんか、一番後ろに 0x00 とかいうのがついてますが、これは文字列の終端をあらわすもので NUL 文字とかヌル文字とかナル文字とか言います。*3
これは文字リテラルでは '\0' と書けます。

ということで、"ABC" と書くと、C コンパイラがメモリ上のどこかに 'A', 'B', 'C', '\0' という文字をあらわす値を書き込んでくれます。
そして、この "ABC" という式を評価すると、先頭の文字 'A' が格納されたメモリ領域を示すポインタが返ってきます!

ポインタと文字列リテラル

ここで、はじめの例に戻ってみましょう...。

char *str2 = "hoge"; /* (B) */

はい。もうわかりますね!
この文の意味は、'h', 'o', 'g', 'e', '\0' というデータがメモリ上のどこかに格納されて、'h' のところを指し示すポインタで str2 が初期化される、ということをあらわしているのです!

配列の初期化

変わって、今度は配列の話です。

int array[3];

これは、int 型の値が 3 個入る配列の定義ですが、定義と同時に初期化する場合はこうしますよね。

int array[3] = { 0, 1, 2 };

こうも書けます。

int array[] = { 0, 1, 2 };

定義の要素数を省略するとコンパイラが自動的に配列の大きさを決定してくれます。

逆に、右側を省略すると...

int array2[3] = { 1 };

こうすると、array2 には 1, 0, 0 という値が入ります。
足りない部分はコンパイラが自動的に 0 で埋めてくれるわけです。

これを使って、よくこんな風にして配列を初期化したりします。(同じ方法で構造体も初期化できますよね。)

int array3[3] = { 0 };
文字列リテラルによる配列の初期化

で、ここで突然話を戻しますが、はじめの例を見てください。

char str1[] = "hoge"; /* (A) */

実はこれは、以下のシンタックスシュガー、つまり、まったく同じ事なんです。*4

char str1[] = { 'h', 'o', 'g', 'e', '\0' };

つまり、(A) は、文字列リテラルを用いた char 型配列の初期化を省略して書いているだけだったのです。

etc...

この事実を使うと、

char str3[10] = "";

これは、

char str3[10] = { '\0' };

と同じ、すなわち

char str3[10] = { 0 };

これと同じなので、str3 という配列の要素 10 個を '\0' で埋めたりできます。*5

書き換え可能とか、不可能とか

ここで、またまた話をもどします。

char str1[] = "hoge"; /* (A) */
char *str2 = "hoge"; /* (B) */

printf("%s, %s\n", str1, str2);

ここまでで、この (A), (B) が、同じように参照できるけど、なんか別物らしいっていう感じはつかめたと思います。
もうちょっと言うと、(A) は、単なる配列なので 'h', 'o', 'g', 'e', '\0' というデータはスタックというところに置かれます。*6

str1[3] = 'a';
  /* -> "hoga" になる */

こんなことして、書き換えたい放題です。

(B) の場合はちょっと違います。
"hoge" というデータがメモリ上のどこかに格納された上で str2 にポインタが入っているのですが、その "hoge" が格納されている場所が問題です。
この "hoge" が格納されている場所を書き換えた場合の結果は不定とされています。*7
ので、

str2[3] = 'a';

とかやったらだめです。場合によっては Segmentation Fault でプログラムが落ちます。

とはいえ、昔の規格ではこれは書き換え可能だったらしく(?) gcc では、書き換え可能にするオプションがあったりします。(-fwritable-strings)


-fwritable-strings
文字列定数を書き込み可能なデータセグメントに配置し、同内容の文字列を 1 つの共有オブジェクトにする処理を行いません。これは、文字定数に書き込むことができることを仮定した昔のプログラムとの互換性をとるために提供されています。`-traditional' オプションも同様の効果を含みます。

文字定数に書き込むという考えは非常によくない考えです。``定数'' はまさに定数であり、変化すべきではありません。

配列とポインタ

そういえば、(A) は配列、(B) はポインタであるのにどうして同じように扱えるのかを解説するのを忘れていました。
プログラミング言語 C には、以下の決まりがあります。

  • foo[bar] は *(foo + bar) のシンタックスシュガーである
  • int array[]; という配列 array があったとき、array を評価すると array の先頭要素のポインタを返す。

よって、*(str + 1) と str[1] と 1[str] が同じものを意味したりします。

ということで、なんとなく配列とポインタに対して同じような扱い方ができる事がわかりますね。

プログラミング言語C 第2版 ANSI規格準拠

プログラミング言語C 第2版 ANSI規格準拠

*1:っても自分は 2008 年 4 月から 2 年目ですけど...

*2:10 進数だと 65 ですね。

*3:教えてるときは、NULL ポインタと混同している人が結構居ました。EOF と混同している人も...。

*4:[asin:4320026926:title]: 4.9 初期化 参照

*5:規格にのっとった実装ならできるはず…、できない実装もあるらしい?

*6:auto 変数の場合...

*7:[asin:4320026926:title]: 5.5 文字ポインタと関数 参照