プログラミング言語の学習には、どの言語にもいくつかの難関があります。C言語の場合、「ポインタ」が最大の難関と言われていますが、コンピュータの基本 (CPU やメモリの概念) を正しく理解していれば、けっして難しい話ではありません。
問題があるとすれば、C言語のポインタが不適切な操作や演算によってプログラムを簡単に暴走させてしまうことでしょう。もともとC言語は、UNIX という OS を記述するために開発されたプログラミング言語です。マシン語なみの操作ができないようでは役に立たないわけで、ポインタという危険なものでもユーザーに開放されているわけです。
まず最初に、基本となるメモリの構成から説明しましょう。
メモリにはプログラムやデータが記憶されています。メモリのことを「主記憶装置」といいます。CPU はメモリに格納されているプログラムを実行します。ハードディスクや CD-ROM, USB メモリなどに格納されているプログラムは、メモリに読み込まないと実行することはできません。
メモリ以外の記憶装置を「補助記憶装置」といいます。ハードディスクや CD-ROM, USB メモリなどいろいろな記憶装置がありますが、すべての方法に共通しているのは、情報をなんらかの方法で ON / OFF という 2 つの状態で表していることです。
この ON / OFF を数値の 1 と 0 に対応させます。つまり、コンピュータは情報を 0 と 1 で表すわけです。これを「ビット (bit)」といいます。ひとつのビットでは、0 か 1 かの 2 つの情報しか表せませんが、使用するビットの数を増やすと、それだけたくさんの情報を表すことができます。たとえば、4 ビット使用すると 16 通りの情報を表すことができます。このビットをたくさん集めたものがメモリなのです。
ビ ッ ト 7 6 5 4 3 2 1 0 ┌─┬─┬─┬─┬─┬─┬─┬─┐ 0 │ │ │ │ │ │ │ │ │ ├─┼─┼─┼─┼─┼─┼─┼─┤ 1 │ │ │ │ │ │ │ │ │ ア ├─┼─┼─┼─┼─┼─┼─┼─┤ 2 │ │ │ │ │ │ │ │ │ ド ├─┼─┼─┼─┼─┼─┼─┼─┤ ・ │ │ │ │ │ │ │ │ │ レ ・ │ │ │ │ │ │ │ │ │ ・ │ │ │ │ │ │ │ │ │ ス ・ │ │ │ │ │ │ │ │ │ ・ │ │ │ │ │ │ │ │ │ ├─┼─┼─┼─┼─┼─┼─┼─┤ M-1│ │ │ │ │ │ │ │ │ ├─┼─┼─┼─┼─┼─┼─┼─┤ M │ │ │ │ │ │ │ │ │ └─┴─┴─┴─┴─┴─┴─┴─┘ │←─── 1バイト ───→│ 図 : メモリの構成
ビットでは情報が細かすぎるので、いくつかのビットをまとめた「バイト (byte)」を単位として、メモリは構成されています。現在は、1 バイトを 8 ビットとしてメモリを構成するコンピュータ [*1] がほとんどです。1 バイトは数値で表すと 0 から 255 までの 256 通りの情報を記憶できます。
メモリから値を読み出す、または書き込む場合、最小の大きさがバイトとなります。どのメモリから値を読み出すのか、またはどのメモリに値を書き込むのかを指定するために、メモリにはバイト単位で「アドレス (番地 : address)」がつけられています。これは私たちの住所や電話番号と同じです。メモリの場合は単純に数値で表します。下図を見てください。
C言語の場合、配列は 0 から数えましたね。コンピュータの世界では、ビットもアドレスも 0 から数えるのが普通です。32 bit CPU の場合、M の値は 232 - 1 = 4,294,967,295 (約 4 ギガ: giga) になります。64 bit CPU の場合は 264 - 1 = 18,446,744,073,709,551,615 (約 18 エクサ: exa [*2]) になります。
数値計算をするときなど、1 バイトでは情報量が少ない場合は、2 バイトまたは 4 バイトまとめてメモリを使用します。文字を表したい場合は、文字を数値に対応させます。アルファベットは 26 種類ありますから、大文字小文字、そしてほかの記号を合わせても 1 バイトあれば表現できます。たとえば、アスキーコードという規則では、A という文字は 0x41 に対応します。漢字は 1 バイトで表現できないので、複数のバイトを使います。よく使われている漢字コードに JIS, シフトJIS, EUC などがありますが、最近は UTF-8 を採用する Unix 系 OS やプログラミング言語が増えてきました。
グラフィックの場合は、点 (ドット : dot) をメモリに対応させて表現します。たとえば、1 ドットを 1 バイトで表現してみましょう。そのメモリの内容が 0 ならば、ドットが書かれていないことにします。それ以外のときはドットが書かれているという規則にします。このとき、1 から 255 に対応する色を決めておけば、256 色の絵が描けるわけです。もし、65536 色の絵を描くのであれば、1バイトでは表現できないので、1 ドットにつき 2 バイト使用することになります。フルカラー (1600 万色) 表示であれば、1 ドットにつき 3 バイト必要になるわけです。
このように、メモリに格納されるデータは、単なる数値に過ぎないのですが、使うソフトウェアによってその意味は異なるのです。ある場合は、数値計算のために使用され、ほかでは文字を格納するために、またあるときはグラフィックデータを保持します。
メモリをどのように使うかは、プログラマが決めます。そして、それを実現するための道具がプログラミング言語なのです。変数、配列、文字列といったデータも、すべてメモリに割り当てられますが、マシン語以外の高級言語ではアドレスを意識することはありません。プログラマは変数を定義するだけで、メモリの割り当てといった面倒なことはすべてプログラミング言語 (と OS) が行ってくれます。
一般に、プログラミング言語で扱う名前は、そのデータが割り当てられたメモリの先頭アドレスを表しています。変数名や配列名は、そのデータが割り当てられたメモリの先頭アドレスであり、関数名は、そのコードが配置されたメモリの先頭アドレス (関数の開始アドレス) を表しています。
高水準と呼ばれるプログラミング言語の場合、名前から実際のアドレスを求めることはできません。プログラマが勝手にメモリ割り当てを操作したり変更すると困る場合があるからです。もっとも、そのようなことをしなくてもプログラミングできないようでは、とても高水準とはいえません。
ところがC言語の場合は違います。C言語では名前からアドレスを求める演算子 & [*3] が用意されているのです。変数名にこの演算子を適用することで、変数が割り当てられたメモリの先頭アドレスを求めることができます。また、配列名や関数名はデータやコードの先頭アドレスに付けられた名前にすぎず、& 演算子を使わなくてもアドレスとして使用することができます。
そして、C言語ではアドレスを格納する変数を定義することができます。これが「ポインタ」です。アドレスは整数値ですから、ポインタの中身は整数値です。ですが、その値はある変数が割り当てられているメモリの先頭アドレスです。つまり、ポインタは「ある変数を指し示している変数」ということになります。また、関数名もアドレスを示しているのですから、ポインタに代入することができます。これを「関数へのポインタ」と呼びます。どちらにしても、ポインタはあるデータを指し示している変数なのです。
これを図に示すと次のようになります。
番地 0x68000 0x70000 ↓ ↓ ┬─┬─┬─┬─┬─┬─┬─┬─┬ ┬─┬─┬─┬─┬─┬─┬─┬ メモリ │ 0x70000 │ │ │ │ │~│ │ │ │ 0x100000 │ ┴─┴─┴─┴─┴─┴─┴─┴─┴ ┴─┴─┴─┴─┴─┴─┴─┴ 変数 p │ ↑ 変数 i └───────────────────┘ 図 : ポインタ
変数 p はポインタです。図に示すように、ポインタもメモリ [*4] に割り当てられます。この例では 0x68000 番地になっています。CPU が 32 bit の場合、Cコンパイラはポインタに 4 バイトのメモリを割り当てる場合がほとんどで、0 から 0xFFFFFFFF までのアドレス指定が可能になります。これだと約 4 G バイトのメモリを扱うことができます。CPU が 64 bit の場合、ポインタには 8 バイトのメモリを割り当てることになりますが、本ページではポインタの大きさは 4 バイトとして説明することにします。
変数 i は int 型としましょう。上図の場合、変数 i は 0x70000 番地から割り当てられていて、値は 0x100000 です。変数 p はポインタなので、変数 i のアドレスを代入することができます。すると、図のように変数 p の値は 0x70000 となり、変数 i を指し示すことになります。そして、ポインタ p を使って、変数 i の値を読み書きすることができるのです。これがポインタの基本的な考え方です。
ポインタは次のように宣言します。
データ型 *変数名; データ型 *変数名 = &変数;
ポインタは変数名の前にアスタリスク ( * ) を付けて表します。たとえば、int *p; とすると、p は int 型のポインタになります。変数 i を参照したい場合、アドレス演算子を使って p =&i; とします。これで変数 p は int 型の変数 i を指すポインタになります。また、ポインタを宣言するとき、初期値に変数のアドレスを指定することもできます。
p が参照している変数の値にアクセスする方法も簡単です。変数名の前に * を付けて *p とするだけです。これでポイントしている変数 (この場合は int 型の整数) にアクセスすることができます。つまり、*p は p が保持しているアドレスに格納されているデータを参照 [*5] するわけです。たとえば、int j = *p; とすれば、変数 j の値は 0x100000 になります。また、*p = 10 とすれば、p が指し示している変数の値を書き換えるので、i の値は 10 になります。
それでは実際にポインタを使ってみましょう。なお、図のアドレスは説明のためのもので、これから示すプログラムとは関係ありません。プログラムがロードされるアドレスは実行環境によって異なるので、変数 i と p のアドレスは実際にプログラムを実行してみないとわからないのです。ご注意くださいませ。
リスト : ポインタの使用例 (sample50.c) #include <stdio.h> int main(void) { int i = 10; int *p = &i; printf("%p\n", &i); printf("%p\n", p); printf("i = %d\n", i); printf("*p = %d\n", *p); *p = 100; printf("i = %d\n", i); printf("*p = %d\n", *p); return 0; }
$ clang sample50.c $ ./a.out 0x7ffc65ed1688 0x7ffc65ed1688 i = 10 *p = 10 i = 100 *p = 100
printf の %p はポインタ変換指定子で、引数がポインタであれば格納しているアドレスを 16 進数で表示します。&i のように変数のアドレスを渡してもかまいません。
ポインタ p は &i で初期化されているので、どちらも同じアドレスが表示され、i と *p は同じ値 10 が表示されます。*p に 100 を代入すれば、i の値も 100 に書き換えられます。p は変数 i のアドレスを保持し、*p はそのアドレスに格納されているデータにアクセスできるのですから、*p に書き込みすれば変数 i の値も書き変わるのは当然ですね。
ところでこのポインタ、一体何の役に立つのでしょうか。C言語の場合、次の利点があります。
(1) 「参照呼び (call by reference)」を実現する (2) コンパクトで効率的なプログラムを書くことができる
(1) ですが、C言語の関数は「値呼び (call by value)」です。関数の引数にポインタを渡すことで、呼び出し先の関数から呼び出し元の関数に定義されている変数にアクセスすることができるようになります。これはあとで詳しく説明します。
(2) はアドレス計算によって実現されます。ポインタが格納しているアドレスは単なる正の整数値です。四則演算ができるわけではありませんが、整数値の代入や加減算 [*6]、ポインタ同士の比較を行うことができます。とくに、インクリメント (++)、デクリメント (--) 演算子と組み合わせることで、効率的なプログラムを記述することができます。
このほかに、プログラムの実行時にメモリを取得するときや、連結リストや二分木といった複雑なデータ構造を作るとき、ポインタはとても役に立ちます。
もっとも、いいことばかりではありません。よくある間違いがポインタの初期化忘れです。局所変数の場合、ポインタを宣言しただけでは、その値は定まっていません。どこをポイントしているのかわからないのですから大変危険です。また、ポインタには整数値を代入することができますが、次のように 0 を代入したらどうなるでしょうか。
リスト : ポインタの危険な操作 (sample51.c) #include <stdio.h> int *q; int main(void) { int *p; p = 0; printf("%p\n", p); printf("%p\n", q); printf("%d\n", *p); return 0; }
$ clang sample51.c $ ./a.out (nil) (nil) Segmentation fault
近代的な OS の場合、メモリはシステムエリアとユーザーエリアに区別されていて、ユーザーのプログラムがシステムエリアにアクセスすることを禁止しています。もし、このメモリにアクセスすると OS で Segmentation fault という例外 (エラー) が発生します。
C言語の場合、アドレス 0 を指すポインタを「ヌルポインタ (null pointer)」といい、マクロ記号 NULL で表します。実際には、次のように定義されています。
#define NULL (void *)0
void * は void 型のポインタを表していて、NULL は整数値 0 を void * に型変換した値になります。明示的に型変換を行うことを「キャスト (cast)」といい、望む型をカッコでくくって式の前につけます。void * はどんな型のポインタにもキャストせずに代入したり、値を比較することができます。
printf の p 変換指定子はヌルポインタを (nil) と表示します。ポインタを外部変数として宣言したとき、初期値を指定しないとヌルポインタに初期化されます。ご注意くださいませ。
このように、ポインタの操作には危険がつきまとうのですが、そのかわりに、ハードウェアを制御するプログラム [*7] でも、C言語だけで作ることができます。このため、C言語は高級アセンブラとか汎用アセンブラと呼ばれています。
次は配列へのポインタを考えてみましょう。C言語の場合、配列名が配列の先頭要素のアドレスを表します。たとえば、int a[100]; という配列を定義すると、配列名 a は配列の先頭要素のアドレス &a[0] と同じ意味になります。C言語の配列は連続したメモリ領域に配置されるので、ポインタを使って配列の要素にアクセスすることができます。
簡単な例を示しましょう。次のリストを見てください。
リスト : 配列とポインタ (sample52.c) #include <stdio.h> int main(void) { int a[] = {1, 2, 3, 4, 5, 6, 7, 8}; int *p = a; printf("p = %p\n", p); printf("%d\n", *p); printf("%d\n", *(p + 3)); printf("%d\n", *(p + 7)); int *q = a + 8; printf("q = %p\n", q); while (p < q) { printf("p = %p\n", p); printf("%d\n", *p++); } return 0; }
$ clang sample52.c $ ./a.out p = 0x7fff6a89bfc0 1 4 8 q = 0x7fff6a89bfe0 p = 0x7fff6a89bfc0 1 p = 0x7fff6a89bfc4 2 p = 0x7fff6a89bfc8 3 p = 0x7fff6a89bfcc 4 p = 0x7fff6a89bfd0 5 p = 0x7fff6a89bfd4 6 p = 0x7fff6a89bfd8 7 p = 0x7fff6a89bfdc 8
配列 a の要素は int なので、変数 p を int 型のポインタとして宣言して、p を a の先頭要素のアドレスに初期化すれば、p を使って配列の要素にアクセスすることができます。配列名は先頭要素のアドレスを表すので、int *p = a; とすれば OK です。
要素のアクセスは簡単で、*p が先頭要素、*(p + 1) が 1 番目の要素というように、*(p + n) で n 番目の要素にアクセスすることができます。p は int 型のポイントなので、p + 1 は p のアドレスに 1 を加えるのではなく、int の大きさである 4 が加算されることに注意してください。また、*(p + 1) = 100; とすれば、a[1] の値を書き換えることができます。
配列は連続した領域に配置されるので、配列 a の領域を半区間 [p, q) (p 以上 q 未満) で表すと、[0xbffcc79c, 0xbffcc79 + 4 * 8) であることがわかります。これは int *q = a + 8; で求めることができます。ここで、*p++ とすると、*p の要素を参照したあと、ポインタ p を次の要素へ進める (アドレスを +4 する) ことができます。
C言語の場合、ポインタ同士の比較は可能なので、条件 p < q を満たすあいだ処理を繰り返せば、配列 a の要素を順番に表示することができます。このとき、p のアドレスが +4 ずつ増えていることに注意してください。
もう一つ簡単な例を示しましょう。C言語の文字列は文字 (char) の配列なので、ポインタを使って操作することができます。次のリストを見てください。
リスト : 文字列とポインタ (sample53.c) #include <stdio.h> int main(void) { char a[] = "hello, world"; char b[] = "hello, Linux"; char c[16]; char *p, *q; // 長さ int len = 0; p = a; while (*p++ != '\0') len++; printf("%d\n", len); // コピー p = a; q = c; while ((*q++ = *p++) != '\0'); printf("%s\n", c); // 比較 p = a; q = b; while (*p == *q) { if (*p == '\0') break; p++; q++; } if (*p == 0 && *q == 0) { printf("equal\n"); } else { printf("not equal\n"); } return 0; }
$ clang sample53.c $ ./a.out 12 hello, world not equal
文字列はヌル文字 '\0' で終端されているので、文字数を数えるのは簡単です。ポインタ p を a に初期化し、ヌル文字でなければ p と len をインクリメントしていくだけです。文字列のコピーも簡単です。p を a に、q を c に初期化します。*p の値を *q に代入して、その値がヌル文字でなければ p と q をインクリメントしていきます。
文字列の比較も簡単です。p と q を初期化して、*p と *q が等しいあいだ、p と q をインクリメントします。このとき、*p がヌル文字かチェックして、そうであれば文字列の終端に到達したので break で while ループを脱出します。*p と *q がともにヌル文字であれば、文字列の内容は等しいことがわかります。そうでなければ、途中で文字が一致しなかったので、文字列は等しくないことがわかります。
このように、文字列の操作はポインタを使って簡単に行うことができます。なお、C言語には文字列操作用の標準ライブラリ (string.h) が用意されているので、実際にこのようなプログラムを作る必要はありません。
一般に、関数呼び出しには二つの方法があります。一つが「値呼び (call by value)」で、もう一つが「参照呼び (call by reference)」です。近代的なプログラミング言語では「値呼び」が主流です。
値呼びの概念はとても簡単です。
(1) 受け取るデータを格納する変数 (仮引数) を用意する。 (2) データを引数に代入する。 (3) 関数の実行終了後、引数を廃棄する。
値呼びのポイントは (2) です。データを引数に代入するとき、データのコピーが行われるのです。たとえば、変数 a の値が 10 の場合、関数 foo(a) を呼び出すと、実引数 a の値 10 が foo の仮引数にコピーされます。変数に格納されている値そのものを関数に渡すので、「値渡し」とか「値呼び」と呼ばれます。また、値呼びは任意の式の値を実引数として渡すことができます。たとえば foo(a + b) の場合、引数に渡された式 a + bを計算し、その結果が foo の仮引数に渡されます。
値呼びは単純でわかりやすいのですが、呼び出し先 (caller) から呼び出し元 (callee) の局所変数にアクセスできると便利な場合もあります。仮引数に対する更新が直ちに実引数にも及ぶような呼び出し方が「参照呼び」です。
C言語の関数は「値呼び」ですが、引数にポインタを渡すと「参照呼び」と同様の動作を行うことができます。次の例を見てください。
リスト : 関数にポインタを渡す (sample54.c) #include <stdio.h> void swap_bad(int a, int b) { int c = a; a = b; b = c; } void swap(int *a, int *b) { int c = *a; *a = *b; *b = c; } int main(void) { int a = 10; int b = 20; swap_bad(a, b); printf("a = %d, b = %d\n", a, b); swap(&a, &b); printf("a = %d, b = %d\n", a, b); return 0; }
$ clang sample54.c $ ./a.out a = 10, b = 20 a = 20, b = 10
関数 swap はC言語の関数が「値呼び」であることの説明によく使われる例題です。C言語で局所変数の値を交換するには、一時的に値を格納する局所変数を使って行います。この処理を関数で行う場合、swap_bad のように引数の型を int a, int b とすると、変数の値が引数にコピーされるので、元の局所変数の値を交換することはできません。この場合、swap のように引数の型を int *a, int *b として変数のアドレスを渡すと、元の局所変数の値を交換することができます。
C言語の場合、関数の引数に int a[10] のような配列を宣言することはできますが、実際に配列の領域が確保されて要素がコピーされることはありません。引数 a に渡されるのは配列の先頭アドレスだけです。このとき添字は無視されます。意味を持つのは多次元配列を渡すときです。
関数に配列を渡す場合、引数の宣言はポインタを使って int *a とするか、[ ] の中の数字を省略して int a[] とします。どちら方法でも引数 a には配列の先頭アドレスが渡されます。簡単な例を示しましょう。
リスト : 配列と関数 (sample55.c) #include <stdio.h> void foo(int a[], int n) // int *a でもよい { for (int i = 0; i < n; i++) printf("%d ", a[i]); printf("\n"); a[0] = 100; } int main(void) { int a[8] = {1, 2, 3, 4, 5, 6, 7, 8}; foo(a, 8); for (int i = 0; i < 8; i++) printf("%d ", a[i]); printf("\n"); return 0; }
$ clang sample55.c $ ./a.out 1 2 3 4 5 6 7 8 100 2 3 4 5 6 7 8
関数 foo は受け取った配列 a の内容を表示します。配列の大きさは引数 n で受け取ります。最後に a[0] の値を 100 に書き換えます。foo を呼び出したあと、配列 a の内容を表示すると、先頭要素が 100 に書き換えられていて、配列 a の先頭アドレスが関数 foo に渡されていることがわかります。
なお、C言語の文字列は文字の配列なので、文字列を関数に渡す場合も、配列と同様に先頭アドレスが渡されることになります。
C言語の場合、ポインタを指すポインタを作ることができます。次の図を見てください。
番地 0x68000 0x69000 0x70000 ┬─┬─┬─┬─┬ ┬─┬─┬─┬─┬ ┬─┬─┬─┬─┬ メモリ │ 0x69000 │~│ 0x70000 │~│ 0x100000 │ ┴─┴─┴─┴─┴ ┴─┴─┴─┴─┴ ┴─┴─┴─┴─┴ 変数 q │ ↑変数 p│ ↑ 変数 a └─────┘ └─────┘ 図 : 多段階のポインタ
変数 q はポインタです。q はポインタ p を指しています。p は変数 a を指しています。つまり、q は p を経由して変数 a を指し示しているのです。これをC言語のプログラムで表すと、次のようになります。
リスト : 多段階のポインタ (sample56.c) #include <stdio.h> int main(void) { int a = 100; int b = 1000; int *p = &a; int **q = &p; printf("%d\n", a); printf("%d\n", *p); printf("%d\n", **q); **q = 200; printf("%d\n", a); printf("%d\n", *p); printf("%d\n", **q); *q = &b; printf("%d\n", b); printf("%d\n", *p); printf("%d\n", **q); return 0; }
$ clang sample56.c $ ./a.out 100 100 100 200 200 200 1000 1000 1000
C言語の場合、ポインタを指し示すポインタは、経由するポインタと同じ数だけ * を追加します。変数 q はポインタ p を経由して変数 a を指し示すので int **q; となります。2 つのポインタを経由するのであれば、int ***q; と宣言します。
ポインタ q はポインタ p を指し示すので、初期化は変数 p のアドレスをセットします。p の値は変数 a のアドレスなので、q にセットしてはいけません。もし q = p; とプログラムすると、コンパイル時にワーニングが表示されます。
これで、**q とすることで変数 a の値にアクセスすることができます。**q =200; のように値を代入すると、変数 a の値を書き換えることができます。また、*q とすることで変数 p の値にアクセスすることができます。このとき、*q の値を書き換えると、ポインタ q と p は変数 a ではなく、別の値を指し示すことになります。このように、ポインタを操作するときには細心の注意が必要になります。
C言語はポインタを配列に格納することができます。
データ型 *配列名[大きさ];
配列名の前に * を付けると、配列の要素はポインタになります。たとえば、文字列を格納する配列は次のように定義することができます。
リスト : 配列に文字列を格納する (sample57.c) #include <stdio.h> int main(void) { char a[] = "foo"; char b[] = "bar"; char c[] = "baz"; char d[] = "hello, world"; char *msg[] = {a, b, c, d}; /* char *msg[] = { "foo", "bar", "baz", "hello, world" }; */ for (int i = 0; i < 4; i++) { printf("%s\n", msg[i]); } return 0; }
$ clang sample57.c $ ./a.out foo bar baz hello, world
変数 a, b, c, d は文字列で、それを格納する配列が msg です。msg は char *msg[] と宣言し、{a, b, c, d} で初期化します。これはコメントにあるように、直接文字列を指定することもできます。普通はコメントのような書き方をします。これで、msg の要素は文字列を指し示すアドレスが格納されます。つまり、msg[1] は "foo" で、msg[3] は "hello, world" になります。
もちろん、文字列以外のポインタも格納することができます。次のリストを見てください。
リスト : 配列に int 型の配列を格納する (sample58.c) #include <stdio.h> int main(void) { int a[] = {1, 2}; int b[] = {3, 4, 5}; int c[] = {6, 7, 8, 9}; int *d[] = {a, b, c}; /* int d[3][4] = { {1, 2}, {3, 4, 5}, {6, 7, 8, 9} }; */ printf("%d\n", *d[0]); printf("%d\n", *(d[0] + 1)); printf("%d\n", d[1][0]); printf("%d\n", d[2][3]); return 0; }
$ clang sample58.c $ ./a.out 1 2 3 9
配列 a, b, c を定義し、それらを配列 d にセットします。この場合、d は int *d[] と宣言します。これで d の要素は各配列の先頭アドレスになります。なお、この場合はコメントのように多次元配列で定義したほうが簡単ですね。int d[n][m] と定義した場合、d[0] は d[0][0] の先頭アドレス、d[i] (0 < n) は d[i][0] の先頭アドレスになります。
C言語の場合、関数名はコードの先頭アドレスを表します。そのアドレスを格納する変数が「関数へのポインタ」になります。基本的な考え方は他のポインタと同様に簡単なのですが、関数へのポインタを表す構文がちょっと複雑になります。関数へのポインタの構文を示します。
返り値のデータ型 (*変数名)(引数のデータ型 仮引数名, ...); 返り値のデータ型 (*変数名)(引数のデータ型 仮引数名, ...) = 関数名;
関数プロトタイプと同じような形式で、関数名のかわりに変数名を指定します。そして、名前の前に * を付けます。これでこの変数は関数へのポインタになります。仮引数名は省略してもかまいません。呼び出し方は (*変数名)(実引数, ...) とするか、変数名(実引数, ... ) とします。
簡単な例を示しましょう。次のリストを見てください。
リスト : 関数へのポインタ (sample59.c) #include <stdio.h> double identity(double x) { return x; } double square(double x) { return x * x; } double cube(double x) { return x * x * x; } double sumof(double (*func)(double), int n, int m) { double sum = 0; for (; n <= m; n++) { sum += func(n); // (*func)(n) でもよい } return sum; } int main(void) { double (*func)(double); func = identity; printf("%f\n", func(1.2345)); func = square; printf("%f\n", func(1.2345)); func = cube; printf("%f\n", func(1.2345)); printf("%f\n", sumof(identity, 1, 100)); printf("%f\n", sumof(square, 1, 100)); printf("%f\n", sumof(cube, 1, 100)); // 配列 double (*func_tbl[])(double) = {identity, square, cube}; for (int i = 0; i < 3; i++) printf("%f\n", func_tbl[i](6.789)); return 0; }
$ clang sample59.c $ ./a.out 1.234500 1.523990 1.881366 5050.000000 338350.000000 25502500.000000 6.789000 46.090521 312.908547
関数 identity は引数 x をそのまま返します。このような関数を「恒等関数」といいます。関数 square は引数 x を二乗し、関数 cube は三乗を計算します。どの関数も引数の型は double で、返り値の型も double です。これらの関数へのポインタは double (*func)(double) で表すことができます。
func = identity でポインタ func に identity のアドレスを代入し、func(1.2345) と呼び出すとポインタ経由で identity を呼び出すことができます。同様に、関数 square と cube を func にセットすれば、func(1.2345) で square や cube を呼び出すことができます。
C言語の場合、関数へのポインタを使って、関数を他の関数の引数に渡すことができます。関数 sumof の第 1 引数 func は引数が double で返り値が double の関数を表します。func に identity を渡せば、n から m までの和を求めることができます。square を渡せば二乗の和を、cube を三乗の和を求めることができます。
sumof のように、関数を引数として受け取る関数を「高階関数 (higher order function)」と呼ぶことがあります。Lisp / Scheme などの関数型言語の場合、高階関数は特別なものではく、ごく普通に使用する機能です。C言語の場合、高階関数を使う機会は少ないと思いますが、標準ライブラリには配列をソートする qsort や、配列を二分探索する bsearch などの高階関数が用意されています。これはあとで試してみましょう。
また、double (*func_tbl[])(double) とすれば、関数へのポインタを格納する配列を定義することができます。初期化も簡単で、{identity, square, cube} のように関数名を記述するだけです。呼び出し方も簡単で、func_tbl[0](1.2345) とすれば、func_tbl[0] に格納されている関数を呼び出すことができます。
関数へのポインタはデータ型が複雑になるので、typedef で別名を付けると便利です。typedef で別名を付ける場合、他のポインタとは違って書式はちょっと複雑になります。以下に構文を示します。
typedef 返り値のデータ型 名前(引数のデータ型 仮引数名, ...); 名前 *変数名 = 関数名;
typedef 返り値のデータ型 (*名前)(引数のデータ型 仮引数名, ...); 名前 変数名 = 関数名;
typedef の場合、定義した "名前" または "(*名前)" の部分の "名前" が新しいデータ型名になります。最初の構文は、typedef で関数のデータ型に新しい名前を付けるもので、関数へのポインタを定義しているわけではないことに注意してください。したがって、関数へのポインタを宣言するときは、変数名の前に * を付ける必要があります。二番目の構文は * を付けて定義しているので、名前 変数; と宣言すれば関数へのポインタになります。
簡単な例を示しましょう。次のリストを見てください。
リスト : 関数へのポインタ (sample5a.c) #include <stdio.h> // 別名の定義 typedef double (*Func)(double); // 恒等関数 double identity(double x) { return x; } // 二乗 double square(double x) { return x * x; } // 三乗 double cube(double x) { return x * x * x; } // 総和 double sumof(Func func, int n, int m) { double sum = 0; for (; n <= m; n++) { sum += func(n); } return sum; } int main(void) { Func func_tbl[] = {identity, square, cube}; for (int i = 0; i < 3; i++) { printf("%f\n", func_tbl[i](1.2345)); printf("%f\n", sumof(func_tbl[i], 1, 100)); } return 0; }
$ clang sample5a.c $ ./a.out 1.234500 5050.000000 1.523990 338350.000000 1.881366 25502500.000000
double を受け取り double を返す関数へのポインタに typedef で別名 Func を付けます。すると、関数 sumof の引数のデータ型は Func func と表すことができ、func(実引数) で引数 func の関数を呼び出すことができます。関数へのポインタを格納する配列 func_tbl も簡単に定義することができます。
最後にライブラリ関数 qsort と bsearch の使い方を簡単に説明しておきましょう。これらの関数のプロトタイプはヘッダファイル stdlib.h に定義されています。
<stdlib.h> void qsort(void *buff, size_t size, size_t n, int (*comp)(const void *, const void *)); void *bsearch(void *key, void *buff, size_t size, size_t n, int (*comp)(const void *, const void *));
引数 buff が操作対象となる配列へのポインタ、size は配列の大きさ、n は要素の大きさ (バイト数)、comp が比較関数へのポインタ、bsearch の key が検索するキーへのポインタです。size_t は無符号整数を表すデータ型です。bsearch は見つけた要素へのポインタを返し、見つからない場合は NULL を返します。
これらの関数は汎用的に作られているので、配列の先頭アドレスは void * で、比較関数の引数も、要素を値渡しするのではなく要素へのポインタ (void *) を渡すようになっています。関数内部では、要素の位置をアドレス (たとえば char *ptr) で管理していて、次の要素は ptr に要素の大きさ n を加えて求めます。たとえば int であれば 4 を加え、double であれば 8 を加えます。
要素を比較するときは、そのアドレスを比較関数に渡します。比較関数は要素のデータ型がわかっているので、比較するのは簡単です。comp(a, b) の返り値は a == b ならば 0 を、a < b ならば負の整数、a > b ならば正の整数とします。
const で宣言された変数は値を変更することができません。たとえば、const int a = 10; と宣言したあと、a = 20; のように代入するとコンパイルエラーになります。ポインタの場合、たとえば const int *p であれば *p = 100 のような代入はエラーになりますが、p++ とか p = &a; のように p が保持しているアドレスを変更することは可能です。つまり、ポインタが参照している変数の値を更新できない、という意味になります。
簡単な例を示しましょう。次のリストを見てください。
リスト : qsort と bsearch (sample5b.c) #include <stdio.h> #include <stdlib.h> #define N 10 int buff[N] = {5, 6, 4, 7, 3, 8, 2, 9, 1, 0}; int comp(const int *x, const int *y) { return *x - *y; } int main(void) { qsort(buff, N, sizeof(int), (int (*)(const void *, const void *))comp); for (int i = 0; i < N; i++) printf("%d ", buff[i]); printf("\n"); // int key = 2; int *p = bsearch(&key, buff, N, sizeof(int), (int (*)(const void *, const void *))comp); printf("%p\n", p); key = 7; p = bsearch(&key, buff, N, sizeof(int), (int (*)(const void *, const void *))comp); printf("%p\n", p); key = 10; p = bsearch(&key, buff, N, sizeof(int), (int (*)(const void *, const void *))comp); printf("%p\n", p); return 0; }
int 型の配列 buff を qsort で昇順に並べ替えて、そのあと bsearch で検索します。比較関数は int comp(const int *x, const int *y) と宣言します。返り値は *x - *y で OK です。comp をそのまま qsort や bsearch に渡すとワーニングが出るので、comp をキャストする必要があります。関数へのポインタをキャストする場合、データ型の指定は次の形式で行います。
返り値のデータ型 (*)(引数のデータ型, ...)
変数名の部分を取り除いた形式になります。(int (*)(const void *, const void*))comp と指定すれば、ワーニングは出なくなります。キャストするのがいやであれば、比較関数を次のように定義すればいいでしょう。
リスト : 比較関数 int compare(const void *x, const void *y) { return *(const int *)x - *(const int *)y; }
引数のデータ型を const void * とし、関数の中で x と y を (const int *) でキャストします。このほうが簡単かもしれませんね。
それでは実行してみましょう。
$ clang sample5b.c $ ./a.out 0 1 2 3 4 5 6 7 8 9 0x555eab605048 0x555eab60505c (nil)
正常に動作していますね。今回はここまでです。二分探索とソートについては Yet Another Clang Problems (2) で取り上げる予定です。