注意:本記事のタイトル・内容は不正確です(2018/09/08 追記)
初出時の内容では構造体のポインターフィールドを「配列」で初期化していませんでした。(コメントありがとうございます)
ポインターを配列で初期化するためには、初出時のとおり配列を別に定義して参照するか、あるいはコメントでいただいた「複合リテラル」を使うか、いずれかの方法を取る必要があります1。
struct foo { char *p; };
/* method 1: initialize by defined array. */
char hello[] = {'h', 'e', 'l', 'l', 'o',};
struct foo f1 = {hello};
/* method 2: initialize by compound literal. */
struct foo f2 = {(char[]){'h', 'e', 'l', 'l', 'o',}};
間違いの原因は「初期化リスト」を「配列」を混同したことです。これについて「[メモ]C言語配列の本体」を書きました。本記事をお読みのみなさまの一助になれば幸いです。
以下は初出時のまま、変更を加えていません。読み進める場合は、配列と初期化リストの混同に注意してください。
ポインターフィールドはインラインの配列で初期化できない
構造体に文字型ポインターを持たせたときに、これを文字列で初期化できる。
struct foo {
char *string;
};
void some_fn() {
struct foo value = {"hello"};
...
この伝でフィールドを配列でも初期化できるか? というと、不可。
void another_fn() {
struct foo value = {{'h', 'e', 'l', 'l', 'o', '\0'}};
...
コンパイラーに処理させると次のような警告が表示される。
local_var.c:11:24: warning: incompatible integer to pointer conversion initializing 'char *' with an expression of type 'int' [-Wint-conversion]
struct foo value = {{'h', 'e', 'l', 'l', 'o', '\0'}};
^~~
local_var.c:11:29: warning: excess elements in scalar initializer
struct foo value = {{'h', 'e', 'l', 'l', 'o', '\0'}};
^~~
2 warnings generated.
コンパイラーのバージョンは次のとおり。
Apple LLVM version 9.1.0 (clang-902.0.39.2)
Target: x86_64-apple-darwin17.7.0
インライン配列でポインターフィールドを初期化できたとしたら?
配列ストレージの確保場所を考えると、これで初期化できたらまずそうだと想像できる。
another_fn
で期待したように、その場で確保した配列のアドレスがポインターに暗黙で変換されるとする。そのようにして初期化した value
を構造体のコピーで関数の外に戻すとする。
配列は another_fn
のローカル変数として宣言されていたので、つまり、スタック上に確保されている。ここで another_fn
の呼び出し元が別の関数を呼び出した状況を考えてみよう。
struct foo another_fn() {
struct foo value = {{'h', 'e', 'l', 'l', 'o', '\0'}};
return value;
}
void print_foo(struct foo* foo) {
int i;
for (i = 0; i < strlen(foo->string); ++i) {
printf("%c", foo->string[i]);
}
}
int main() {
struct foo value = another_fn();
print_foo(&foo);
...
そもそも警告が出て期待したようにはコンパイルされないから想像してもらうしかないのだけれど、ローカル(スタック)に確保した配列を戻して、別の関数でスタックを利用したときに起きる事故は考えたくもない。
この場合は print_foo
を呼び出した時点で、 another_fn
が構築したスタック上の struct foo value
は綺麗さっぱり破壊される。(i = 0
によってゼロクリア、と言いたいところだけれど print_foo
が引数をスタックにプッシュするので、そのアドレスで破壊される。どんな文字列長になってどれほどループが回るかは神のみぞ知る)
余談だけれど、次のように書けばコンパイルも通るし合法。暇があるようだったら、何が起きるか試してみるといい。
struct foo another_fn() {
char string[] = {'h', 'e', 'l', 'l', 'o', '\0'};
struct foo value = {string};
return value;
}
警告の解説
丁寧に警告を読むと、なにを間違っていてどう直せばいいのかヒントが得られる。
incompatible integer to pointer conversion initializing 'char *' with an expression of type 'int'
以下 struct foo
の定義に対して value
の最初の 'h'
が整数値 int
でありポインター char *
と互換性がないことを警告されている。
struct foo { char *string; };
struct foo value = {{'h', 'e', 'l', 'l', 'o', '\0'}};
コンパイルは通ってしまうので、 'h'
の値が文字列を格納するアドレスだとみなされていることになる。
excess elements in scalar initializer
struct foo
はポインターをひとつしか持っていない。これに対して6つの要素を指定しているので初期化子の数が多すぎるよ! という警告。
struct foo { char *string; };
struct foo value = {{'h', 'e', 'l', 'l', 'o', '\0'}};
文字列のストレージ
以下が問題なく通る理由も見ておこう。
struct foo { char *string; };
struct foo value = {"hello"};
これをアセンブリに変換してみる。
$ gcc -S -x c - <<EOF; cat ./-.s
> struct foo { char *string; };
> struct foo value = {"hello"};
> EOF
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 13
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "hello"
.section __DATA,__data
.globl _value ## @value
.p2align 3
_value:
.quad L_.str
.subsections_via_symbols
はい。
コードセクション(text)に "hello"
が確保されている。
構造体 foo
は文字列ポインターと同じサイズでしかないためストリップされデータセクションにグローバルシンボル _value
が確保される。そしてそのアドレスはコードセクションの "hello"
に張られたラベル L_.str
を指している。
C コード中の文字列リテラルは、初期化済み配列オブジェクトを指すシンタックスシュガーだとおもえばいい。
-
文字列リテラルでポインターを初期化する方法もありますが、文字列リテラルが「配列」と同じかどうか、規格書で確認が取れていません。実装上、文字列リテラルが配列のように振る舞うことは確かだとおもいますが……。 ↩