C言語のポインタは難しいと言われます。しかし,ポインタについて覚えるべき文法は,それほど多くはありません。Cを勉強してきて,ポインタのところでつまずいたという場合でも「&や*などのポインタに関する文法はわかるのだけれど」という人は多いでしょう。むしろ,ポインタで難しいのは,その考え方にあります。

 考え方さえわかってしまえば,ポインタはそれほど難しいものではありません。もしプログラムを読んでいて,ポインタの動作が理解できない場面があったら,そのポインタが「何の機能を実現するために使われているか」を考えてみることが理解につながります。この特集では,考え方に重点を置いてポインタを説明したいと思います。

Cではポインタは必須

 読者の中には,JavaやC#,Perlなどのプログラミング言語を使っている人もいるでしょう。これらをはじめとする多くの言語では,ポインタというものは無いか,あったとしても,あまり意識せずプログラミングすることができます。しかしC言語では,実用的なプログラムを書くとき,必ずといっていいほどポインタを使う場面が出てきます。また,多くの標準ライブラリ関数が,引数にポインタをとるようになっています。これはどうしてなのでしょう?

 C言語は簡潔な文法を持つ言語です*1。他の多くの言語では標準で組み込まれている機能なのに,Cには存在しないといった機能があります。例えば,文字列の比較です。BASICでは,二つの文字列の間に比較演算子“=”を置くことで,文字列を比較することができます。しかしCでは,文字列をchar型の配列として実現するため,簡単に比較をすることができません。比較のために標準ライブラリ関数strcmpが用意されていますが,その引数はポインタになっています。このように,Cでは,標準で用意されていない機能の多くを,より基本的な機能を組み合わせて実現するようになっているのです。その際に重要な役割を果たすのがポインタです。

 Cではポインタを使うことによって,主に次のような機能を実現しています(それぞれについては後ほど説明します)。

1.変数の参照渡し
2.文字列や配列へのアクセス
3.動的なデータ構造

ポインタがこうした役割を果たすため,引数で呼び出し元に値を返す関数や文字列を扱う関数など,多くの標準関数が引数にポインタを指定するようになっているのです。逆にいえば,ポインタを理解しなければ,C言語をフルに使うことはできないといっても過言ではありません。

配列を関数に渡すときは要素数に注意

 次のような関数を作成すると,引数に配列を渡せるように見えます。
void func(int a[])
しかし,実際に渡されるのは配列へのポインタです。関数の仮引数宣言では,*aとa[ ]は同じ意味になります。

 つまり,次のように書いてもまったく同じになります。
void func(int *a)
この場合,関数内では渡された配列の要素数はわかりません。要素数を伝えたい場合は,
void func(int *a, int n)
のように要素数を別の引数で渡すような工夫が必要です。


ポインタを+1したときアドレスはいくつ進む?

 配列を宣言したとき,メモリー中でそれぞれの要素が占有するサイズは型によって決まります。型のサイズは,sizeof演算子を使ってバイト単位で求めることができます。例えば,
sizeof(int)
のように使います。

 Borland C++Compiler 5.5の場合,int型のサイズは4バイトになっています。型のサイズは,プラットフォームやコンパイラによって異なります。

 int型の配列では,ある要素と次の要素との間は,アドレスがsizeof(int)バイトだけ離れていることになります。つまり,int型へのポインタを+1すると,指し示す実際のアドレスはsizeof(int)だけ加算されるというわけです。

ポインタはアドレスを特定する情報

 ポインタとは一体何なのかについて説明する前に,まずコンピュータの中での変数について触れておきましょう。変数は,メモリー上のある場所に置かれます*2。メモリーは,幅が1バイトの高層ビルのような形をしていると考えることができます*3図1)。ビルに階数があるように,メモリーにも場所を特定するための指標があります。それがアドレスです。メモリー上に置かれた変数は,指定された型によって決まる大きさを持ち,その場所はアドレスによって特定できます。

図1●メモリーは幅が1バイトの高層ビルのような形をしている
図1●メモリーは幅が1バイトの高層ビルのような形をしている

 では,ポインタは一体何なのでしょう。英語ではポインタ(pointer)は「指で物や方向や場所を示す(ポイントする)もの」という意味を持つ単語です。同じように,Cでもポインタは指し示すものです。ポインタはメモリー上の,ある場所を指し示します。先ほど,メモリー上の場所はアドレスで特定すると説明しました。つまり,ポインタとは,アドレスを特定する情報のことです*4図2)。

図2●ポインタはアドレスを特定する情報
図2●ポインタはアドレスを特定する情報

ポインタ変数を使ってみよう

 概念だけではなかなか理解できないと思います。実際にポインタを使ったプログラムを見てみましょう。リスト1はポインタ変数の基本的な使い方をしているプログラムです。はじめにint型変数aを宣言し,3で初期化しています(1)。続いてintへのポインタ変数pを宣言しています(2)。ポインタ変数は,アドレスを格納する変数です。通常の変数と異なり,次のように変数名の前に“*”を付けて宣言します。

リスト1●ポインタ変数の基本的な使い方をしているプログラム
リスト1●ポインタ変数の基本的な使い方をしているプログラム

型 *ポインタ変数名
ここでの型は,ポインタ変数が指す先に格納されているものの型を指定します。int *pの場合は,pが指す先に格納されているものがint型の値であるということを意味しています。p自体はint型ではなく,アドレスを格納するための特殊な型(ポインタ型と言います)であることに注意してください。「int型の値を指すポインタ」のことを省略して,「int型へのポインタ」または「intポインタ」と呼ぶことがあります。

 (3)で,pにaのアドレスを格納しています(図3)。“&”はアドレス演算子といい,右側に書かれたもののアドレスを求める働きをします。ここでは変数aのアドレスを,ポインタ変数pに格納しています。つまり,この時点で,ポインタ変数pには,変数aの位置を示す情報が格納されていることになります。通常の変数と同様,ポインタ変数は自動的には初期化されませんので,処理の中で使う前に(3)のように値(ポインタ)を入れるのを忘れないようにしましょう。

図3●int *p =&a;を実行したときのメモリーの状態
図3●int *p =&a;を実行したときのメモリーの状態

 ポインタ変数に格納されているアドレスにあるものを実際に使うには,間接参照演算子“*”を使います。いま,pにaのアドレスが入っているため,*pはaを間接的に参照することになります。したがって(4)のように*pに値を代入することは,aに代入することと同じ意味になります*5。それを確認するために,最後にaの値を表示します(5)。このプログラムを実行すると,pを通して間接的に格納した値が表示されるのがわかります(図4)。

図4●リスト1を実行したところ
図4●リスト1を実行したところ

文字列は配列でできている

 Cでは文字列をchar型の配列として扱います。次のようなプログラムを考えてみましょう。
printf("Hello !");
文字列リテラル(定数)は内容の固定されたchar型配列とみなされ,それ自体は先頭の文字を指すポインタになりますので,このプログラムは実際には次の2段階で動作します。

1."Hello !"の先頭アドレスを獲得
2.それをprintf関数に渡す

つまり,次のように書いても同じ動作になります。
char *p = "Hello !";
printf(p);

 また,文字列リテラルは配列とみなされるので,添字演算子を付けて次のようにアクセスすることも可能です。
"abcde"[2]
これを評価すると“c”になります。