やっぱりポインタの話

少し前、以下の記事がホットエントリに上がっていました。

givemegohan.pigboat.jp

タイミング的にかなり遅れてしまいましたが、せっかくCの話題が出たことですし、宣伝しますよ。

メモリとアドレスの話

 ポインタの話をするにあたり「例え話をしない」というスタンスには共感しますし、ポインタを理解したいなら結局メモリを理解しないといけない、ということにも同意します。

ただ、拙著「C言語 ポインタ完全制覇」の旧版では、メモリとはどういうものかについてあまり説明しませんでした。17年も前の本ですから、当時は、プログラムを書こうというような人はある程度メモリの概念ぐらいはわかっているだろう、と仮定していたのだと思います。その仮定が正しかったかどうかはわかりませんけれども。

今となってはメモリの概念とか考えたこともない、という初心者も多そうなので、「C言語 ポインタ完全制覇」の改訂版では以下のような説明を入れました(1-2-1 メモリとアドレス)。

現在のコンピュータで主記憶として主に使われているのは、ダイナミックRAM(DRAM)と呼ばれるもので、非常に小さな蓄電器(コンデンサとかキャパシタとか呼ばれます)の充電の有無*1で、0か1かのいずれかの状態(これをビット(bit)と呼びます)を表現します。

(中略)

そして、メモリの内容を読み書きするためには、膨大にあるメモリのうちのどこの情報にアクセスするのか、ということを指定しなければなりません。この時に使う数値がアドレス(address)です。メモリ中の各バイトに、0から順に「番地」が振ってある、と今のところは考えておいてください*2

f:id:kmaebashi:20180213002945p:plain

「(中略)」のあたりには、2進数の説明が書いてあったりします。

 

――ただ、問題は、これで「メモリとアドレス」を理解したからといって、Cのポインタを理解できるとは限らない、ということです。

上記の記事に、私は以下のブックマークコメントを付けました。

例え話をしないC言語のポインタの説明 | 右や左の旦那様

ポインタは、「メモリ上の他のアドレスを指す変数」ではなくて派生型の一種。Cのポインタが難しいのは主に宣言の構文がクソだから。この辺のことは拙著「C言語 ポインタ完全制覇」をどうぞ。C99対応の改訂版出たよ!

2018/02/01 20:41

b.hatena.ne.jpそしたら以下のように書かれた。(´・ω・`)

例え話をしないC言語のポインタの説明 | 右や左の旦那様

↓kmaebashiのような説明がポインタの理解を阻害していると思う。/「緑の板に黒い四角いのが付いたアレ」これも余計な話。

2018/02/03 13:47

b.hatena.ne.jp

まあこんなことを100文字しか書けないブクマコメントで説明しようというのはどだい無理な話なので、ここでちょっと補足します。

ポインタは派生型の一種である

上記リンク先の記事には、

ポインタは、「メモリ上の他のアドレスを指す変数」です。

とあります。

Cのバイブルと言われる「プログラミング言語C」(通称K&R)においても、ポインタについて同様の説明をしています。

これについて、「C言語 ポインタ完全制覇」では以下のように書きました。

 

「ポインタ」という言葉について,K&R では以下のように説明しています(p.113 第5 章「ポインタと配列」冒頭部分)。

ポインタは,他の変数のアドレスを内容とする変数であり,C では頻繁に使用される。

「アドレス」については前項で説明しましたので、「他の変数のアドレス」とは何か、ということについては、なんとなくイメージが付くのではないでしょうか。

ただ、この説明、バイブルにケチをつけるようでなんですが、かなり問題のある表現だと思います。この説明では,まるでポインタといえば「変数」であるかのようですが、実際には必ずしもそうではないからです。

一方、規格のほうでは「ポインタ」という言葉を以下のように定義しています(6.2.5「型」)。

ポインタ型(pointer type)は,被参照型(referenced type)と呼ぶ関数型,オブジェクト型又は不完全型から派生することができる。ポインタ型は,被参照型の実体を参照するための値をもつオブジェクトを表す。被参照型T から派生されるポインタ型は,“T へのポインタ”と呼ぶ。被参照型からポインタ型を構成することを“ポインタ型派生”と呼ぶ。

派生型を構成するこれらの方法は,再帰的に適用できる。

何のことだかさっぱりかもしれませんが(なにしろ規格書なので、そう読みやすいものではありません)、とりあえず、最初の一句に注目してください。「ポインタ」と書いてありますね。

型、といえば,int型やdouble型が思い浮かびますが、Cには、それと同様に「ポインタ型」という型があるのです。

ただし、大急ぎで付け加えますが、「ポインタ型」という型が単独で存在するわけではなくて、他の型から派生することにより作り出されます。上記の引用でも、後ろのほうに「被参照型T から派生されるポインタ型は“T へのポインタ”と呼ぶ」と書いてあります。

つまり,実際に存在する型は「int へのポインタ型」や「doubleへのポインタ型」だということになります。

「ポインタ型」は型ですから、int型やdouble型がそうであるように「ポインタ型の変数」も「ポインタ型の値」もあります.そして――厄介なことに、世間では「ポインタ型」も「ポインタ型の変数」も「ポインタ型の値」も,単に「ポインタ」と呼んでしまうことが多いので、混同しないように気をつけてください。

POINT

最初に「ポインタ型」がある.

「ポインタ型」があるんだから「ポインタ型の変数」も「ポインタ型の値」もある.

たとえば,C では、int という型は整数を表します。int は「型」ですから、int型を格納するための変数もありますし、int型の値もあります(「5」とか)。

ポインタ型もそれと同じで、ポインタ型の変数もあり、ポインタ型の値もあります。

そして、「ポインタ型の値」は、実際にはメモリのアドレスのことです。

 

バイブルに楯突いてでもこのような説明をした意図はふたつあります。ひとつは、実際に「ポインタ型変数」と「ポインタ値」を混同してしまう人がそれなりにいること、もうひとつは、ポインタが「型」であることがわからなければ結局Cの宣言からして理解できないからです。

よく、たとえばintへのポインタを宣言する際、

int *a;

ではなく

int* a;

 のように*をintに寄せて書こう、という人がいます。これは、そう書いたほうが、「int*という型の変数aを宣言している」ということが明確になるからですよね。まあ、この書き方は一度に複数のポインタ型変数を宣言しようとすると破綻しますし、Cでは配列も派生型の一種なのに(これも重要!)配列を示す[]はintに寄せて書きようがない、という点で不完全ではありますが、「int*という型」を強調したいという気持ちはわかります。

そして、この宣言の構文こそがCのポインタを奇ッ怪なものにしている主犯だと私は思うわけでして――

Cの宣言の構文はクソである

えー、ここから先は本を読んでください、と言いたいところですが、何もたとえば

void (*signal(int sig, void (*func)(int)))(int);

みたいな極端な例を出さずとも、それこそ前述のとおり

int *a;

という宣言からして「int*という型の変数aを宣言している」ということがたいがいわかりにくいですし、

char *color_name[] = {"red", "green", "blue"};

とあったら、このcolor_nameは「charへのポインタの配列」です。

二次元配列を関数に受け渡すなら、

/* 3×3の行列をfuncに渡したい */
void func(double (*matrix)[3])

といった書き方もします。まあこれは

void func(double matrix[3][3])

と書くこともできますが、こうして受け取ったmatrixを別変数に退避したり、malloc()で領域を確保しようと思ったら、いずれにしても「double (*matrix)[3]」という謎の表記から逃げることはできません。

実のところ、Cを何年も書いているようなプログラマでも、このあたりのことをちゃんとわかっていない人は多いものです(というか私自身、理解に数年かかりました)。

だからといって、どうでもいいようなトリビアだとは私は思いません。実際に実用のプログラムで十分使う知識だからです。ちゃんと理解しないでなんとなく既存プログラムから写して使うんじゃ、コピペプログラマじゃないですか。

C言語 ポインタ完全制覇」は、こういった宣言の読み方をはじめ、実行時に変数のアドレスがどう決まるのかとか(コンパイラが決めるんじゃないよ!)、物理アドレスと仮想アドレスとか、連結リストのようなデータ構造とか、実践的な内容が盛りだくさんです。伊達にポインタだけで300ページ以上書いているわけではありません。手前味噌になりますが、おすすめできると思っています。

gihyo.jp

*1:蓄電器が小さすぎて一定時間で放電してしまうので、定期的に書き込みなおす(リフレッシュする)必要があり、この特徴から「ダイナミック」RAMと呼ばれます。

*2:実際には、これは物理アドレスであり、今時のPCでプログラマが普通に扱うアドレス(仮想アドレス)とは異なります。「2-1 仮想アドレス」にて後述します。