C言語の配列名とポインタ変数は相互互換ではないことがわかったのでメモ。
ポインタと配列の微妙な関係 - めもめも には char[] で定義した文字列を extern char * で参照すると正しく扱えないと書いてあります。
本当かどうか実験してみました。
実験
次のような3つのファイルを考えます。
main1.c:
#include <stdio.h> extern char hello[]; int main(void) { puts(hello); return 0; }
main2.c:
#include <stdio.h> extern char *hello; int main(void) { puts(hello); return 0; }
sub.c:
const char hello[] = "Hello world.";
sub.c
で定義した文字列 hello
を読み込んでメインで表示していますが、helloの読み込み方に次の違いがあります。
- main1.c -
extern char hello[]
- main2.c -
extern char *hello
実行します。
main1.c:
$ gcc main1.c sub.c -o main1 && ./main1
Hello world.
main2.c:
$ gcc main2.c sub.c -o main2 && ./main2 Segmentation fault (core dumped)
main2の方だけ Segmentation fault が発生します。 配列名とポインタ変数が互換ではないことがわかりました。
説明
この現象について C FAQ 1 によい説明がありました。
char a[] = "hello"; char *p = "world";
は以下のように表現できるデータ構造を初期化する。
+---+---+---+---+---+---+ a: | h | e | l | l | o |\0 | +---+---+---+---+---+---+ +-----+ +---+---+---+---+---+---+ p: | *======> | w | o | r | l | d |\0 | +-----+ +---+---+---+---+---+---+
x[3]を参照したときに産み出されるコードが、xがポインターか配列 かで違うのだと理解することは大事なことである。上記の宣言を与えられたとして、コンパイラはa[3]という式を見たところで、「a」のところから始めて、そこから3つ進んで、そこにある文字を取り出す、というコードをはきだす。p[3]という式を見ると、「p」に進み、そこに存在するポインターの値を取り出し、ポインターの値に3を加え、 最後にポインターが指す場所から文字を取り出す、というコードをはきだす。
どうやら a
はアドレス値(定数)なのでそのまま使え、p
は変数であるため中身のアドレスを取り出す必要がある(p
自体のアドレスではない)ということのようです。
確認
実際にそうなっているかアセンブリ言語で確認してみました。
以下のようにすると、それぞれ main1.s, main2.s のアセンブリソースコードが出力されます。
$ gcc -S main1.c $ gcc -S main2.c
main1.s:
.file "main1.c" .text .globl main .type main, @function main: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $hello, %edi # <------ call puts movl $0, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (GNU) 11.3.0" .section .note.GNU-stack,"",@progbits
main2.s:
.file "main2.c" .text .globl main .type main, @function main: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movq hello(%rip), %rax # <------ movq %rax, %rdi call puts movl $0, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (GNU) 11.3.0" .section .note.GNU-stack,"",@progbits
main1.s の方では movl $hello, %edi
で hello のアドレスをそのまま使っているのに対し、main2.s の方では movq hello(%rip), %rax
で hello の中身をアドレスとして取り出しています。