ポインターは難しくない

メモリーの利用形式を説明する前に、ポインタについて触れておいた方がいいでしょう。よく、ポインタの理解が、初心者のぶつかる最初の難関だとかいわれます。簡易なプログラミング言語だと、「ポインタのような難解な概念を用いることなくプログラミングできる」などというのが売りになっています。

ちょっとまってくれよ、といいたいわけです。実は、ポインタの概念自体は全然難しくないのです。ただのメモリーの地番のことなのですから。ここまでにも、アドレスとかポインタとかいう話はでてきました。難しかったですか?ポインタというのは、だいたい次のようなことです。

メモリーの地番

パソコンにはメモリー(RAMですね)がついています。それは、数百メガとかギガとか、かなり広大な領域になっています。単位はバイトです。このメモリーの場所を特定するのに1バイト毎に、数字を割り振っているのです。つまり始まりが0、1バイト上が1、さらに1バイト上が2、という風にです。普通、番号が若い(小さい)方を"低い"(low)といい、だんだん上に数字が増えて行きます。数値が大きい方は高い(high)といいます。低い方のメモリーは、だいたいOSが専用しますので、アプリケーションは、それより高いところを使わせてもらうことになります。

日常に溢れる"ポインタ概念"

このメモリーの場所を特定するために打たれた番号がアドレスとかポインタとよばれるものです。メモリーに何かを記憶したとしても、それを取り出せなければ意味がありません。このポインタをつかって、格納した場所を特定して引き出すわけです。例えば、ロッカーがあるとしますね。ロッカーをメモリーと思ってください。まず、空いているところを捜して(個人占用のロッカーじゃなくて、駅とか銭湯とか、あと公共のプールの更衣室とかにあるやつですよん。知らない?こまったな...下足札じゃ、もっと知名度ないよなあ。クロークとかの番号札にしてもいいです)どこかに決めます。このとき、ロッカーの番号がありますよね。これが格納時点で得たポインタ。そしてそこに何か荷物を入れて鍵をかけます(料金はこの際無視します)。で、適当に過ごしたあと、ロッカーに入れておいた荷物が必要になったとします。まず、鍵を取り出して、番号を確認しますね。まあ、短時間なら自分で覚えてればいいんですが、普通、忘れないように、鍵に番号ついてますよね。で、その番号で、自分の荷物を入れたロッカーを特定します。そしてめでたく見つかったら、鍵を開けて荷物を取り出します。このときの、荷物がメモリーに入れた値で、ロッカーの番号が値取り出しに使われるポインタです。そして、番号を忘れないように貼付けてある鍵が、ポインタ型変数ということになります。まあ、メモリーの値は取り出してしまうことは滅多になくって、コピーして使うんですけどね(ロッカーの中身を確認するだけ)。何か難しいですか。

もしも、ポインタの概念が難しいというのなら、電話もかけられない(電話番号の概念が難しい)、郵便も出せない(宛先の概念が難しい)、集合住宅にも住めない(部屋番号の概念が難しい)でしょう。"私はこの人に電話をしたいんだ。数字に電話したいんじゃない!"とか?そんな風にいえる人の方が難しいですね。いや、まあ、力んでもしょうがありませんね。ともかく、ポインタの"概念自体は"全然難しくないのです。少なくとも、なぜこういうものが必要かがわかれば、難解というのは間違いです。確かに最近のOSは仮想メモリーとか色々複雑なことをやって(たまにバグって)ますが、そういう話じゃないですよね、ポインタが難しいって話は。

難しいのは"ポインタ型変数"

でも難しいと思われているのはなぜでしょう。実は、ポインタの概念ではなくて、ポインタ型変数の扱いが難しいのです。"変数の型(type)"ですね。C言語(ポインターの王様)とかJava言語(ポインターの隠蔽)とかがthe programing languageであるため、初心者は変数とその型と値をごっちゃにしてしまうのです。って、解説とかも、ポインタの概念とかいって、ポインタ型変数の説明してますけど、まあ、概念だけじゃ説明すること少ないですから。確かに、ポインタ型変数の扱いはミスを誘発しがちですが、それでも、まず、この機構がなぜ必要かにピンとくれば、理解できないことはないはずです。

問題状況をC言語を念頭において書いてみます。うまくいったら、おなぐさみ。

ポインタ変数は、普通そのメモリの場所にどんな種類の値が入るかをつけて宣言します。4バイト整数が入るとすれば、
long*p;  //long(4バイト)値のポインタ宣言
なんてやるわけです。変数の前に星印がつくのがポインタって証拠ですね。この星はlongの後ろにつけて"longのポインタである変数p"と読む方法もあります。まあ、どこかに星があればいいってことですか。つまり、変数名は"*p"ではなくて"p"なのです。星印はC言語では特別な意味を持つ(演算子)ので、変数名や関数名には使えません。さて、宣言の仕方ですが、私は変数の前につけるのが好きです。というのは、星印"*"には、"その変数に入っているアドレスの場所にある値を取る"という意味があるからです。いわゆるデレファレンスですね。つまり、pに入っている値はデレファレンスするとlong整数がとれるよ、と。デレファレンスできるんだから、場所の値のはずで、つまりポインタということになります。ポインタのポインタというのも当然あり得て、星を二つつけるだけですね。格納されている値はアドレスで、そのアドレスの場所にはやはり別のアドレスが入っており、そのアドレスの場所には何かの値があるということです。ポインタのポインタのポインタ....と、どんどんいけますが、星二つを越えて必要になることはまずないでしょう。つまり、星二つ目までは、単に人を混乱させるためのオマケではなくて、使う必要があるから準備されているのです。

ポインタはコンピュータには是非必要

じゃあ、なんでこんなポインタみたいなものを使うのか。理由は単純で、ポインタを通してしか操作できないタイプのデータがあるからです。具体的には、データを1セルよりも大きなデータの塊をまとめて扱う場合です。前回触れた、構造体やアレイもその例ですね。また、関数も1セルより大きいデータ(コード)のまとまりという意味では、データとして特定するにはポインタを使うしかありません。コンピュータというのは、データを処理するときに1セルを越える大きさをもつものを一挙に扱うことはできないのです。ですから、そのようなデータの塊を格納した変数に名前があるとしても、普通の変数と同じやり方でその名前と値そのものとを同一視することはできないのです。大きなデータ処理するときには、細かく区切って順次全体を処理することができるに過ぎません。例えば、テキストファイル中のテキスト全体を一挙に処理する場合、どうやってデータを特定するでしょう。これは、全体をメモリーの中に読み込んで、その先頭のポインタと、データの長さで特定するのです。構造体やアレイなども、データの先頭からのズレ(オフセット)でメンバーや項目要素が特定できます。結局、こういった大きなデータの塊は、そのポインタで特定するしかないのです。ですから、構造体やアレイには名前はつけられますが、その名前が意味するのは、データ領域のポインタです。構造体などを値として解釈できるようにする場合には、値であるかのように見せかけるための特別なやり方を決めなければならないのです。つまり、実際にはポインタとして扱っているにもかかわらず、値であるかのようにプログラミングできる文法をこしらえるということです。

ポインタでしか特定できないタイプのデータがあるからポインタを使う。全く、ストレートで当たり前のやり方です。これに対して、そういうのは嫌だというひとが多くなって無理に隠そうとする言語も出てきたんでしょうが、この程度なら特に無理する程のことでもないように思います。

ところが、ここから艱難辛苦が始まるのです。とくにC言語などでは。私の全く個人的な考えでは、この困難は、プログラミング言語設計者の、変数を値と同一視させようとするあからさまな努力がもたらしたものです。この同一視は、突き詰めれば、変数のタイプとデータのタイプを混同させようとする努力にもつながって行くのは明らかです。

確かに、論理的にはこれらは一致しなければならないのです。しかし、同じものではない、つまり別個の概念であるからこそ、"一致しなければならない"という規範(規則)がでてくるわけです。論理的に誤りなく考えるには、強く意識しなければならない事柄を、初心者向けの解説程混同させようと躍起になっているのはグロテスクではあります。頭で理解させるよりも、命令通り動くようにさせようということでしょうか。

無効ポインタ

まず、例えば、前で、longのポインタとして宣言されたポインタ型変数pのポイントする場所に値を格納しようとして、
*p = 5;  //このままだとできない
とやってみたとします。これはできません。コンパイラはエラーを吐きます(多分)。なぜでしょう。ポインタpのデレファレンスしたときの値として5を代入したんだからいいじゃないか、と思われますか?

でも、ポインタがメモリーの場所を意味していることに注意すれば、なぜダメなのかがわかります。pに割り当てられるメモリーの場所が、どこでも特定されていないからです。この場合、pの中身は大抵無意味な値となっています。これでは、コンパイラがエラーを吐かなかったとしても、5はどこに入るかわかりません。何か重要な値を上書きしてしまうかもしれません。

もっとも、メモリーの割当くらい、宣言のときにコンパイラが自動にやってくれればいいじゃないかと思われるかもしれません。どうせ必要なのはわかっているんだからと。ところが、longのポインタであるpのアドレスの場所に確保される領域は、ちょうど4バイトだけとは限らないのです。例えば、long*であるポインタの先には、4バイト幅の構成員をもつアレイがある場合もあるのです。ですから、必要な分を自分で確保しなさい、ということになっているのです。まあ、この点も、コードの全体を見ればわかることなのでコンパイラが自動にやってくれよ、という考え方もなくはありませんが、C言語のコンパイラはそこまでバカ親切ではない、ということです。というよりも、状況に応じて動的に対応するというのは、Cコンパイラの辞書にはありません。プログラマが初めから全体をきっちり決めていないといけないのです。そうすることによって効率的なコードを生成する、というのが、C言語系のひとつのポリシーみたいなものなわけです。

有効なポインタを得る

そこで、普通は、既に確保されたメモリ領域のポインタをpに代入します。
long x;
p = &x;
こうしてしまえば、pがポイントする先のメモリはlong整数変数xのために確保されたメモリのアドレスが代入されます。"&"というのもC言語では演算子で、後続の変数のアドレスを取るという効果があります。以後はメモリの場所は既に割り当てられているわけですから、値の割り付けも普通に可能になるはずなわけです。つまり、
*p = 5 ;
とやっても、今度はエラーにならず、変数xには5が格納されることになるのです。間接的な代入ですね。

ヒープ領域のポインタ

変数をもう一つ宣言するのはバカっぽいので、状況に応じてメモリーを準備する方法はないかというと、ないわけではありません。それには、malloc()という関数を使い、ヒープメモリー(単にヒープともいいます)に必要な大きさの領域を確保するのです。ヒープメモリーというのは、アプリケーションが随時使えるようになっているメモリー領域のことです。普通の変数用の領域とは別に準備されています。これをOSにお願いして使わせてもらうわけです。
p = (long*) malloc((size_t)100);
これで、100の領域が確保され、そのポインタがpに格納されます。"(long*)"とかついているのは、malloc()の返す値の型が、必ずしも、long*じゃないので(Macintoshでは確かvoid*と定義されていたと思います)、型を変えちゃうんですね、"p"に合わせて。こうしないと、エラーがでるかも(忘却の彼方^^;;)。ともかく。こういうのは、キャストといいます。まあ、普通は100とか定数を使わず、sizeof()関数をつかって計算したりして、他所のコードが変わっても直しやすいようにするんですが。型も合うし。数値のキャストも要らないかも知れません(コンパイラ依存?100だと何になるんでしょ?やっぱりエラーかな。)。まあ、つけて悪いことはないですけど。初めから"void* p;"と宣言しておけば良かったですね(いいのか?)。すみません、長いことCで書いてないもんで(しかもまだ初級者)。

メモリーリーク

ところが、また、この扱いが面倒(といわれている)なのです。pを局所変数として宣言しておくと、pの有効期限が切れたとき、その格納領域は廃棄されてしまいます。ところが、廃棄されるのは、pの値、つまりアドレスを格納していた場所だけであって、malloc()でヒープに確保した領域はそのままになってしまいます。また、誤ってpに別の値を格納してしまうこともあります。これもpの値を上書きするだけで、確保されたヒープ領域はそのままです。これらの事態が起こってしまうと、このヒープ領域のアドレスを保管していたのは変数pだけなのですから、もう、アクセスしようにも、それがどこにあるのかわかりません。いってみれば、ロッカーの鍵をなくしてしまったわけです。少しならまあ、それほど大きな弊害は出ませんが、みんながあちこちで鍵をなくしてしまうと、だんだん空きロッカー、つまり空きメモリーが少なくなってきます。これが、よくいわれるメモリーリークというバグです。

ですから、自分でヒープ領域を確保した場合には、用が済んだら、まだポインタを保持している間に、必ず自分で解放しておかなければなりません。ヒープ領域の解放にはfree()という関数をつかいます。
free(p);

(補遺)void*の意味

malloc()の戻り値の型はvoid*と定義されています。また、free()の引数もvoid*と定義されています。
void* malloc(size_t size);
void free(void* ptr);
voidというのは"空"という意味です。それだけだと、無いという意味になります。free()には戻り値はありません。void*なら"何もないもののポインタ"で意味が分かりませんね。これは、"なんでもいいから何かのポインタ"という意味になるらしいです。malloc()は受け側の変数"p"にlong*という確定した型があるので、合わせる必要があるということです。free()の引数の場合は、何でもいいから何かのポインタ、という意味からして、適当にポインタを渡せば解放してくれるわけです。("どんなポインタもvoid*には自動型変換される"とかいう言い方の方が、好ましいんですかね。型通りの世界では。)

ガベージコレクション(ゴミ集め)

ところが、これが、複雑なプログラムになると、いつ解放していいのか、ものすごく難しくなるのだそうです。勝手に合鍵をつくっちゃって、荷物(データ)を共有しちゃってたりすることがあるのです(多重参照)。そこで、ロッカーに使用期限をつけるわけです。駅前のコインロッカーだと3日間くらい?よく知りませんが。まあ、期限が来たら一斉に整理、というのはちょっと暴力的なので、機構としては、初めに荷物を預けるときに、連絡が取れるように、住所とか、電話番号とかメールアドレスとか書かせておくわけです。そして、一定時間ごとに、「鍵持ってますか?まだ使いますか?」とみんなに聞いてまわるわけです。連絡がとれない、というか、鍵を持ってる人が誰もいない(そのポインタがどこからも参照されていない)ということがわかると、そのロッカー(メモリー領域)は強制的に空けられて、別の人がまた自由に使えるようにされてしまうわけです。この仕組みは「ゴミ集め」(garbage collection)といわれます。Javaにはこれがついているので、初心者でも安心といわれます。これでまた一歩、初心者をコンピュータの理解から遠ざけた、、、ような気が私にはするんですが、、、、表向きは、複雑でどうしようもないときに使うってことなんだと思いますがねえ。

もともと、ガベージコレクションの機構というのは、確かLispで、リスト操作についてどうしてもそれが必要(あるいは関数型プログラミング以外のことを表に出したくない?)という理由で装備されたものだったと思います。近代的開発環境には不可欠みたいに宣伝したこともまた事実でしょうが。是非必要、あるいは極めて便利、であるかどうかは、その言語の特性にもよるでしょう。PowerMopsにもレファレンスとの関係でガベージコレクションは実装されています。

インクリメントのステップ

もう一つ、混乱を招くかもしれないポインタ型変数の性質があります。ポインタは普通の(符号なし)整数値であるということは、もうお分かりでしょう。ですから足し算とかもできます。ところが、例えば、前のようにlong *pで宣言したpを1増やす操作(++p)をするとpの値が1ふえたものになるかって言うと、そうはならないのです。4増えます。なぜでしょう。それは、pのポイントしている領域の値がlongで4バイト整数だからです。ポインタに1を足すと、"次の区画"の先頭に移るのだと解釈されます。ですから、char *qの++qとすれば、qの値+1になるのです。あるいは、80バイトくらいの構造体のポインタとして宣言された変数は、1増やすとポインタの値は80増えるわけです。ポインタの型指定のときに「何の」ポインターかを明示する意味は、ここにあります。重要なのは、この「何」が何バイトの大きさなのかというところだけなのですが、ともかく、型指定すれば、その型のバイト幅はコード内のどこかからわかるわけですから、アレイの計算が型通りに手早くできるわけです。

これはポインタ型変数の性質というよりもコンパイラの挙動、というか解釈方法の問題で、コンパイラがご親切にも合理的な意図を汲み取って、そのように処理してくれるということです。これで、ポインタがメモリーに順番でつけられた番号の値で、その計算はメモリー内の位置のズレ(オフセット)を計算することだ、という事実が、中途半端に隠されてしまうわけです(^^;;)。

結語

大体、基本的なところはこんなところでしょうか。以上の"ポインタの難解さ"を巡る話は、結局、ポインタ型変数を扱い、解釈する際のコンパイラの挙動が多岐にわたっているということの帰結であるように思われます。つまり、理解が難しいのはポインタの概念ではなくて、ポインタ型変数の処理法である、と。これに、C言語の関数は値を一つしか返せないので、その制限を誤摩化すために入力(引数)変数に対して出力を返せるようにするためにアドレスを使う、とかいう話になると、何それと思う人がいても無理はありません。これはもともとポインタの概念の話ではないと思いますが、アドレスを使うので関係はあります。こういう"制限のかいくぐり"が初めから必要になるような文法規則もどうかと思いますが、ポインタ(アドレス)による値の参照がまさに規則の抜け穴を作り出す局面で大いに役立てられているのは、C言語系の特色といえなくもありません。

たしかに、ポインタというのは、いわばメタデータ(データに関するデータ)であって抽象的なものであるため、これを用いればかなり複雑で柔軟なプログラムを組むことができるようになります。その点で、ポインタの取り扱いに習熟することは、より一段高度なプログラミングに移行するための登竜門である、と考えられていたふしも、なくはありません。でもポインタだろうがなんだろうが、高度なプログラミングは難しいのです。ポインタの概念が特に難解というのには同意できませんね、私は。まあ、何かあれば簡単に気は変わりますけど(^^;;)。

ヨタ話です。何となく漂っているポインタに対する嫌悪感というのは、コンピュータ科学の領域では、メモリーの管理というのは本来のプログラミングの内容であるべきではない、という固い信念があることから来ているようにも感じます。ポインタというのは、結局、メモリーを操作することに帰着しますからね。だから、避けたい、と。だから、ポインタが嫌がられているのは好都合、白い猫でも....(知らないか、このたとえ(トシがバレる))....でも、そういうもんですかね。自分がこれから処理しようとしているデータをどこかに置いたとかいうの、意識しちゃいけないんですかね。要らなくなったものは、そこら辺にポイ捨てしておけば、清掃員が片付けてくれるからいい、という感じですか。自分のケツぐらい自分で拭く自由があってもいいかなと。まあ、コンピュータを忘れられるというのが、高級言語の神髄らしいですが。すみません、これは、ここだけの話ということで。別に何かを攻撃しようという意図はありません、決して。特に、上でやや斜めから取り扱ったガベージコレクションなんかも、Mopsに限っていえばその動的オブジェクト生成のクラスデザインとかテンポラリオブジェクトの機構のせいで、なくても困らない気がしますが、Forth規格のヒープ領域確保のワードセット(オプション)を見ると、あると便利な感じですね。ヒープポインタをスタックに落とすとなると、ねえ(多分、まず大域変数に格納するでしょうが。)。

ともかく、難解なポインタ概念という表題でもって、ポインタ型変数を持つ特定のプログラミング言語の仕様の複雑さを、縷々解説されてきたことに、私達は気付くべきです。それは、Mops/Forthのような"非標準な"言語に多少触れれば、すとんと腑に落ちることがらであるのに。





最終更新:2019年07月04日 23:43