ポインタ変数って型指定する意味あるん?


C言語のポインタで長年疑問に思ってたんですが、ポインタってアドレスを示すわけですよね?そしたらアドレスさえ代入できれば型を指定する必要なくない?と。とりあえず下記のサンプルコードをベースに考えてみる事にしました。

#include <stdio.h>

int main(){

        int i = 100;

        printf("i   = %d\n"  , i  );
        printf("&i  = 0x%x\n", &i );

        int *pointer_to_i;
        pointer_to_i = &i;

        printf("pointer_to_i   = 0x%x\n", pointer_to_i        );
        printf("*(pinter_to_i) = %d\n"  , *(pointer_to_i)     );

        return(0);

}

内容はいたってシンプル。i=100をした後、そのポインタをpointer_to_iに代入して、最後の方はポインタから参照してるだけです。実行するとこんな結果になります。

=====================
i   = 100
&i  = 0xe2149404
pointer_to_i   = 0xe2149404
*(pinter_to_i) = 100
=====================

まぁ、当然といえば当然の結果です。そしたら試しに、pointer_to_iをchar型にしてみましょう。

< int *pointer_to_i;
> char *pointer_to_i;
gcc pointer.c
pointer.c: In function ‘main’:
pointer.c:11:15: warning: assignment from incompatible pointer type [-Wincompatible-pointer-types]
  pointer_to_i = &i;
               ^
=====================
i   = 100
%i = 0x5e88c5c4
pointer_to_i   = 0x5e88c5c4
*(pinter_to_i) = 100
sizeof         = 8
=====================

コンパイル時にwarningが出ますが、問題なく動いています。

やっぱり、型指定する意味ないじゃん?

ポインタ変数はアドレスが代入されているので、int、char, longどんな型でもポインタ変数自体は同じ大きさです。

int  *p1;
char *p2;
long *p3;
printf("*p1 size: %d\n", sizeof(p1));
printf("*p2 size: %d\n", sizeof(p2));
printf("*p3 size: %d\n", sizeof(p3));

=====================
*p1 size: 8
*p2 size: 8
*p3 size: 8
=====================

じゃぁやっぱり、型指定意味ないじゃん?って思うかもしれませんが、下記で問題が起きます。

int i[5] = { 1, 2, 3, 4, 5};
char *pointer_to_i = i;

printf(" pointer_to_i     = 0x%x\n",  pointer_to_i);
printf("*pointer_to_i     = %d\n"  , *pointer_to_i);
printf("\n");

printf("  pointer_to_i+1  = 0x%x\n",   pointer_to_i+1);
printf("*(pointer_to_i+1) = %d\n"  , *(pointer_to_i+1));
printf("\n");

printf("  pointer_to_i+2  = 0x%x\n",   pointer_to_i+2);
printf("*(pointer_to_i+2) = %d\n"  , *(pointer_to_i+2));
printf("\n");

printf("  pointer_to_i+3  = 0x%x\n",   pointer_to_i+3);
printf("*(pointer_to_i+3) = %d\n"  , *(pointer_to_i+3));
printf("\n");

printf("  pointer_to_i+4  = 0x%x\n",   pointer_to_i+4);
printf("*(pointer_to_i+4) = %d\n"  , *(pointer_to_i+4));
printf("\n");

このコードはint型5個の配列ですが、それを示すポインタ変数をあえてchar型にしています。実行してみると下記のような結果になります。

=====================
 pointer_to_i     = 0x1c5a7e00
*pointer_to_i     = 1

  pointer_to_i+1  = 0x1c5a7e01
*(pointer_to_i+1) = 0

  pointer_to_i+2  = 0x1c5a7e02
*(pointer_to_i+2) = 0

  pointer_to_i+3  = 0x1c5a7e03
*(pointer_to_i+3) = 0

  pointer_to_i+4  = 0x1c5a7e04
*(pointer_to_i+4) = 2
=====================

一見するとポインタは1つずつ増えていって、正しいアドレスを示しているかのように見えますが、中身が変です。1の次は2が出てくるはずなのに、ここでは0が出て、ポインタを+4してやっと2が出てきました。

int型は1バイトじゃない

察しが良い方はもうお気づきでしょう。そうです、int型は1バイトではありません。処理系によって左右しますが、筆者環境では8バイトでした。なので配列を次に進めるにはアドレスを+8する必要があるのです。

今回のコード、配列に代入した段階でメモリ上は下記のようにデータが格納されています。

先頭アドレス
  0x1c5a7e00 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 ...
       中 身 01 00 00 00 02 00 00 00 03 00 ...
             [0]~~~~~~~~ [2]~~~~~~~~ [3]~~~...

int型は8バイトに対してchar型は1バイトなので、ポインタを+1をすると先頭+1のアドレスを参照しに行きます。しかしここはまだi[0]の領域で、その中身は00です。そしてポインタを+4した時やっとi[2]にたどり着き、02が表示されます。

ちなみに何で00 00 00 01じゃなくて、 01 00 00 00なの?と疑問に思った方はリトルエンディアンでググってみましょう。

やっぱりポインタも型は大事

以上の事から、やっぱり型をちゃんと指定しないとトラブルの元になりますが、正しくない指定の仕方をした時にどういう動きをするのか確認する事も大事だなと今回感じました。