わさっきhb

大学(教育研究)とか ,親馬鹿とか,和歌山とか,とか,とか.

ポインタ,identical

これまでのアンケートを読ませてもらいましたが,ポインタが分かりにくかったという回答が少なからずありました.たしかに,配列とポインタの比較に時間を多くとってしまって,1個の変数を指し示すポインタについては簡単に済ませてしまったかなという感があります.そこでもう一度詳しく,解説したいと思います.ついでに,"identical" という単語も覚えてください.

まず前提となるコードを*1

int a;

こう書けば,int型の変数aを宣言します.続いて

int *pA;

これで,intのポインタの変数の宣言で,変数名はpAです.くれぐれも,「int型の変数*pA」と考えないように.「int *型の変数pA」という表現は,僕は好んで使いますが,正式なものではないことも,まあ知っておいてください.
さてですね.aはint型の値を一つ,保持します.一方,pAは,intのポインタとなる値…「アドレス」とも言いますね…これを一つ保持します.二つとも,mainかどこかの関数の中で宣言したものとすると,初期値は不明ですので,ここで値を設定しておきましょう.

a = 3;
pA = &a;

一つ目の文では,aに3を代入します.3は整数値で,まぎれもなくint型の値です.
次に「pA = &a;」ですが,これは,変数pAに,変数aのアドレスを代入します.これが,「変数pAに,aの指し示す先をセットする」ということです.

ここで,型と,代入できるかどうかを確認することにします.aはint型の変数であり,かつ,代入可能な左辺値になり得ます.次に,&aは,intのポインタ,つまりint *型です.しかしこれ自体は変数ではありません.x+2という値は作れても,そこに何かを代入できないのと同じです.すなわち,&aは代入可能な左辺値ではなく,言い換えると「&a = なんとか;」と書くことはできません.
それから,pAは…int *型の変数であり,かつ代入可能な左辺値になり得ます.「なり得ます」と言って,「なります」と言わないのは,式のどこに書かれるかによって,必ずしも左辺値にならないからです.
ここまでで出てきませんでしたが,ポインタを使う上で欠かせないものがありますね?
はい,式の中で,ポインタ変数などの左につける「*」です.間接参照演算子と呼ばれます.これをつけた「*pA」の型と,代入できるかどうかは,どうでしょうか.pAはint *型なので,*pAはint型となります.「*」を移動するだけで,非常に単純に,型名が出てくるのです.いや,そのように「*」をつけることで型が区別できるように,C言語の設計者は,ポインタ変数の宣言と,式の中での値の参照について,ルールを定めたのです.
*pAの代入可能性というのは,要は「*pA = 5;」のように書いていいかということです.これも文法上は可能です.「文法上は」という条件を付けるのは,実用上は,pAに適切な値,すなわちアドレスをあらかじめ代入しておかないといけないからです.コンパイラは,初期化されていないポインタを使って,参照先に代入しようとしても,エラーも警告も出しません.実行時にセグメンテーション違反で終了するとか,実行時エラーは出ないけど期待する動作はしないとかいうことになるかもしれません.ともあれ,文法的にも,また実用的には「あらかじめ適切な参照をしておく」ということにしておけば,*pAもまた代入可能な左辺値になり得るということです.

ここで,「pA = &a;」という代入をした状態での,「a」と「*pA」の違いについて,考えたいと思います.*pAは,変数aのアドレスとなるポインタが参照するものです.そして先の代入文があるので,これは変数aと等価と言えます.
細かいことですが,ここは「等価」とか「同一」とか,あるいは「同一視される(同一視できる)」と言います.「同じ」でも意味は通じますが,ニュアンスが変わってきます.
英語で「同じ」はsameです.「同一」と言ったら,何になると思いますか? ここでsameしか思い浮かばなかったら,語彙は広がりません.identicalという単語を知ってください.初めて知ったという人は,授業が終わってから,辞書でsameとidenticalを見比べてください.
プログラミングを離れて,sameとidentical,「同じ」と「同一」の違いを表す例を作ってみましょう.いつも僕は,この黒い安物のカバンに,ノートPCや教科書や配布資料を入れて講義室に来ますが,私があるときに,このカバンを,中身ごとなくしたとしましょう.置き引きに遭ったと考えるのでもかまいません.そして数日後に,これと同じ形,同じ色,同じキズの位置,そして中身もちゃんと入っているといった特徴を持つカバンを見つけたとしたら,まずは外観から「俺がなくしたんと同じ*2や!」と判断し,キズや中身など,詳細を確認してから,「これは俺の(カバン)や!」となります.ここに「同じ」が出てきまして,ここを「同一」とは言いにくいです,
「同一」の使われ方ですが…僕がこの授業を終えて部屋を出たあと,何らかの事情で行方不明になったとしましょう.そこで捜索していただく際,身体的特徴だけでなく,着用物や所持品の特徴も記されることになります.このカバンが特徴物となるのであれば,誰かがこのカバンの写真を見つけてきて,「これと同一のカバンを持っていると思われます」といった表現になります.ここは「同じ」でも意味は通じますが,「同一」のほうが適切でしょう.
「同じ」と「同一」の違いはここにあります.つまり「同じ」というときは,実体として同じとなるかもしれませんが,「同一」というときは,実体として同じにならないことが,そこに込められているのです.「実体として」というのは,「物理的に」のほうが分かりやすいですかね.
さらに脱線しますが,僕がこのidenticalという単語を身にしみて理解したのは,麻雀です.昔むかしは「中国語の勉強」とも言われたものですけどね.学生時代に,興味があって,英語で書かれた麻雀の本を何冊か取り寄せたことがあります.その中の1冊に,"three identical tiles" という表現がありました.tileは麻雀牌のことです.identicalは機械的に「同一の」と訳しますと,結局これは「3つの同一の牌」で,刻子(コーツ)と呼ばれるものです.これも,物理的に異なるけれど,同じものが描かれているので,「同一」,そして英語では"identical"と表現するほうがよいということです.

プログラミングに戻りましょう.変数aと,pA=&aと設定したpAがあるわけですが,ここでメモリにどのように変数aとポインタ変数pAが格納されているか,そして,代入可能な左辺値としての「a」と「*pA」が何なのかを考えると,「同じ」と言っても問題はないけれど「同一」のほうがよりふさわしいなとなります.
というのは,「a」と「*pA」が同一でない状況が,簡単に作れるからです.今,「pA=&a」という代入を前提としてきましたけど,ポインタ変数のpAに別のアドレスを代入してしまったら,それ以降は,aと*pAは同一ではないのです.先ほどのカバンの例で言うと,「同一のカバン」を見つけたのはいいけれど,そこに犬がふらっとやってきて,そのカバンを食べ物と思って噛み噛みして,ぼろぼろにしてしまったら…「これと同一のカバンを持っていると思われます」として使うことはもはやできませんね.
さて,aと*pAが同一なのはいいけれど,その同一性をプログラムのどこで使うかが気になる人もいることでしょう.このコードのような代入文を書くことはほとんどありません.使いどころは,関数の引数の受け渡しです.関数の仮引数に「int *pA」といった書き方をして,実引数では,int型の変数aに対して「&a」と書くことで,ポインタ渡し,あるいは参照渡し*3と呼ばれるものになります.代入演算子は書かなくても,関数定義の仮引数,関数呼び出しの実引数の間で,代入がなされるわけです.
そうするとですね,関数の中では,変数pAが使用可能ですが,aは,グローバル変数にしていない限り,関数の中でアクセスすることができないのです.それぞれの変数にはスコープ(有効範囲)がありますから.変数aのスコープに着目すると,これは関数処理と重ならないのです.別の言い方をすると,変数aは,関数の外にあるのです.
んだけどそんな関数の中で,しかしaの中身を見たい,そして必要に応じて値を変えたい,というときに,ポインタ渡しにすれば,「aと*pAは同一」なのですから,中身を見ることもできますし,*pAが代入可能な左辺値になり得ることを利用して,代入すれば,関数呼び出し後の変数aの値は,呼び出し前の値と変わっているということになります.これによって,「関数の中で実引数の値を変更する」,すなわちCにおいて参照渡しと同じ効果が得られるわけです.
ここまでのことをまとめますと,ある型Tの変数aと,そのアドレスを格納するためのポインタ変数pAについて,

T a;
T *pA;

と書けば,

pA = &a;

という代入で,aと*pAが同一になります.すなわち,*pAに値を代入することができ,それはaの値にもなります.
しかしaと*pAは完全に一致するものではなく,たとえばpAの値を変更してしまうと,そのポインタ変数が保持するアドレス,すなわち参照先が変わることになります.あるいは,関数のポインタ渡しをすると,aとpAのスコープが異なることになります.通常,aは関数からアクセスできないけれど,代わりに*pAはアクセスできるので,これを使って参照や代入をすることで,外部にある変数aの値を知るだとか,値を書き換えるだとかができるということです.

*1:やさしいC 第2版,p.281より.

*2:「おんなじ」と発音します,関西人は.

*3:最近,K&Rとその日本語訳を読み直し,wikipedia:引数を見かけたりしまして,授業や学生指導で「参照渡し」を言うのに慎重になりつつあります.K&Rについて「参照渡し」という概念が出てくるのは,手元に本がないのでうろ覚えで書きますが,「FORTRANPascalで採用されている」といった,Cでの引数の渡し方と対比させるためなのです.