ポインタ虎の巻

ポインタの型

ここでは、変数には、整数型int,文字型char,浮動小数点型doubleなどの型があることに注意してポインタを見て行こう。

これらの変数の型は、その変数の値が必要とするメモリのサイズの情報を持っている。一般的なUNIXでは、int = 4byte, char=1byte, double=8byte が普通であり、変数の型に合わせてその変数がメモリ上で占めるべきサイズが確保されている。サイズを知るためには

printf("size of int=%d\n", sizeof(int)); 
printf("size of char=%d\n", sizeof(char)); 
printf("size of double=%d\n", sizeof(double)); 
printf("size of pointer to char=%d\n", sizeof(char *)); 
printf("size of pointer to double=%d\n", sizeof(double *)); 
(プログラム5−1)

の要領で表示してみるといい。

ポインタ自体はアドレスを表す数値に過ぎないために、通常 int と同じサイズであるが、そのポインタが指し示す対象のサイズは千差万別である。そのために、ポインタにも型が存在する。だからポインタを宣言するときに、そのポインタが何を示すのかを明示して宣言する必要がある。

実はアセンブリのレベルでは、単なるアドレスを示す数値であるために、ポインタの型は存在しない。しかし、ポインタの型を区別し、代入関係をチェックすることで、ポインタのミスを未然に防ぐことができるため、C言語ではポインタの型を積極的にチェックしているのである。また、ポインタが指し示すデータオブジェクトのサイズが千差万別であることは、ポインタの加算について実は特別な扱いを要することになる。

先程の例では、forループの中で、"p++;" の式でポインタを加算している。ポインタ変数pが指し示すデータ・オブジェクトはint型であり、そのサイズは4byteである。そのために、配列xが1000番地から始まるとすると、&x[0] = 1000, &x[1] = 1004, &x[2] = 1008, &x[3] = 1012 という風に4つおきにアドレスが取得されることになる。だから、ポインタ変数pに1000番地のアドレスが入っているとすると、ポインタ変数の加算によって次のデータオブジェクトを指し示すのならば、p+1 = 1004 になるのである。つまり、数値の加算とは違って、ポインタの加算は以下の式で計算される。

p + sizeof(ポインタが示す型) * 増やす増分

もう一度確認するが、ポインタ自体は単なるint値を同じサイズしかない。だから、ポインタはint値にもキャストすることができるし、他の型のポインタにキャストすることも出来る。以下の例はトリッキーな例であり、決して真似すべきプログラムではないが、この事情をうまく説明できる。

char s[4] = { 1, 2, 3, 4 };     
   /* たとえば1000番地(1)から1003番地(4)まで */
int *p;

void main()
{
        p = (int *)s;      /* p = 1000 */
        /* 1000番地からintのサイズ分(4byte)を0にする */
        *p = 0;     
        printf( "%d %d %d %d\n", s[0], s[1], s[2], s[3] );
}
(プログラム5−2)

本来配列名sはchar型のポインタである。その数値を強引にint型ポインタにキャストしてやって、int型ポインタpに代入する。そうすると、ポインタpに0を代入すれば、s[0]が0になるだけではなくて、配列sの4つの要素が0になる。これはポインタの型を強引に読み替えて代入したために、こういう効果が得られるのである。

このようにポインタの型は強引にキャストして別な型として読み替えることもできるが、そうではなくて、特に対象を明示しないポインタとして定義することもできる。このような指し示す対象を明示しないポインタは void *(ヴォイドポインター) と呼ばれる。void * は実際に値を参照するときには必ずキャストしなければならない。なぜなら、指し示す対象のデータオブジェクトのサイズもまったく不明であるからである。だからvoid *では、p = p + 1 はサイズが判らないために、計算ができない。計算をするためには、

        p = (char *)p + 1;      /* p = p + 1 */
        p = (int *)p + 1;       /* p = p + 4 */
(プログラム5−3)

の要領で、キャストをしなくてはならない。

void * の使用例を表示する。

int x = 10;
char *s = `"test";
double y = 3.1415;
void *p;

void display( void *p, int type )
{
        switch( type ) {
        case 0:  /* int型の時 */
                printf( "int: %d\n", *(int *)p );
                break;
        case 1:  /* 文字列を渡している時 */
                printf( "string: %s\n", (char *)p );
                break;
        case 2:  /* double型の時 */
                printf( "double: %lf\n", *(double *)p );
                break;
        }
}

void main()
{
        p = (void *)&x;
        display( p, 0 );
        p = (void *)s;
        display( p, 1 );
        p = (void *)&y;
        display( p, 2 );
}
(プログラム5−4)

以上の例はややトリッキーであるが、構造体ポインタが絡むとこのような扱いをすると便利な場合がある。

intの数値とポインタは実質的には同じものである。ということは、Cのプログラムで直接に数値をポインタにキャストして使うことができるだろうか、という問題を考えてみよう。Cのプログラムでも、intの数値を直接ポインタにキャストすることは可能である。

void main()
{
        char *p;
        p = (char *)0;
        *p = 10;
}

これは、ごく簡単にセグメンテーション・フォルト(メモリの保護違反)を引き起こすためのプログラムである。つまり、コンパイルは可能であり、合法なC言語である。ただし、代入されたアドレス0をアクセスすると、セグメンテーション・フォルトが起きるというUNIXの常識により、CPUによって停止させられるのである。

ということは、そうではないアドレスをポインタとして代入し、何かの機能を実現することはあるのだろうか。実際にはカーネルのコードや、デバイスドライバでは、実メモリの上での特定のアドレスが、周辺機器への入出力ポートとなっている例があり、それらに対するアクセスは、上記のように即値にキャストをしてポインタに代入してアクセスする。とはいえ、一般のアプリケーション・プログラムではほとんど現われることはないだろう。(root特権を持っていて、SVGAlibのプログラミングをするときくらいかな?)

課題

  1. 強引にポインタをキャストすると、情報を落したりすることがある。以下の例でどういう出力が得られるか考えてから、確認せよ。結果はIntelなどのリトル・エンディアン・CPUと、PowerPCなどのビッグ・エンディアン・CPUとで異なる。
    int value = 256; /* 適当に値は変えてみる */
    void main()
    {
            char *p;
            p = (char *)&value;
            printf( "char value = %d\n", *p );
    }
    
  2. 強引なキャストを使うと、ビットパターンがintとfloat(共に4byte)で同じ数値を探すプログラムを書くことが出来る。それを書いて、どの数値がそうであるか探してみよ。プログラムのヒントは、
            unsigned int i;
            /* unsigned int の最大値まで */
            for( i = 0u; i < 4294967295u; i++ ){ 
                    ...................
            }
    
    の要領でループを作ればいい。若干実行時間がかかるが探すことができるはずである。


copyright by K.Sugiura, 1996-2006