はとのーと

エジソンノート(アイデア、思い付き、メモ)として使っています。誰かの役に立つかもしれないので公開しています。

配列名とポインタ変数は相互互換ではない

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 の中身をアドレスとして取り出しています。