C言語は、ポインタが使える言語です。ポインタを使えば、メモリの直接的な操作など、より柔軟なプログラミングが可能です。一方、そのためにはポインタがどのようなメモリ領域を指し示しているか、またポインタ自体が有効なアドレスを保持しているかなどについて十分な理解が必要です。

本稿では、C言語のポインタについて、配列との対比に焦点を当てて解説を行います。

***

C言語では、ポインタを配列のように扱うことができます。また、配列名はポインタに変換可能で、別のポインタに代入できます。

まずは1次元の場合について、ポインタと配列の関係を考えてみましょう。

宣言文の記述

いま、ポインタと配列を次のように宣言したとします。

char *p;    ← char型へのポインタpを宣言
char a[5];  ← char型を要素とする、要素数5の配列aを宣言

この宣言の結果を図にすると、図1、図2のようになります。

図1 char型へのポインタpを宣言

図2 char型を要素とする、要素数5の配列aを宣言

このように、「char *p;」という宣言を行うと、ポインタ(p)自体を格納するためのメモリ(32bitのOSの場合は通常4バイト)が確保されますが、そのポインタの値(pの値=アドレス)は初期化されておらず、ポインタ(p)が指し示すchar型(*p)のメモリ領域もまだ存在しない状態となります。このあと実際に*pを使用するには、あらかじめpに何らかの有効なアドレス値(たとえば別の配列の先頭アドレスやmalloc()で確保したアドレスなど)を代入しておく必要があります。

一方、「char a[5];」の宣言では、配列要素のchar型5個分のメモリ(5バイト)が実際に確保されます。そして、配列名aは、配列の先頭アドレスを示すアドレス定数となります。アドレス定数は、値が定数であるポインタと考えられます。なお、aは定数であるため、a自体を格納するメモリ領域は存在せず、aの値を変更するような操作(a++とかaへの代入など)はできません。つまり、配列の宣言とは、「配列要素分のメモリ確保を伴う定数のポインタの宣言である」と言えます。

以上のように、宣言文では、*と[ ]は違う意味を持ちます。ところが、式の中(プログラム本体)では、*と[ ]とは、互いに書き換え可能です。このことが、ポインタと配列の混乱の一因かもしれません。

式の中(プログラム本体)では

式の中(プログラム本体)では、「char *p;」と宣言されたポインタに対して、pに適切なアドレスが代入されたあとでは、p[0]やp[1]のように配列のような記述が可能です。同様に、「char a[5];」と宣言された配列に対して、*aのように、ポインタのような記述が可能です。

一般に、式の中では、a[i]という記述は*(a+(i))と等価で、結局、ポインタで記述しても配列で記述しても同じことになるのです。

ところが、前述のとおり、宣言文においてはポインタの宣言と配列の宣言では意味がまったく異なり、たとえば「char *p;」と宣言したあと、pが未初期化のままp[0]やp[1]を使用すると、未定義のメモリをアクセスしてプログラムが正常に動作しません。つまり、式の中では*pと書いたりp[1]と書いたりするとしても、宣言時には*と[ ]とをしっかり区別して宣言する必要があるということです。

以上を踏まえて、リスト1を見てみましょう。このプログラムの3カ所のprintf()では、すべて文字「X」を表示するはずです。

リスト1 ポインタと配列を使ったサンプルプログラム

  char *p;
  char a[5];

  a[2] = 'X';              ← 配列の添字[2]の要素に文字'X'を代入
  p = a;                   ← 配列名(定数アドレス)はポインタに代入できる
  printf("%c\n", p[2]);    ← ポインタは配列のように記述できる
  printf("%c\n", *(p+2));  ← ポインタとして記述する場合はこうなる
  printf("%c\n", *(a+2));  ← 配列名もポインタとして記述できる