- ポインタとはアドレスではなく、C言語がプログラマに公開した間接性のモデルである -
C言語を学ぶ上で「ポインタ」というのは昔からの鬼門らしい。というのは私はポインタで躓いたことがないからで、もちろん最初から全てを完全にマスターしたわけではないですが、それでも『「ポインタ分からない」が分からない』という話になります。
したがって自分のブログでポインタについての説明をしませんでした。要するに私自身「どういうふうに説明したらよいか」分かりませんでした。
一方で、とあるSNSのC言語のポインタの説明記事の
『実用上は「ポインタ=アドレス」で構いません。「XXのアドレス」を「XXへのポインタ」と置き換えても ( あるいはその逆でも ) なんら支障が出ないからです。』
というコメントを読んだ時に「ポインタが難しい」ということの底深さは少し理解しました。
良い子のみんなは真似して「ポインタ=アドレス」と言わないでねと言いたいのですが、
それだけではブログにする意味がないかと思うので、なぜ誤解なのかを説明します。
まず、言葉の意味ですが、
pointer:指し示すもの
address:住所
です。
つまり「ポインタとは道標のようなもので、アドレスとはその場所そのものの情報」ということになります。
この観点に立つと、ポインタとアドレスの違いが分かるかと思います。「ポインタはアドレスを保持する」という認識は間違っていませんが、「ポインタはアドレス」というと重要な見落としが出てきます。
C言語やアセンブリ言語に精通した人なら『”ポインタ=アドレス”ではないという」という主張には同意してもらえるかと思いますし、K&R第2版の巻末には「Is a pointer always the same as an address?」という有名な問題があります。私自身はあたりまえの認識だとおもっていたのですが、この「ポインタ=アドレス」を主張する人が日本でも海外でも一定数いらっしゃるようです。また、それなりにキャリアを重ねた人達もいらっしゃるようで、つまり「ポインタ=アドレス」という主張が市民権を得つつあるようです。
C言語が出現して50年が経ちます。私がC言語を学んだのは約37年前になります。その頃がちょうどC言語の全盛ということでした。おそらくですが、多くの人達がポインタについて十分な知識を持たないでプログラミングをしていたでしょう。ポインタに対する理解が不十分な人達は時にプログラムをキチンと動作させることができませんでした。C言語と現代の言語を例えるのならMT車とAT車ぐらいの違いがあります。つまりC言語のポインタは正しく使わないとエンストするのですが、今の言語はAT車のようにエンストすることなく、上手にポインタを隠して安全に使えるようにしています。
つまり現在の言語を主言語としている人達がC言語を横目でみると「ポインタ=アドレス」という勘違いをしても無理からぬ面もあります。しかしながら「ポインタ=アドレス」という主張はある意味でポインタの意味論を考えたときには後退した考えになります。ここら辺で一度ポインタについて再考するのが良いかと思います。
前おきはこのくらいにしてポインタについて説明します。当初は初心者用の記事と思ったのですが、「ポインタの意義」を強調するあまり、細かな文法についての説明が完全・完璧ではないですので本当の初心者の方がこれを読んでも「?」となるだけかもしれません。解らないところはAIに聞いてもらえればよいですし、一度C言語の文法の解説を一読されてからこのブログを読まれた方が良いかもしれません。
C言語のポインタは様々な機能を実現するツールですが、ここでは
・変数の変数化
・配列の操作
について説明します。実は他にも重要な機能(関数ポインタ等)があるのですがややこしくなるので重要なこの2点に絞ります。
C言語ですが、システム記述用言語ともいわれることがあります。実は多くのプログラミング言語がC言語で書かれています。システムプログラムにおいてはポインタは中心的が技術といっても過言ではありません。実は多くのプログラミング言語の機能も内部ではポインタが使われています。ポインタとはいわば縁の下の力持ちといったところです。別の方向から見るとポインタは機械語から備わっており、プログラミングの基本とも言えますが、C言語ではそのポインタを上手く抽象化しています。
それぞれ、見ていきます。
変数というのはある種の抽象化の機能ともいえますが、プログラムを汎用的なものにする道具でその必要性は言うまでもないでしょう。
一方で変数の変数化とは、もう一段高い、抽象化とも言えます。
つまり、
int *v;
とすれば、
v = &a;
としたり、
v = &b;
としたりできます。これによりvはaやbと(変数の変数として)関連付けられます。
vが関連付けている変数(aやb)にアクセスしようとすると、
*v
とします。*記号は&記号の反対の操作を行うことになります。つまり
printf("%d", *v)
のように*vとするとaまたはbの値を取り出すことができます。
また、
*v = 1
とすると、変数aまたはbの値が1になります。

『これの何が便利なのか?』
と質問されそうですが、「変数の変数」と言われると、まずは「どこかで使えそう」という認識でよいかと思います。
以上、ポインタについて細部に入らないで利用目的の説明を行うと、「実体はaかもしれないしbかもしれないが値を操作したい場合は一つの変数として扱いたい場合は変数の変数化(ポインタを用いた段階的な参照(間接参照))を行うと便利ということが言えます。
具体例をだしますと関数の定義において2つの値を返そうとするとポインタを使う必要が出てきます。 整数除算を行う関数を考えます。a / b の商(d)と余り(r)を返す関数を考えますと例えば以下のようになります。
int mydiv( int a, int b, int *d, int *r)
{
if ( b == 0 ) return 0;
*d = a / b;
*r = a % b;
return 1;
}
関数の戻り値で1は成功、0はゼロ割を表します。ちなみに「Cにはライブラリにdiv関数があり、しかも構造体を使って値を返しているからこの関数は無意味」とおしかりを受けそうですが、そのおしかりはそのとおりですが、私的にはたまにこういう関数を作りたくなる場合があるのも事実です。
この関数(mydivとdiv)の例は「C言語で複数の値を返すポインタを使った例」となりますが、「構造体を使えば複数の値を返せる」というのもそのとおりです。ちなみに構造体を返すのは潜在的に効率が悪くなる可能性があります。64ビット時代ではdiv関数の戻り値ぐらいですと逆にパフォーマンス上推奨される場合がありますが、構造体のサイズが大きくなるとパフォーマンスの問題が発生することを頭の片隅に置いておけば将来困らないかもしれません。
ポインタに限った話ではないですが、プログラミングの難しさの1つが「やり方が複数出てくる」ということがしばしば起こります。どちらのやり方を選ぶのか?というのは悩ましい問題となりますが、どちらを選ぶかはある意味設計の範疇になるでしょう。またこの例では必ずしもポインタを使うことが良いとは限らない例としても出しています。
話は変わって、
『「変数の変数」があるのなら「変数の変数の変数」はないのか?』
と質問されそうですが、ポインタを使えばそれも可能です。変数の変数の変数は
int **p;
と定義できるでしょう。これで、
p = &v;
として、
*p
とするとvを取り出すことができます。vは先ほどの例では、aまたはbの値を取り出すことができます。 pと間接参照を2回すれば、一気に先ほどの a または b の値を取り出すことができます。 pのことを「ポインタのポインタ」とよばれることもあります。要は*が2回出てくるのでそのように呼ばれますが、「ポインタ=アドレス」というとこの「ポインタのポインタ」を「アドレスのアドレス」という風に解釈することになります。私が「ポインタはアドレスでない」とする根拠の1つがこのポインタの機能になります。
少し話が高度になりますが、ポインタに似た機能で「参照」というものがあります。参照も「変数の変数化」を行うものですが、「変数の別名」の側面が強くなります。また、多くの言語(C言語は参照はない)でサポートされている参照は「変数の変数の変数」といった数珠つなぎ的な操作は直接は行えないです。またオブジェクト指向(肥大化した構造体をポインタで扱う)との関係で、この参照をポインタという風に認識する人達もいます(実際に一部の言語ではポインタと呼ぶ)。このような経緯から「ポインタ=アドレス」という認識が出てきたようです。
最後になりますが、変数の変数化という言葉はあまり一般的でないので、あくまでも「学習のエッセンス」として覚えていただければと思います。私自身もうあまり使わないかもしれません。
ただAIによると、C言語の解説において、「ポインタはアドレスです」という説明の次によく出てくるのが、「ポインタは、(別の)変数を指し示す変数である」という表現です。
英語圏のチュートリアルでも "A pointer is a variable that stores the memory address of another variable" という表現が頻出します。
ここで補足を行いたいのですが、ポインタが難しいところの別の理由になりますが、コピーとポインタの機能的な共通点が挙げられます。
例を出します。
int型の変数、adultprice, childprice, ageがあったとします。それぞれ大人料金、子供料金、年齢としましょう。
規則として13歳以上(12歳より上)が大人料金とします。また、actualpriceに適用料金を入れることにします。
素直にコードを書けば
/* コピー */
if ( age > 12 ) {
actualprice = adultprice;
} else {
actualprice = childprice;
}
となるでしょう。つまりactualpriceは、adultpriceかchildpriceのどちらかがコピーされます。
一方で、同じことをポインタを使ってみましょう。
/* ポインタ */
int *actualprice_p;
if ( age > 12 ) {
actualprice_p = &adultprice;
} else {
actualprice_p = &childprice;
}
actualprice = *actualprice_p;
「これでは全く便利になっていない!」というおしかりを受けそうですがごもっともです。これは重要な点で「実は単純に値を変数化したいときは、『変数の変数化』を行うより『コピーを行った方が良い』場合があります(考えてみれば自明ですね)。
先ほどの例では「C言語で複数の値を戻り値とする場合、構造体にする方法もあればポインタを使う方法もある」ということですが、今回の例では『場合分けを行い値を決めたいとき、ポインタを使って複数ある値を一つのように扱える』ということが言えます。実はこの時に「値が変化しないのであればコピーで十分の場合が多い」ということがいえます。
この「コピーとポインタの役割の重なり(ポインタよりもコピーで十分)」が私なりの、ポインタの難しさの理由の1つになります。
ここまでですとポインタは使えないと判断されそうですが、もう少しポインタの利用について便利な例を次の章の最後に示します。
C言語の、「コロンブスの卵」的な設計上のすばらしさの1つが
「配列は連続したメモリとして扱う」
ということになります。
実は、ほとんどのプログラミング言語でも、内部的に見れば、メモリの連続領域を配列として扱っています。で、多くのプログラミング言語がこの事実を隠す方向にデザインされていますが、C言語はこれを前面に出してきています。ポインタを使うと配列の要素に自由にアクセスできるようになります。
C言語では、配列は以下のように定義します。
int a[10]; /*要素数10(0~9)のint型の配列を確保(注、値は不定) */
ここで、
int *p = a;
とすると、*pは配列の0番目の要素を指すようになります。先ほどの言葉をかりますと
pはa[0]の変数の変数としてセットされます。
この0番の要素というのは決まり事になります。C言語の場合、配列の先頭は0番目ということになります。
さてここでa[1]やa[2]等にアクセスしたいのですが、pを使うとどのようになるでしょうか?
実は、
*p は a[0]
*(p+1) は a[1]
*(p+2) は a[2]
・
・
・
*(p+9)はa[9]
と言ったように関連付けられます。イメージすると下記の図1のようになります。

また、例えばですが、配列aの値の最初の5個の要素の合計を計算しようとすると
int sum = 0;
for ( i = 0; i < 5; i++ ) {
sum += a[i];
}
と記述することもできますし、
int sum = 0;
int *p = a;
for ( i = 0; i < 5; i++ ) {
sum += *p++;
}
と記述することもできます。イメージすると下記の図2のようになります。

これの何が便利か?という話ですが、実は最後の
*p++
がC言語の真骨頂ともいえる記述になります。これは*pにより配列の要素にアクセスするということですが、同時に++によりポインタをインクリメントしています。このインクリメントにより配列の次の要素にアクセスできるようになります(単純にポインタに数値を足すと次の要素にアクセスできるようになるというのはメモリの連続性によるものです)。
つまり、p++とfor文を組み合わせると、
*(p + 0), *(p + 1),… *(p + 4)
と配列を順番にアクセスするようになります(図3)。

ここにさらにC言語の尖がった仕様
・文字列はchar型の配列
・文字列の最後は0
が入ると文字列のコピーは、
char dst[MAX_BUF];
char *src = "文字列";
char *p = dst;
char *q = src;
while ( *p++ = *q++ );
となり(見る人が見れば)美しいコードになります。
ここまでの説明ですが、今一つ分からないかもしれません。*p++ がなぜ美しいのかを充分に理解するにはアセンブリ言語の知識も必要かと思います。
実は一部のCPUになりますが、条件が整えば*p++は1命令にコンパイルされることがあります。
さらにいうと、while ( *p++ = *q++ ); も1命令(実際には準備を行うコードが前に作られたりします)にコンパイルされることがあります。
このあたりの詳細については次回になるかと思いますが、C言語のポインタの基本のゴールの1つがここにあります。
もっとも、これで終わりではありません。この章の最後になりますが、先ほどの料金計算についてポインタを使った例を見せたいと思います。
先ほどのコードですが、料金がadultpriceやchildpriceと大人と子供それぞれ1つでした。例えば電車の運賃を考えますと料金計算は年齢および距離から計算されるでしょう。ということで、下記の仕様でプログラムを記述してみましょう。
/*料金表*/
int adult_table[4] = { 200, 270, 340, 410, };
int child_table[4] = { 100, 140, 170, 210, };
/*計算対象(3名)*/
int people_age[3] = { 20, 10, 21 };
int price_index = 2; /* 距離要素(距離テーブル配列の要素番号を指す)*/
/* コピー */
int totalprice = 0;
int i;
for ( i = 0; i < 3; i++ ) {
if ( people_age[i] > 12 ) {
totalprice += adult_table[price_index];
} else {
totalprice += child_table[price_index];
}
}
/* ポインタ */
int totalprice = 0;
int *actualprice_table;
int i;
for ( i = 0; i < 3; i++ ) {
if ( people_age[i] > 12 ) {
actualprice_table = adult_table;
} else {
actualprice_table = child_table;
}
totalprice += actualprice_table[price_index];
}
となります。私にとってですが、実はポインタを使った例の方がコードの保守性(コードリーディングや変更に対する容易性)が高いです。
ポイントは料金計算をしているコードでコピー版の方はtotalpriceへの加算が2か所に分かれてしまっていますが、ポインタ版を一行にとどまっているところです。
totalprice += actualprice_table[price_index];
仮にですが、大人料金・子供料金だけでなく「シニア料金」や「乳児」が足されるとコピー版ではtotalpriceへの加算を行う行が増えます。そして例えば消費税の計算を入れるとか割引処理を入れるとなるとどちらのコードのメンテナンスが厄介でしょうか?ということです。
実は、配列のコピーをおこなえば、コピー版の方でも同様コードが書けますが、C言語の場合は配列のコピーは直接サポートされていないことと配列のコピーは効率的でないのであまりお勧めできないところです。ここがポインタを使うメリットの1つです。
さらに、少し進んだ話をしますとこのようなポインタの使い方(状況に応じて対象(この場合は表))を切り替えることは、はオブジェクト指向言語では良くあるテクニックになります。覚えておいて損はないです。
さて、ポインタを使った方がコードがよりクリアーになるという話をしましたが、実は上手く配列を使えばもっと良くなります。
その例を示します。
/*料金表*/
int price_table[2][4] = {{ 200, 270, 340, 410, }, /* 大人料金 */
{ 100, 140, 170, 210, } };/* 子供料金 */
/*計算対象(3名)*/
int people_age[3] = { 20, 10, 21 };
int price_index = 2; /* 距離要素(距離テーブル配列の要素番号を指す)*/
int totalprice = 0;
int age_index;
int i;
for ( i = 0; i < 3; i++ ) {
if ( people_age[i] > 12 ) {
age_index = 0;
} else {
age_index = 1;
}
totalprice += price_table[age_index][price_index];
}
料金テーブルを素直に表現している面で、私としてはこれが一番可読性もメンテナンス性も高いですが、ポイントは
・テーブルアクセスにより料金算出の一本化
・テーブルのインデックスの計算は別で行う。
ということを徹底しています。
プログラマーによっては「トリッキー」と感じるかもしれませんし、状況によると前のポインタの方が良い場合もありますが、この例から言える重要なことは、『アプリケーションプログラムを作成する上ではポインタをマスターすることは必ずしも必要ではない』というのは残念ながら今までは正当でありました。
これが、ポインタの理解がいい加減になる理由の一つかもしれません。(アプリケーションプログラム作においてはポインタに対する知識がいい加減でもなんとかなったりする)
さてこの料金計算ですがせっかくなので「サンプルから」の改善ポイントを指摘します。
・通常料金テーブルはデータベースなどに格納されているのでそこから取り出すことになります。
・今回の例では、「家族などのグループの移動」が想定されています。個別の計算も考えられます。
・いわゆるマジックナンバー(3とか12)があります。
最後のマジックナンバーについてですが、これはサンプルということでそのまま出していますが、大人と子供を分ける年齢(12)についてはテーブル化したほうがよいでしょう。具体的には、
/*料金表*/
int price_table[2][4] = {{ 200, 270, 340, 410, }, /* 大人料金 /
{ 100, 140, 170, 210, } };/ 子供料金 /
int age_rank[2][3] = {{ 0, 12, 1 }, / 子供料金対象年齢とテーブルへのインデックス /
{ 13, INT_MAX, 0 }, }; /大人料金の対照年齢とテーブルへのインデックス */
/* 計算対象(3名)*/
int people_age[3] = { 20, 10, 21 };
int price_index = 2; /* 距離要素(距離テーブル配列の要素番号を指す)*/
int totalprice = 0;
int age_index;
int i, j;
for ( i = 0; i < 3; i++ ) {
for ( j = 0; j < sizeof(age_rank)/sizeof(age_rank[0]); j++ ) {
if ( age_rank[j][0] <= people_age[i] && people_age[i] <= age_rank[j][1] ) {
age_index = age_rank[j][2];
break;
}
}
totalprice += price_table[age_index][price_index];
}
となります。テーブルのバグでage_indexに適切な値が入らない場合を考慮する必要があるのですがそのコードについては省略します。
また、age_rankを2次元配列で実現していますが、本来は構造体の配列になります(こうするとDBとの相性が良くなります)。
このコードが強力なのは、年齢区分が増えてもデータ定義の部分(料金表)の変更だけで済みます。また、いわゆるロジックで使われるマジックナンバー(この場合、12)もデータ化しているのでメンテナンス性はかなり高いです。要するにアプリケーションプログラマーとしてはこのようなコードを書くことを目指そうということになります。ポインタの趣旨からは外れますがプログラミングを考えるとこういうことを言及しなければなりません。つまり「ポインタよりもこのようなデータ駆動の方法論を使う方がアプリケーションプログラムにおいては多くの場合有効である」ということになります。
ちょっと長くなり様々な論点が出てきましたが、改めて補足とまとめを行います
・C/C++言語のポインタとは、比喩的にいうと「道標」でありアドレスは「道標が示す住所」ということになります。
・変数の値にアクセスしようとした場合に、直接変数にアクセスする方法もありますが、ポインタ(道標)を使うことにより「変数の変数化」が行えます。
・「変数化された変数」を通して元の変数にアクセスすることを「間接参照」ということもあります。
・間接参照は数珠繋が行えます。つまりポインタのポインタも作ることができます。
・ポインタと似た概念(機能)に参照というものがあります。C言語では参照は直接サポートされていませんが、しばしばポインタのことを参照と言ったりします。
・C言語では配列は、連続したメモリブロックとして認識されます。
・ポインタを使うとメモリブロックに順番にアクセスする場合、p++と言った魔法の呪文が使えます。つまりfor each(やeach)と同様の機能がポインタを使えば実現できます。
・ポインタは基本的な機能ですが、C言語上でもアプリケーションプログラムを作る上では完全ではないですがポインタを避けるプログラムもできます。
・構造体をつかったりデータ駆動アリゴリズムという高水準の機能や概念をマスターすることが、ポインタをマスターするよりもアプリケーションプログラム作成においては重要な場合もあります。
・ポインタを避けることは可能ですが、C言語(その他の言語でも)ポインタを除くことはできません。ポインタはプログラミング言語を実現する上での基本機能の1つです。C言語では配列へのアクセスがポインタの利用と同一視できるようにしていますが、概念的には配列のアクセスa[i]などは表面上だけでなく内部的にも(p+1)と認識されているという関係を持つと言えます。
・ポインタは重要かつ基本的な機能ですが、一方で危険な機能でもあります。プログラミング言語の歴史上、ポインタは避けられる傾向にあります。このような経緯から誤解が発生し「ポインタ=アドレス」という認識が生まれたようです。
ポインタとはメモリアドレスそのものではありません。ポインタとは、「間接性」という仕組みをC言語がプログラマに公開した機構です。
そして
「ポインタ=アドレス」と説明してしまった瞬間に、その間接性という本質が見えなくなります。
この記事ですが、記述するのにかなりの体力を使いましたが、本来は書くべきことの半分になります。書けるかどうか分かりませんが次回の予定は『AI時代に必要な教養としてのポインタとアセンブリ言語』がテーマになります。