【素朴な疑問・相談室】
(9)

プログラマはなぜポインタが理解できないのだろう

本掲示は、企業内掲示板 VOX に掲載されたものの転載(一部修正あり)です。

お断り:本欄は、悩める相談者(筆者)が提起する“素朴な疑問”に対して企業内掲示板の読者の方々が解答してくれるという新しい形の相談室の記録です。
 私が抱いた疑問に対し、読者から寄せられた掲示による解答あるいは私宛ての私信による解答などで記録は構成されています。それらを公開するに当たっては、もちろん(企業内掲示の範囲で)公開を可とする承諾を解答者各人から得ました。しかしそれを、この一般向けホームページ上で公開するにはやはり問題があります。そこで解答者の意見は「要旨」とするにとどめました。解答者の生の声をそのまま紹介できないことは残念なことですが、ご了解ください。

 登場人物:


【悩める相談者】木下 恂

【相談にのってくれた解答者諸氏】
 Oさん  Sさん  Saさん
 Oiさん Kさん


【相談者の悩み】

 「プログラマはなぜポインタが理解できないのだろう」


【相談にのってくれた解答者の解答】

 Oさん  [解答]

 Sさん  [解答]

 Saさん [解答]

 Oiさん [解答]

 Kさん  [解答]

===========================================================
【悩める相談者】ポインタが分からない新人が、この欄を読んで「あぁ、そういうことなんだ」と合点してくれたとしたら、とてもうれしいのですが。

============================================================
【悩める相談者による追記】
 「素朴な疑問」シリーズは、これでおしまいです。一番最初の投稿の時に書きましたが、去年(1996年)の5月連休中に8個ばかりの“素朴な疑問”を作りました。それを1年間かけて少しずつ投稿してきた訳ですが、もう手持ちのものがなくなりました。あれ以後、新たな「素朴な疑問」は2件しか思い浮かばなかったことになります。これは、もはや私の頭には新たな発想の疑問が何も浮かばない程老化現象が起っているということではないでしょうか。遂に私にも惚けが始まったのではないかという“素朴な疑問”が湧いてきました。あぁ、今夜は眠れそうにない。

(了)

(終り)

============================================================
【お断り】解答文は、読みやすいように加筆あるいは一部省略してあります。したがって文責は私にあります。
============================================================




















 


【素朴な疑問(9)】
 プログラマはなぜポインタが理解できないのだろう

 新人プログラマは、なぜかポインタというものがなかなか理解できない。C言語などを習い始めたプログラマが、最初にぶつかる壁はポインタである。配列の登場とともに「ポインタ」という言葉が頻繁に使われるようになると、もういけない。頭が混乱してきて、すっかり自信をなくしてしまう。これは、教えている先輩の側にも責任がある。理解の進まない後輩を慰める積りなのか、教えながら「ポインタは難しいから‥‥」などと口走ったりする。これでは、教わる側も最初から「ポインタとは難しいものである」という先入観をもってしまうことになる。その結果、ポインタが分からなくなると、それは当然のことであるとしてそれ以上理解を深めようと努力をしなくなるのであろう。

 しかしアセンブラ言語で番地操作を常日頃からやってきたものにとっては、ポインタなど何ほどのことはない。アドレスのことをちょっと気取って呼ぶとポインタになるのだ、という程度の理解で済んでしまう(正確には違うのだが)。だから最初にアセンブラ言語から入って、次にコンパイラ言語でポインタに出会った人にとっては何の苦もなく理解できることなのである。

 私が思うに、ポインタが理解できないのは、ポインタというものを直接自分の目で見たことがないからではなかろうか。アセンブラあるいは直接機械語で命令語を扱ってきた者にとっては、アドレスなどというものは直接自分の目で見たことがあるかのように感じられる存在なのである。彼らの頭の中には、メモリのアドレスがありありと思い描けるようになっている。そら、そこにメモリという名の器(うつわ)が整然とならんでいるのが見えるでしょうが。

 しかし不思議なことに、ポインタが理解できないプログラマでも、整数データとか実数データなら容易に理解できるものらしい。“1”とか“1.3”とかいう数字なら今まで慣れ親しんできたものだし、自分の目で見て確かめたことがあるから直ぐに理解できるのだという。

 しかし、これは本当だろうか。彼らは、整数や実数を見たことがあると錯覚しているだけではなかろうか。メモリという器に格納されている整数型のデータの実体など、誰も確かめたことがないというのが本当のところであろう。整数データの実体を想像するのは、私にとっては、アドレス(番地)の付いた器を想像するよりもはるかに難しいことのように思えるのである。つまり、整数も実数もポインタも、誰もその実体など見たことがないのである。

 プログラマはなぜポインタが理解できないのだろう。あぁ、今夜も眠れそうにない。

(終り)

◆私信によるご教示も歓迎します(匿名可)。その場合、掲示板への転載の可否と実名/匿名の別を記入願います(省略時解釈:実名・公表可)。


[Return]




















 
=【解答者(Oさん)】==============================私信===

(略)

やはり、説明不足が原因ではないでしょうか。

| 計算機は一次元の一つの長〜い長い一個の配列のような
| メモリしか使えない。
| 上からは、mallocの様な一時記憶が、下からは関数内でしか
| 有効でないスタック用記憶が使われてくるんですよ。

という風に教える。で、32ビット単位の長い一次元の表を書いて、

| 関数呼ぶときは、ここの番地を、値渡しのときは、ここを
| このスタックにコピーして
| ここの番地をこうして、関数で値を参照するときは.....

などとする。私は、実際にデバッガで、0x31240みたいな変数の
アドレスとかを表示させて、中身を確認させたりしました。

# 徹底的に演習。

2次元配列と1次元のポインタ配列の違いも図解するとよく
理解してもらえるような気がします。

--- 蛇足になりますが... (具体的な例とともに私見です)

ポインタ変数は、難しいのではなくて、不具合の元だと
考えられませんか? (略) 私は、C言語の特徴ともいえる
ポインタ変数は極力使わないようにしています。
ループの中で、
  *(q++)= *(p++); ではなく、
  q[i]= p[i]; i++; のように書く。

リストも便利なのですが、あえて配列にして、
  p= p->next; ではなく、
  i= p[i].next; のように...

性能的には配列を使う方が遅くなりそうですが。

---

私の場合は、学生時代は、Z80ワンボードで遊んでいました
ので、ポインタには何の苦労もありませんでしたが、
FORTRANが長かったため、文字処理がなかなか理解でき
ませんでした。 処理系によって結果が変わるとは思いますが、
  char cerr[1024],cbuf[80];
  sprintf(cbuf,"%lf",1.e256);
  printf("cbuf %s cerr %s \n",cbuf,cerr);
| なぜ、cbuf は80文字しか宣言してないのに、メモリを壊す
| んだろう???? なぜ、80文字以上印刷するんだろう??

 Cでは、sprintf に「ポインタ」しか渡してないから、
変数の領域が80だよなんていう属性が全く無いということ
について体が理解するまで1ヵ月以上かかったような気が致
します。

これはあくまで私の当時の感想であり、C言語自体は融通が
きくので現在では嫌いな点はありません。
(ANSI よりは K&R が好きですが...)

(補足)
2次元配列と1次元のポインタ配列の違い
int array[32][80]; と
int *array[32],i; for(i= 0;i<32;i++) array[i]= malloc(80*sizeof(int));

以上、失礼いたしました。

=============================================================
【悩める相談者】なるほどねぇ。真っ正面から取っ組み合いをする訳ですね。それでも彼らには「一次元の一つの長〜い長い一個の配列のような」という説明とC言語の記述とがまったく合致しないようなんですよ。困ったことです。

[Return]






















 
=【解答者(Sさん)】==============================私信===

 いつも楽しく拝読致しております。
 今回もなかなか挑戦的な内容だったので、再び性懲りもなく(?)ご返事さし上げます。

(略)

 最近、ソフトウェア技術者はアセンブラ言語を全くやらなくなりました。これは由々しき事態であると思います。いやしくもソフトウェア技術者を自認する者、アセンブラは必須の素養であると断言できます。もちろん、今時アセンブラでソフトを組むなどということは殆ど有り得ないわけですが、いやそれだからこそアセンブラはシッカリ勉強しておかなくてはなりません。

 流行っているからといって、何もIntel系のスッチャカメッチャカ・アーキテクチャの石を勉強するには及びません。68000系のきれいなアーキテクチャでしかるべきソフトを書いてみれば、それで十分でしょう(68000は美しすぎてもの足りないという声があることは重々承知の上)。

 アセンブラが必須科目であることは、これを勉強することで、ソフトから見たハードウェア・アーキテクチャ(論理アーキテクチャ)を知ることができるためで、またその唯一の方法だからです。自分の書いたソフトウェアが最終的にはどういう風に計算機の上で実行されるのか、計算機の上での「処理」というのがどんなことをするのか(例えば、アドレス計算などというものが何なのか、どんな意味をもっているのか、どんなに重要か)、性能がどんなことがポイントになるのか、性能向上をするためにどんなことが問題になるのかを、実際に目で確かめることになるのです。もっとシッカリ勉強するとハードウェアモデルまで理解できるようになります。例えばメモリなら特殊なレジスタでいろいろなアドレッシングができることが重要であるとか、I/Oはこれも特殊なレジスタで特に割り込み源であることが重要でありI/Oプログラムは時間制約(タイミングと順序)が肝であること、などなどを理解できるようになります。

 しかし、どうしてアセンブラやらないんでしょうかねぇ。

(略)

 ポインタなどが理解できないのは、イメージの問題なのだと思います。数多く接していると何となく分かった気になる、つまりイメージが湧くようになります。誰も数概念をとことん突き詰めて考えた上で数を扱っているわけではなく、何度も何度もつかっているうちに数のイメージが出来上がり、何か実態があるような錯覚を持つんだと思います。これに似た例では、エネルギーなんていうのもその一つでしょう。物理学上の概念であって、これをチャンと理解することはすごく難しいと思うのですが(そういう自分だって良く分からない)、なに、普段良く使っているものだから何となく分かったつもりになっているんです。
 ポインタは悲しいかな、そんなに数多く登場する代物ではないので、なかなかイメージがつかめない。分かったつもりになれないんじゃあないでしょうか。かわいそうな「ぽん太」。筆者注:“ポン太”については「素歩人徒然」欄参照

 だから、ポインタを分からせるにはいやになるくらい沢山、ポインタを使った「良い」(これが肝心)プログラムを読ませれば良い。昔はそうやって勉強したものでした。この方法は今でも有効じゃあないかしら。

(略)

 さて、本論から離れますが、もう一つ問題があります。それは「ポインタ」は罪悪か否かという議論です。以前「ぽん太」の話題の中で議論されていたかと思いますが、私は「ポインタは罪悪である」という意見です。少なくとも私はよほどのことが無い限り使わない。配列でやることにしています。これで不自由したことが殆ど無い。たまに使わなくてはならない羽目になると大抵デバッグにえらい苦労をします。まあ、私の様な素人に毛が生えた程度のプログラマは使わない方が身のためというか、プロなら自分の技量をわきまえて使え、と言ったところでしょうか。恐らく「一流」のプログラマ以外使うべきではないと信じます。

 確かに、上手く使えば本当にきれいなプログラムが書けるのは事実で、しかし、一方どうしようもないバグを作り込むのもいとも簡単で、だからシニア・プログラマは若い経験の浅いプログラマに「分かって使え」と口を酸っぱくして説教をするわけです。しかしながら、若い血の気の多いプログラマはこういった便利な道具を示されると、使わなければ沽券に関わると思うので、どうでもいいところでも使ってみるわけです。特に本などでポインタを使わないと性能が出ない、などと書いているのを見ると、配列なんてと軽蔑するようになるわけです。

 私は、ポインタなど使う前にもっとやるべきことが沢山あるはず、良い設計のためにはもっともっと頭を使わなければならないことがあるんだ、ということをしっかり叩き込むことが重要であると考えています。設計がシッカリできるようになればポインタなど知らなくたって、良いプログラムは書けるんです。
 だから、ポインタなど教えなくたっていっこうに構わない。いや、教えない方が良いくらいで、ポインタを本当に教えたくなったら、アセンブラを習わせた方が遥かに良い結果になると信じております。

 憎むべきは「ぽん太」なり。だが、しかし、嗚呼、どこまでいってもかわいそうな「ぽん太」。

(略)

============================================================
【悩める相談者】私のまわりには、アセンブラを用いている人がまだ大勢いるんです。でも、そういう人は大丈夫だろうということですね。
 一方、アセンブラ未経験の人がポインタに対してどういうイメージを構築しているのか、それが問題です。とんでもない誤解をしていることがよくあるのです。
 ポインタ属性は使うべきでないという意見の人は確かに相当多いようですね。

[Return]






















 
=【解答者(Saさん)】============================私信===

(略)

いつも、私にとって未知のお話を大変楽しみにしております。(略)

今回のポインタの件は、計算機の素人の私には、さっぱりわかりませんが、理解と言う言葉に大変興味を持ちました。
『つまり、整数も実数もポインタも、誰もその実体など見たことがないのである。』

このくだりは、色々考えさせてくれます。実態を見ると言う事は、実は、見る事(ある時間で止まっていて視神経が分掌している)とその時間積分(養老さんの本だと聴覚に関係ある運動?神経だそうですが)即ち動きがイメージ出来ることの様です。

するとポインタなるものを例えば → の様な記号で表して、それが動く様(さま)をそれこそ見せれば、理解となる様な気がします。
どうも人間は、視と聴(運動、積分する系)を動員して初めて理解した事になるそうですから。

でも実態は、何もわかっていないのでしょうが。
いったいわかるとは、何なのでしょうか。わからなくなってしまいました。
(略)

=============================================================
【悩める相談者】そうなんです。どうもC/C++系の言語はポインタ属性が表面に出てこないので「それが動く様(さま)を見せ」られない、という欠点があるような気がします。

[Return]






















 
=【解答者(Oiさん)】============================私信===

 ポインタについて

確かに、アセンブラ言語を経験した人間は、ポインタを見たことがある方が多いと思います。それは、別に幽霊ではなく、まさに、実体なのです。どの様にして出会うかというと、ダンプという形態が一番多いと思います。アセンブラ言語でソフトウェアを組む人間は、そのデバッグでマシンの前に張り付くようなことはあまりしません。不具合があると、ダンプ採取して、さっさと自分の席に戻ってダンプリストにポストイットを貼ったり、何色ものマーカーでカラフルにしながら、原因を追求します。その過程で、ポインタやデータ構造体の実体に遭遇することになります。ポインタを見つけると、その示す内容のアドレスにデータがあることを体で覚える事になります。ポインタは体で覚えるので、確かにポインタ恐怖症にはならないと思います。蛇足ながら、このカラフルなダンプリストは、後生大切に保管され、次のダンプ解析でも引っぱり出され、重要な役目を果たすのが普通です。ダンプリストには、「宝が埋まっている」と名言を吐いた私の友人がおります。

しかし、この頃は、アセンブラ言語で記述することは、しっかり無くなってしまいました。OSもシステム記述言語である「C言語」なるもので記述されるケースが多いではありませんか。これでは、ダンプ解析することは、不可能です。また、最近は高級言語の開発支援をするためのシンボリックデバッガが充実しているのも、ダンプ解析することがなくなった理由です。シンボリックデバッガでは、ポインタの実体に迫ることは、新入社員には、困難かもしれません。

一方、実数について知るために、ダンプリスト解析するのはお勧めできません。浮動小数点の表現方法を知っていても、ダンプリストを見て、実数に変換することは至難のわざです。頭がおかしくなる前に、止めることをお勧めします。実数については、シンボリックデバッガーで気楽に見ましょう。

話は、全くあらぬ方向に飛びます。考えて見れば、
命令語のための、 CODE  SEGMENT
データのための、 DATA  SEGMENT
レジスタのための、STACK SEGMENT
が、あるのに、ポインタはPOINTER SEGMENTを作って貰えず、反逆に出て、自分自身の理解を困難にするように振る舞っているのかもしれません。今度、ポン太さんと、じっくり、話し合ってみる必要があるかも知れません。

==========================================================
【悩める相談者】そうですか。私の時代には(なぞと言うと年寄りくさくなりますが)、デバッグにダンプリストを使うというのは最も下手なやり方で、最も避けたい方法でありました。どうにもならない最後の手段として使ったものです。なぜなら、膨大な量のプリントアウトを必要とするからです。ダンプリストでは、ゼロ“0”が一番多く使われるので、ラインプリンタのゼロの文字が摩耗して潰れてしまう程でした。普通はOSの開発者はダンプリストを愛用していたようですね。ラインプリンタのゼロを摩耗させたのは、主に彼らの責任だと私はかねがね思っておりました。

[Return]






















 
=【解答者(Kさん)】==============================投稿===

re:ポインタがなぜ理解できないのだろう

4月まで、C言語を教える立場にいた経験をもとにして、思いつくことを列挙しました。

1)1個の変数が、2箇所(以上)のメモリ領域に関わっている。
  このため、労力が2倍(以上)必要である。
 例1.1)pint と *pint は別のもの。

2)ポインタ変数が実際に何を指しているのか、常に意識していな
  ければならない。 どこで、何が、代入されたかによって結果
  が全く異なってしまう。ポインタ変数はいくつでも宣言でき、
  (インデックスレジスタより数が多いので)ストレスが溜まる。

3)ポインタの宣言がわかりにくい。
 例3.1)char * str = "abc"; は、str の宣言と初期化を一
    度に記述しているが、*str の初期化と勘違いしやすい。
 例3.2)int (*pai)[10]; と int *api[10]; の意味と()の
    付け方は、日本語の語順では理解しづらい(K&R 5.12
    参照)
 例3.3)int main( int argc, char *argv[] );のように、ポイン
    タを配列の形で記述できるため両者が混同され、違いを正
    しく理解できなくなる。

4)C言語では演算子を組合せて左辺値(Lvalue)をつくる事がで
  きる。他の高級言語より柔軟であるが、わかりにくい。
 例4.1)*(ia + i) = 5;

5)ポインタ変数と整数との加減算の意味が、通常の加減算と全く
  異なることを、十分に説明していない。
 例5.1)int *pi; に対して、*(pi ++) は pi の値を4増やす。

6)ポインタが指す構造体のメンバの参照が、わかりにくい。
 例6.1)(*pstruct).member と pstruct->member は同じであ
    るが、*pstruct.member は別の意味である。

7)C言語の文法において、ポインタ専用の演算子が割当られなか
  ったため、変数がポインタであるかどうか常に意識していなけ
  ればならず、労力が余分にかかる。

8)何度もコンパイラに怒られたり異常終了したので、ポインタを
  見たくなくなった。

=============================================================
【悩める相談者】C言語を教育する立場の専門家の意見として、これは貴重ですね。よくまとまっていると思います。

[Return]