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

C/C++のポインタの機能--変数の場所(アドレス)という記事が一時お祭り状態になっていたようです.内容云々に関しては様々な人が指摘しているので置いておいて,今回はポインタ関連で理解に時間がかかった事例を一つ紹介してみようと思います.

// sample1
#include <stdio.h>

int main() {
    char* hello = "Hello, world.";
    printf("%s\n", hello);
    return 0;
}

Cでは,上記のプログラムは正常に動作します.しかし,このプログラムを最初に見たとき2行目の記述に違和感を覚えました.具体的に言うと,領域を確保しないままcharポインタに直接文字列を挿入してるけど大丈夫なのかな?というものです.私の想像でしかありませんが,上記のコードが,先の記事のように,ポインタに対して値を代入すると自動的にその値を記憶するための領域が確保されると言った誤解(記事のコードを見る限りはそう言った誤解をしているように見えたのだけど,実際はどうだったのだろう?)を招く原因の一つになっているのではと思います.

また,当初は以下のコードとの違いも良く分かりませんでした.

// sample2
#include <stdio.h>

int main() {
    char hello[] = "Hello, world.";
    printf("%s\n", hello);
    return 0;
}

そこで,上記2つのコード(sample1, sample2)いろいろ書き換えているうちにようやくどのような動きをしているのかが分かってきました.

まず,文字列リテラル(上記のコードだと"Hello, world.")を記述すると,システムはその文字列を記憶するための領域を確保して文字列を記憶した後,その記憶領域の先頭アドレスを返します.sample1は,特別な初期化を行っている訳ではなく,ポインタ変数にその先頭アドレスを代入しているにすぎません.そのため,以下のようなコードも正常に動作します.

// sample3
#include <stdio.h>

int main() {
    char* hello;
    hello = "Hello, world.";
    printf("%s\n", hello);
    return 0;
}

一方で,sample2のように記述した場合には(恐らくシステムが文字列リテラルのための領域を確保してその先頭アドレスを返した後に)配列の初期化処理が始まり,文字列リテラル(上記のコードだと"Hello, world.")のコピーが行われます.配列の場合の変数名(上記のコードだとhello)は内容を参照する場合(*hello, hello[1]など)はポインタ変数と同じように扱うことができますが,その変数の値を変更することはできません(hello++など).そのため,次のようなコードはコンパイルエラーとなります.

// sample4
#include <stdio.h>

int main() {
    char hello[16];
    hello = "Hello, world.";
    printf("%s\n", hello);
    return 0;
}
コンパイル結果
$ gcc -Wall -o test sample4.c
sample4.c: In function `main':
sample4.c:5: error: incompatible types in assignment

また,sample1の場合,ポインタは文字列リテラルを記憶している領域(の先頭アドレス)を指しているため,ポインタの指している内容を変更しようとすると予期せぬエラーが発生します.例えば,テストした環境(cygwin + gcc 3.4.4)では以下のコードはコアを吐いて異常終了しました.

// sample5
#include <stdio.h>

int main() {
    char* hello = "Hello, world.";
    hello[0] = 'h';
    printf("%s\n", hello);
    return 0;
}
実行結果
$ ./test
Segmentation fault (core dumped)

しかし,sample2の場合は(内容を変更可能な)配列を用意してその配列に文字列リテラルをコピーしたため,ポインタが指している先の内容を変更しても正常に動作します.

// sample6
#include <stdio.h>

int main() {
    char hello[] = "Hello, world.";
    hello[0] = 'h';
    printf("%s\n", hello);
    return 0;
}
実行結果
$ ./test
hello, world.

以上です.私自身も理解があやふやな状態で実際に実行するまで分かっていなかった部分もありました.それと,このテストをしていて分かったことですが,sample5のコードは-Wallオプションをつけても警告なしでコンパイル完了するんですね.私の環境だと実行時にコアを吐いて終了しましたが,場合によっては普通に動いてしまうこともあるようです.そんな訳で,今回のテストで得た教訓は,文字列リテラルのポインタにはきちんとconstを付けましょうでした.

// sample7
#include <stdio.h>

int main() {
    const char* hello = "Hello, world.";
    hello[0] = 'h';
    printf("%s\n", hello);
    return 0;
}
コンパイル結果
$ gcc -Wall -o test sample7.c
sample7.c: In function `main':
sample7.c:5: error: assignment of read-only location

プログラミングをしていると,その言語毎に定石と呼ばれているようなことに数多く遭遇しますが(C++だとEffective C++に載っているような事でしょうか.Cだと何だろ?K&qmp;R本?),その多くは実際に自分がつまづくまでその恩恵が理解できないことも多いように思います(知識として知ってはいてもうまく使いこなせないと言った方が正しいでしょうか).その意味では,ガンガン書いてたくさんつまづくしかないのかな,と感じました.

マーフィーに微笑まれないクリティカルな場面で躓かないことを祈りつつ.