C言語の配列とポインタの違いのお話など

今回はちょっと技術的な話を。

私も顔を出している西脇.rb/東灘.rb のメンバーの方からC言語の質問を受けました。
Rubyの勉強会なのにC言語の話題も出てくる、というところがこのコミュニティのよいところです。懐の深さやメンバーの方の強い向上心を感じますね。

その方、Rubyはバリバリですが、C言語は最近使い始めたそうで、

#include <stdio.h>
main()
{
    char  str[] = "hogehoge";   …(1)
    char *str = "hogehoge";     …(2)
    *str = 'f';
    printf("%s\n", str);
}

こういうコードを書いたとき、文字列の設定先を配列にするかポインタにするかで動きが変わるのはなぜか?不思議だったそうです。

char  str[] = "hogehoge";   …(1)

のときは、先頭1文字がfに置き換えられて、狙い通り

fogehoge

と出力されるのに、

char *str = "hogehoge";     …(2)

のときは、

Bus error: 10

とエラーになってしまうのはどうして?という疑問でした。
mac osの場合はBus error、Linuxの場合はSegmentation faultになるなど、OSによって現象は異なります。

たしかに、「C言語ではポインタと配列を区別せず、同じように扱える」と言われていますから、なぜ動きが変わってしまうのか不思議に思えますね。

この問題はわりと有名みたいで、他のブログなどでもよく話題になっているようですが、私なりの説明をしてみたいと思います。

まず、

char  str[] = "hogehoge";   …(1)

は、C言語で配列を初期化するときのシンタックスシュガーで、

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

と同義です。

str[]はローカル変数なので、関数が呼び出されたときにstrという配列用の領域が獲得され、ここに"hogehoge"という文字列が設定されることになります。

この配列は、スタック領域(stack)という読み書き可能な領域に存在します。

一方、

char *str = "hogehoge";     …(2)

のときは、コンパイルされた時点で、"hogehoge"という文字列リテラルが実行ファイル内に設定されています。そして、関数が呼び出されたときに、ポインタ変数strが文字列リテラルの先頭アドレスで初期化されます。

この文字列リテラルは、リードオンリーデータ領域(.rodata)という、読み取りのみ可能な領域に存在します。

つまり、

char  str[] = "hogehoge";   …(1)

は、読み書き可能なスタック領域に文字列が存在するので、文字列を書き換えできる。

char *str = "hogehoge";     …(2)

は、リードオンリーな領域を指し示しているので、書き換えようとするとBus errorとなる。

ということです。

これは、C言語の規約上「文字列リテラルは定数として使用すべきものであり、書き換えた場合の動作は不定である」とされていることに起因しています。そのため書き換えできないようにしている処理系が多いようです(中には書き換えできてしまう処理系もあるようですが…)

C言語は昔よく使ってましたが、最近はややご無沙汰してました。わかっていたつもりでも説明しようとすると難しかったです。
たまには以前習得した技術を棚卸しし、アウトプットしてみるのもいいものですね。