C言語
印刷用ページへ

C言語におけるポインタ型変数の宣言

ポインタはこれまでC言語の鬼門と呼ばれてきましたが、それはC言語設計者が書いた一文が誘因となっています。本稿ではポインタを巡るC言語の問題点と、ポインタ型を意識してコーディングする当社の方針について解説します。

整数型変数の割り当てアドレス

ポインタについて検討する前に、整数型変数のメモリー領域への割り当て状況を見てみましょう。

#include <stdio.h>

int a = 0x12345678;

int main() {
    printf("a = 0x%X\n", a);
    return 0;
}

int.cは、3行目で整数型変数aを定義し(0x12345678で初期化)、その値を6行目で表示するプログラムです。

$ cc -Wall -o int int.c
$ ./int
a = 0x12345678

意図通り動作しています。次に、変数aの格納アドレスを併せて表示させるように手を加えます。

#include <stdio.h>

int a = 0x12345678;

int main() {
    printf("a = 0x%X\n", a);
    printf("a @ %p\n", &a);
    return 0;
}

int_address.cは、新たに7行目にてアドレス演算子&を用いて、変数aの格納開始アドレスを表示させます(%pはポインタの値を表示するための指定子)。

$ cc -Wall -Wl,-no_pie -o int_address int_address.c
$ ./int_address
a = 0x12345678
a @ 0x100001018

Mac OS Xでの実行例を示します。Max OS Xでは、プロセス起動時に割り当てアドレスをランダムに設定する機能がデフォルトになっているため、リンカーに-no_pieオプションを渡し、固定アドレスを使用するように指示します。変数aの格納開始アドレスは、0x10001018番地であることが分かります。

$ cc -Wall -o int_address int_address.c
$ ./int_address
a = 0x12345678
a @ 0x601040

Ubuntuでの実行例を示します。Ubuntuでは割り当てアドレスのランダム化は行われないため、オプションなしでも常に同一のアドレスが表示されます。本例では、変数aは0x601040番地に割り当てられています。

文字型変数の割り当てアドレス

次に、文字型変数の割り当てをchar_address.cで検証します。

#include <stdio.h>

char b = '*';

int main() {
    printf("b = 0x%X\n", b);
    printf("b @ %p\n", &b);
    return 0;
}

int_address.cとの大きな違いは、文字型変数bを定義している3行目のみです。

$ cc -Wl,-no_pie -o char_address char_address.c
$ ./char_address
b = 0x2A
b @ 0x100001018

Mac OS Xでの実行例ですが、文字型変数bの格納開始アドレスは、先程の整数型変数aと同一である点に注意してください。

$ cc -Wall -o char_address char_address.c
$ ./char_address
b = 0x2A
b @ 0x601040

Ubuntuでも同一アドレスに割り当てられていることが分かります。

変数の内容は整数型と文字型で異なるが、割り当てアドレスは同一」という事実は、C言語を操る上でとても大切な点であり、ポインタの本質もここにあります。

sizeof演算子

次に、変数のサイズについて検討します。変数がメモリ領域上に占めるサイズは、変数の型によって異なるため、C言語にはsizeof演算子が用意されています。これは指定された型もしくは変数のバイト長を返す演算子です。

#include <stdio.h>

int main() {
    printf("sizeof(char) = %zd\n", sizeof(char));
    printf("sizeof(int) = %zd\n", sizeof(int));
    return 0;
}

sizeof.cは、文字型変数と整数型変数のサイズを表示するプログラムです(指定子%zは警告抑制のために必要)。

$ cc -Wall -o sizeof sizeof.c
$ ./sizeof
sizeof(char) = 1
sizeof(int) = 4

文字型変数のメモリ格納サイズは1バイト、整数型変数のサイズは4バイトであることが分かります。

unionによるメモリ内容の読み換え

続いて、C言語に特徴的な概念である共用体 (union)に進みます。今回は共用体を活用した例題として、dump_union.cというプログラムを用意しました。

#include <stdio.h>

union {
    int a;
    char b;
    unsigned char c[ 4 ];
} u;

void dump(void) {
    int i;

    printf("%p : ", u.c);
    for (i = 0; i < 4; i++)
        printf("%02X ", u.c[ i ]);
    printf("\n");
}

int main() {
    printf("sizeof(u) = %zd\n", sizeof(u));
    printf("u.a @ %p\n", &u.a);
    printf("u.b @ %p\n", &u.b);
    printf("u.c @ %p\n", u.c);
    printf("\n");

    dump();
    printf("u.a = 0x%X : u.b = 0x%02X\n\n", u.a, u.b);

    u.a = 0x12345678;
    dump();
    printf("u.a = 0x%X : u.b = 0x%02X\n\n", u.a, u.b);

    u.b = '*';
    dump();
    printf("u.a = 0x%X : u.b = 0x%02X\n\n", u.a, u.b);

    return 0;
}

3〜7行で共用体uを定義していますが、そのメンバとして整数型変数a、文字型変数bおよび文字型変数配列cを宣言しています。共用体では、複数メンバの中で最大のものに合わせてサイズが設定されます。今回は、文字型(1バイト)、整数型(4バイト)、文字型配列(4バイト)ですから、共用体uのサイズは最大値である4バイトとなります。

9〜16行のdumpは、共用体メンバcの内容を4バイト分出力するための関数です。

19行で共用体uのバイトサイズ、続く20〜22行ですべての共用体メンバの格納開始アドレスを表示します。

25行で初期化された共用体uの内容をダンプし、

28行では、整数型変数メンバaに0x12345678を格納し、再びdump関数を実行、

32行で、文字型変数メンバであるbにアスタリスクを格納し、3回目のdump関数を実行します。

$ cc -Wall -Wl,-no_pie -o dump_uion dump_union.c
$ ./dump_uion
sizeof(u) = 4
u.a @ 0x100001018
u.b @ 0x100001018
u.c @ 0x100001018

0x100001018 : 00 00 00 00
u.a = 0x0 : u.b = 0x00

0x100001018 : 78 56 34 12
u.a = 0x12345678 : u.b = 0x78

0x100001018 : 2A 56 34 12
u.a = 0x1234562A : u.b = 0x2A

Mac OS X上での実行結果を示します。

共用体uのサイズは予想通り4バイトでした。さらに、共用体uのメンバa、b、cの格納開始アドレスは、すべて0x100001018番地になっており、同一のメモリ開始領域に3つのメンバが割り当てられていることが分かります。

共用体uは未初期化状態で定義されているため、C言語の仕様からプロセス起動時にその中身はゼロで初期化されます。この仕様は、最初のdump関数を実行した際に、メンバcの配列内容がすべてゼロであることからも、確認できます。

次に、メンバaに0x12345678を設定した後にdump関数を実行すると、4バイトの下位から順番に0x78, 0x56, 0x34, 0x12が格納されていることが分かります。x86はlittle-endianのプロセッサであるため、このような順序になっています。ここで、メンバbの値が0x78になっている点に着目してください。

最後に、メンバbにアスタリスク(ASCIIコード 0x2A)を設定すると、共用体uの中身は先頭だけが0x2Aとなり、残りは先程の"残骸"である0x56, 0x34, 0x12が続いています。コンパイラは変数bが文字型(1バイト長)であるため、格納開始アドレス0x100001018番地の内容のみを書き換えています(続く3バイトには関与せず)。この結果、メンバaを評価すると今度は0x1234562Aとなります。

以上の実験結果から、共用体が意味するところをまとめると次のようになります。

  • 共用体のすべてのメンバの格納開始アドレスは同一である。
  • コンパイラは共用体メンバの"型に応じて"格納開始アドレスから続くバイト列の内容を解釈する。

ポインタも共用体と同様に、データの格納開始アドレスと型に応じて処理を行います。

教科書的ポインタ

それではポインタを取り扱ってみましょう。次は、ポインタ操作を含む教科書的なプログラムpointer.cです。

#include <stdio.h>

int a = 0x12345678;
int *p;

int main() {
    p = &a;
    printf("a @ %p\n", &a);
    printf("p @ %p\n", &p);
    printf("p = %p\n", p);
    return 0;
}

3行目で整数型変数a、4行目で整数を指すポインタpを定義しています。

ここではpを定義する際に、教科書的にアスタリスクを変数名に前置していますが、この表記方法の根拠はC言語設計者であるKernighan and Rithcieの著作に書かれている、次の一文に基づいています(THE C PROGRAMMING LANGUAGE, 2nd Ed, p93, 1988)。

A pointer is a variable that contains the address of a variable.

文章全体を直訳すれば、"ポインタはある変数のアドレスを保持する変数である"となりますが、明らかに意味不明瞭な記載です。本来、ポインタとポインタ型変数は全く異なる概念なのですが、両者を混同しているこの一文は、古今東西多くの人々に混乱をもたらしてきました。

同文で重要な点は、C言語設計者達が"ポインタは変数である"と明記していることにあります。このため、"ポインタが通常の変数とは異なることを明示するため、変数名にアスタリスクを前置する"という解釈を取ったのです。

そしてまた、ポインタが変数であるということは、"ポインタ型変数は存在しない"ことも意味します。ポインタは通常の変数とは異なるという先程の大前提に反してしまうからです。

ひとまず、言語設計者の解釈でコーディングしたpointer.cで実験してみましょう。

7行目で&演算子を使い変数aの格納開始アドレスをポインタpにセット、8行目で変数aの格納アドレス、9行目でポインタp自身の格納アドレス、最後にポインタpの内容を出力します。

$ cc -Wall -Wl,-no_pie -o pointer pointer.c
$ ./pointer
a @ 0x100001018
p @ 0x100001020
p = 0x100001018

pointer.cは文法的に正しいプログラムですから、コンパイルは問題なく成功します。

Mac OS Xでの実行例ですが、変数aの格納アドレスは0x100001018番地、ポインタpの格納アドレスは8バイト飛んで0x100001020番地、そしてポインタpの内容は0x100001018番地でした。

後で述べますが、変数aとポインタpの格納アドレスに8バイトの差がある理由は、64ビット環境ではポインタが8バイトの長さを持っているからです。p = &aの実行により、ポインタpの内容は、確かに変数aの格納アドレスと同じ値になっています。

ポインタもしくはポインタ型変数のサイズ

続いて、ポインタのサイズを確認するpointer_size.cへと進みます。

#include <stdio.h>

int a;
int *b, c;
int* d, e;

int main() {
    printf("sizeof(a) = %zd\n", sizeof(a));
    printf("sizeof(b) = %zd\n", sizeof(b));
    printf("sizeof(c) = %zd\n", sizeof(c));
    printf("sizeof(d) = %zd\n", sizeof(d));
    printf("sizeof(e) = %zd\n", sizeof(e));
    return 0;
}

内容はsizeof演算子を用いて、様々な変数のバイトサイズを表示するものですが、このプログラムで注目すべきは4行目と5行目にあります。

4行目は、C言語設計者の解釈に基づいた"整数型変数へのポインタb"と整数型変数cを定義しています。

5行目は、教科書に反してアスタリスクを型名側へ後置しています。この表記が意味したいところは、"整数へのポインタ型変数dとe"を定義するというものですが、その背景には次のような解釈があります。

  • 通常の変数とポインタを区別せず、同じ変数と考える
  • ポインタ型変数はポインタを格納する変数である
  • ポインタは格納開始アドレスと格納データのを保持する
  • ポインタの型には文字ポインタ型、整数ポインタ型、構造体ポインタ型、関数ポインタ型などがある

この内容であれば表現と解釈の間に矛盾は生じません。さて、結果はどうなるでしょうか?

$ cc -Wall -o pointer_size pointer_size.c
$ ./pointer_size
sizeof(a) = 4
sizeof(b) = 8
sizeof(c) = 4
sizeof(d) = 8
sizeof(e) = 4

ポインタbのサイズが8バイト(64ビット環境の場合)、整数型変数aおよびcのサイズが4バイトであるのは、予想通りです。

問題は、"int*を整数ポインタ型"と解釈して宣言した変数dとeのサイズです。変数dは8バイト長ですが、変数eは4バイト長と整数型のサイズしかありません。コンパイラは言語設計者の意図通り、"int* d, e"を"int *d, e"と強制的に解釈したことになります。

それではC言語には、ポインタ型は存在しないのでしょうか?

typedefによるポインタ型の誕生

実は、ポインタ型は存在します。ただし、typedefという小技を使う必要があるのですが。

#include <stdio.h>

typedef int* INT_POINTER;
INT_POINTER a, b;

int main() {
    printf("sizeof(a) = %zd\n", sizeof(a));
    printf("sizeof(b) = %zd\n", sizeof(b));
    return 0;
}

ここに、typedef_pointer.cというプログラムを用意しました。3行目でtypedef機能を用いて、整数へのポインタ型であるINT_POINTER型を新たに定義しています。

4行目でINT_POINTER型変数aとbを定義していますが、それぞれのサイズは果たしていくらになるでしょうか?

$ cc -Wall -o typedef_pointer typedef_pointer.c
$ ./typedef_pointer
sizeof(a) = 8
sizeof(b) = 8

結果はご覧の通り、どちらの変数もサイズは8バイト。つまり、a/bは共に"ポインタ型変数"として意図通りにコンパイルされています。

ポインタ型はC言語仕様上存在しないが、typedefを使えば定義できる

ポインタ型はないとも言えるし、あるとも言える。つまり、ポインタに関してC言語は論理的に破綻しているのです。

オーバーシー・パブリッシング著作物でのポインタ型の取扱い

ポインタを巡る混乱の最大の原因は"A pointer is a variable."という一文にあり、この混迷を打開するためにはポインタ型の導入が不可欠であると、筆者は考えています。

残念ながら、現状ではポインタ型は許されておらず、ポインタ型を意識したコーディングを行うためには、typedefの力を借りなければなりません。

大規模なプログラム中で繰り返し登場する場合はtypedefが有効ですが、小さな実例程度のプログラムでは、かえって可読性が下がることにもつながります。このような場合、当社の記事中では"単一の変数/メンバに対してポインタ型を意識した定義/宣言"を行います。

例えば、符号無し文字型変数へのポインタを保持する変数は、

unsigned char* p;

と定義し、同じ型を持つ複数の変数を続けて定義する場合は、

unsigned char* p;
unsigned char* q;

のように複数行で記載します。

unsigned char* p, q;

とすれば、既に述べた通りプログラマの意図を外れたコードが生成されてしまいますので、この点はくれぐれもご注意ください。

当社は教育的視点に配慮し、上記のリスクよりも、ポインタ解釈の整合性を優先しています。

posted by Wataru Nishida.