Another introduction to programming language C 07: ポインタと配列1

繰り返すように,C言語はモダンなプログラミング言語が備えている機能をほとんど備えていない.配列と文字列は,特にC言語に欠けている機能である.この両者はC言語では同じものである.

最初にポインタを解説する.変数と関数はひとたび定義されると,コンピュータのメモリの一部を占有することになる.(定義はメモリを確保することを思いだそう.)例えば,地の文で

int a = 0;

と書けば,整数1個分(典型的には4バイト)のメモリがどこかに確保される.メモリを箱で表せば次のようになる.(箱一個一個の大きさはいまのところ気にしないこと.)

 ...  0  ...  ...  ...

このメモリのアドレスを求める記号がC言語に備わっている.変数aのアドレスは &a のように&(アンパサンド)をつければ求まる.さて,変数aのアドレス &a を別な変数に保存したいとしよう.こう書けばよいだろうか?

int pointer_to_a = &a; /* まちがい */

間違いである.正しくはこうする.

int *pointer_to_a = &a; /* 正しい */

pointer_to_aがintを指すポインタであることをコンパイラに伝えるために,変数pointer_to_aのほうに*(アスタリスク)をつける.なぜintのほうではなくpointe_to_aのほうにつけるのは,言語仕様として大間違いのように思えるが,涙をこらえて長いものに巻かれる.ちなみにPascalでは

var a: ^integer;

のように型名のほうにポインタであることを示す記号をつける.(たまに

int* a; /* やめなさい */

とさもint*型があるかのような書き方をする人がいる.すぐにやめるべきである.これは

int* a, b;

としたときに,意図しない定義となるだろう.)

混乱を避けるひとつの賢い方法はtypedefである.次の宣言(C言語の教科書には「定義」と書いてあるが,メモリが割り当てられるわけではないので宣言と呼ぶべきである),

typedef int_pointer *int;

は,intへのポインタほ今後int_pointerという型名で呼ぶという意味である.つまり,

int_pointer pointer_to_a = &a;

というふうに書ける.

さて,pointer_to_aは何の役に立つのだろうか.ひとつは,「逆参照」である.例えば

*pointer_to_a = 10;

とすると(pointer_to_aに*がついていることに注意),オリジナルのaのほうも値が書き換わる.上のコードは

a = 10;

と同じ効果を持つ.

ポインタは整数と足し算,引き算が出来る.これは長らくC言語がカテゴリーキラーとしての地位を確保した理由であるが,

pointer_to_a += 1;

としたとき,コンパイラはpointer_to_aがint型へのポインタであることを覚えていて,実際には1ではなく4を足すのである(int型が4バイトを占める場合).

この例のように変数aへのポインタに新しい数値を代入した場合,ポインタの内容は無意味である.そしてほとんどのCプログラムのバグが無意味なポインタに起因する.

typedef int_pointer *int;
int a = 0;
int_pointer pointer_to_a = &a;
a = 1; /* OK. aは1である. */
*pointer_to_a = 10; /* OK. aは10である. */
pointer_to_a += 1; /* 無意味なポインタを作ってしまう */
*pointer_to_a = 20; /* 深刻なことが起こる */

なぜポインタの足し算,引き算が出来るのかと言えば,連続したメモリを確保する機能がC言語に与えられているからである.次のcalloc関数(stdlib.hにプロトタイプ宣言がある)の呼び出し

calloc(100, sizeof(int))

は,intを100個並べただけのメモリを確保して,その先頭アドレスを返す.第2引数のsizeof(int)は,コンパイル時にint型が実際に何バイト占めるかという数値で置き換えられる.第2引数が必要な理由は,calloc関数をint型以外にも使えるようにしようというライブラリ設計者の親切心である.(今となっては出来心のようにも思えるが,当時はこれが親切だったのである.)intが100個並ぶとメモリはこうなる.

 ...  0  0  ...(全部で100個)  0  ...

callocの呼び出しは典型的には次のようになる.

#include <stdlib.h>
...

void func(void) {
  int *b = calloc(100, sizeof(int)); /* 100個の連続したintを用意してbに先頭アドレスを保存する. */
  *b = 0; /* bのひとつめの要素は0. */
  b += 1; /* bのふたつめの要素へ移動. */
  *b = 1; /* bのふたつめの要素は1. */
  *(b + 1) = 2; /* bのみっつめの要素は2. */
  ...
}

C言語では,面白いことに

x[y]


*(x + y)

はいつも同じ意味である.したがって,

b[1] = 2;


*(b + 1) = 2;

と同じことである.もちろん,x+yとy+xは同じだから

1[b] = 2;

としても同じである.(こんな書き方は見たことがないが,規格上は許される.)

変数はアドレスを持つと言った.関数はアドレスを持つだろうか?当然,関数もアドレスを持つ.それは,大変にわかりにくい書き方をしなければならないが,関数もアドレスを持つのである.例えば,次の通り.

void print_int(int a) {
  printf("%d\n", a);
}

void (*pointer_to_print_int)(int) = &print_int;

pointer_to_print_intの唯一の使い道は,それをコピーすることと,元の関数を呼び出すことである.次の書き方はポインタを通した関数の呼び出しである.

(*pointer_to_print_int)(100); /* 100を印字 */

実は逆参照記号は省略出来る.

pointer_to_print_int(100); /* 100を印字 */

は正しいコードである.さらに面白いことに,最初の&print_intの参照記号さえも省略出来る.

void (*pointer_to_print_int)(int) = print_int;

もちろん,関数ポインタへの足し算,引き算は無意味である.関数ポインタはCプログラムのありとあらゆるところで使われている.にもかかわらず,これほど嫌われている機能もない.C++ではメンバ関数が,Objective-Cではメッセージが,Javaではメソッドが,Lisp/Schemeではラムダ式が,エレガントに関数ポインタを包んでいる.(Schemeには関数ポインタの何百倍も強力な第一級継続という機能がある.)

もう一度変数のポインタに戻る.連続したメモリを確保するのにcalloc関数を呼びたくない場合もある.典型的には,数個から数百個の変数を連続して確保したい場合,なおかつ関数の中でだけ使う場合,いちいちcallocを呼びたくないこともある.C言語はこの目的のために,次の定義構文を用意している.

int c[10] = { 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 };

意味は,10個の連続したint変数をメモリ上に確保し,その先頭アドレスをcとするという意味である.つまり,微妙な差異を除けばcallocと同じことをしている.ただし,関数の中でこの書き方をしたときは,cは自動変数であるから,関数終了時にc全体が破棄される.

例によって初期化は省略出来る.

int c[10];

静的変数の場合は0が,自動変数の場合は未定(だいたいランダムという意味)の値が10個分入る.

C言語に多次元配列はない.たまに

int d[20][30];

という書き方を見かけるが,見なかったことにするのが礼儀である.
Comments