C言語 : ダブルポインタと二次元配列

ダブルポインタと二次元配列は別物であるという認識

次のコードを見てみる.

int foo[2][3];
int **ptr;
ptr = foo;

printf ("foo[1][2] = %d\n", ptr[1][2]);

これはポインタの型が違うのに代入してますよといって, 警告が出る. しかし無視して実行すると, 今度はセグメント違反が出る.

なぜか? これを考える.


上のコードは次の認識からきている.

int bar[3];
int *ptr2;
ptr2 = bar;

printf ("bar[2] = %d\n", ptr2[2]);

このコードは問題なく動く. bar は int*型 で, bar[0] へのアドレスを指している.
bar[2] = *(bar + 2) であるから, ptr[2] = *(ptr2 + 2) = *(bar + 2) であり, 配列に正しくアクセスできる.

これと同じ要領で一番上のコードが出来上がったわけだ.
つまり, 二次元配列は (int*)* と同じだと考えてしまったわけだ.
この間違いは, アクセスするときに順を追うと, 容易に理解できる.

foo[1][2];
ptr[1][2];

この二者を順番に見ていく.
以降 => という記号は「このように解釈できる」 という意味を指す.
まず, 配列アクセスの糖衣構文をばらす.

foo[1][2] => *(*(foo + 1) + 2)
ptr[1][2] => *(*(ptr + 1) + 2)

次に, ポインタを加算する. fooは(int[3])*型ポインタ, ptrはint**型ポインタなので, 次のようになる.

foo + 1 => (void*)foo + sizeof(int[3])
ptr + 1 => (void*)ptr + sizeof(int*)

(実際はこうではないと思います. あくまでどれくらい増加するかを表現したかっただけです.)
(2016/12/22 追記: void* に演算できるのはGCC拡張だそうで、他のコンパイラではchar*を用いるそうです。)
ここで違いが現れました. 64bit環境において実行すると, foo は 3byte 進むのに対して,
ptr は 8byte 進みます.

配列とポインタは別物ということです.

さらに進んでみましょう

*(foo + 1) => int[3] //*((int[3])*) => int[3]
*(ptr + 1) => int*   //*((int*)*)   => int*

&foo[0] は int* でしたが, int[3] は int* ではありません.
*(foo + 1) の返す int[3]型の値はアドレス foo + 1 にある int[3]型配列の「先頭アドレス」です.
対して
*(ptr + 1) の返す int*型の値はアドレス ptr + 1 にある int*型変数の「変数への参照そのもの」です.

セグメント違反した原因が見えてきました.

次に

*(foo + 1) + 2 => foo[1] + 2
*(ptr + 1) + 2 => *((int**)((void*)foo + sizeof(int*))) + 2 //同じ foo で比較しました.

(間違ってたらごめんなさい)
(2016/12/22 追記: 上でも述べましたが、void* への演算はGCCでのみ可能です。他の実装系ではchar* をお使いください。)

foo[1] は int* ですから, +2, とは +sizeof(int) * 2 ということです.
*(ptr + 1) は何らかの int型変数を指すアドレスの値(どんな値かは foo に代入した値による)ですので, +2 はfooと同じです.

しかし, 両者はもはや同じ値を指してません.
foo の方は有意義な値を指していますが,
ptr は foo の先頭から 8byte 進んだアドレスから先8byte 分の領域が表現する値をint*アドレスとみなして, そのアドレスの 8byte先のアドレス という, もはやむちゃくちゃな値を指しているというわけです.

そして仕上げに

*(*(foo + 1) + 2) => foo[1][2]
*(*(ptr + 1) + 2) => セグメント違反 //fooはすべて0で初期化しているので, おそらく 番地0x40 の値だと思います.

こうして, セグメント違反が引き起こされたわけです.

どこで違いが起きたかをおさらいすると
1. foo + 1 と ptr + 1 は増加アドレスが違う
2. *(foo + 1) と *(ptr + 1) は型も違えば取り出せる値も違う.

では, 二次元配列をポインタに代入したいときはどうすればいいのでしょうか?
答えは, 型を一緒にするということです.

次のコードは正常に動作します.

#include <stdio.h>
int main(void)
{
    int foo[2][3] = {0}; //(int[3])*型
    int (*ptr)[3];       //(int[3])*型
    ptr = &foo;          //foo が &(foo[0]) を指すのに対して, &foo は &(配列としてのfoo) をさす.

    printf ("foo[1][2] = %d\n", ptr[1][2]);
    //foo[1][2] = 0 と正しく表示される.

    return 0;
}

int (*ptr)[3]; の意味がよくわからない場合は, 次のサイトが役立ちます.
やり直しC言語:複雑な宣言の読み方: Architect Note

おまけ

次の二つのコードは問題がありますが, 値を返してくれます.
1.

int foo[2][3] = {{1, 2, 3}, {4, 5, 6}};
int **ptr;
ptr = (int**)foo;

printf ("foo[0][0] = %d\n", **(ptr + 0)); //foo[0][0] = 1 と正しく表示される.

2. 間接参照演算子が一回しか使われていないことに注目

int foo[2][3] = {{1, 2, 3}, {4, 5, 6}};
int **ptr;
ptr = (int**)foo;

printf ("foo[1][1] = %d\n", *((int*)((void*)ptr + sizeof(int[3])) + 1); //foo[1][1] = 5 と正しく表示される.

(2016/12/22 追記: 申し訳ありませんが、GCCでしか正常に動作しません。)

今回はここまで

Raptor

(2019/12/31: 助詞の間違いを修正)