Another introduction to programming language C 08: ポインタと配列2

ポインタと配列について,別の角度から解説する.

メモリの割り当ては,静的,自動的があることを述べた.calloc関数は第三の方式であって,これを狭い意味での動的と呼ぶ(自動的は広い意味の動的に含まれる).動的な変数のうち,自動的な変数のために確保されたメモリは自動的に破棄される.一方,狭い意味での動的に確保されたメモリは自動的には破棄されない.例えば,

int *func(void) {
  int *a;
  a = calloc(100, sizeof(int));
  return a;
}

として関数funcから取り出したintポインタは,関数funcの外でも「生きて」いる.callocされたメモリを捨てるには,プログラマが明示的にfree関数を呼ばなければならない.ところが,まだ必要なのに捨ててしまうという事故が絶えない.そこで,小さなプログラムでは,callocしたメモリはいつも捨てずに置いておくことを勧める.(C++を除く近代的なプログラミング言語では,メモリは自動管理されているからこのような事故は起こらない.)

callocしたメモリは次のように配列としてアクセスできる.

int *a = calloc(100, sizeof(int));
a[2] = 20; /* aの第3要素(第2要素ではない)に20を代入する */

同じことが,静的変数,自動変数でも出来る.

int b[100]; /* 初期化は省略 */
b[2] = 20;

C言語ではいつでもx[y]と *(x + y) は同じ意味である.ポインタaに対してa[i]は *(a + i) であり,これは配列のi番目の要素を指す.ということは,静的変数,自動変数バージョンのb[i]も *(b + i) と同じという意味である.つまり

int b[100];
int *pointer_version_of_b = b;

とするとpointer_version_of_bはbと同じように使える.pointer_version_of_b[2] = 20 は b[2] = 20 と同じ意味である.なお,pointer_version_of_bの定義は

int *pointer_version_of_b = &b[0];

とする場合もある.こちらのほうがどちらかと言えば通な雰囲気がする(理由は後述).

配列版とポインタ版の唯一の違いは,その変数へ代入が出来るかどうかである.

int *a = calloc(100, sizeof(int));
int b[100];
a += 1; /* OK */
b += 1; /* エラー */

配列に関する注意を列挙する.
  • 添え字の範囲チェックは行われない.
  • 配列の大きさは変えられない.
  • 配列の大きさは配列とは別途管理しなければならない.
  • 配列に対して一括操作(例えば全体の2倍するなど)の仕組みはない.
  • 多次元配列,連想配列は陽にサポートされない.
Cプログラマは,それでも配列とポインタを使わざるを得ない.

配列,ポインタを使ったプログラムの例を挙げる.次のコードは100個の配列の中身を順に偶数で埋めていく.(Pythonで言うところの a = [2 * x for x in range(0, 50)] である.)

int a[100];
int i;
for (i = 0; i < 100; i += 1) {
  a[i] = i * 2;
}

興味深いことにa[100]へのアクセスは無意味であるが(a[0]からa[99]までだけが意味を持つ),&a[100]すなわち a + 100 は意味のある数値であることが保証されている.この性質をC++プログラマは最大限に使う.もしCプログラムの中に次のようなコードを見つけたら,優れたC++プログラマの仕業と見てよかろう.

int a[100];
int *b = &a[0], *e = &a[100];
while (b != e) {
  *b = ...;
  ++b;
}
Comments