Hatena::ブログ(Diary)

偏見プログラマの語り!

2012-06-11

プログラム初心者にC言語のポインタを不本意ながら教える羽目になったなら、こう教えると良いよ

| 10:09

 僕がプログラミングに触れた当時は、プログラミングといえば「まず C 言語」でした。それから 10 年以上が経ちました。学校の授業や企業の研修では未だに C 言語を教えているところがあるようです。関数型プログラミング言語という波が来ている 2012 年にもなって未だに C 言語をやっているというのはまるで進歩が無く残念な気もしますが、比較的多くのプログラマに浸透している共通言語を最初に教えるというのは、一方では喜ばしい事だと解釈する事もできるのかもしれません*1。まぁとにかく、本意にせよ不本意にせよ現場で プログラム初心者に C 言語を教える羽目になった 人がたくさんいて、プログラム初心者なのに C 言語を学ばざるを得なくなった 若者がたくさんいるということです。

 C 言語を教えるときに避けて通れないのがポインタで、プログラム初心者が C 言語を学ぶときにやたらとつまずく人が多いのがポインタです。ポインタの理解ごときに若者の貴重な時間を大量に浪費するのはアホらしいので、こういう教えかたすると早く教えられるよ、というハナシを書こうと思います。本稿は「ポインタの解説」ではなく「C 言語のポインタの教え方」です。

1. そもそも "変数は箱"、という例え話がポインタの説明をややこしくしている

 変数という概念を、全く別のメタファーである "箱" に喩えて説明するのはよく使われる手法ですが、その喩えはポインタの説明をするときに破綻します。「箱にラベルを貼るのがポインタです」という "喩え話続行戦略" にはそもそも無理があるのです。どうしても喩えを持ち出したいのであれば、その喩えを一旦信じてもらって理解をレベルアップさせた後、その喩えがなぜ妥当であったかを先生と生徒が共有できる程度まで噛み砕いた説明を付け足して、理解を定着させる必要があります。これはハイレベルな説明テクニックです。喩え話をした後の責任が取れないのであれば、その喩えはゴミ同然でしょう。

2. 「ポインタを教える」ために変数を教える

 ご存知の通り、C 言語のポインタ変数の説明抜きには説明できません。また、メモリの "効率的な利用" という基本的な存在意義があります。従ってメモリ空間をイメージさせることが、変数ポインタの両方を理解させるための近道です。もちろんメモリ空間のイメージを抜きにして文法だけで理解させることも不可能ではありませんが、その場合は C 言語のポインタ変数の宣言周りの文法で間違いなくハマります。表面の文法だけを舐めて理解させようとするのは、かなりハイレベルな説明スキルが必要だということです。相手はプログラミング初心者です。屈強な相手です。この戦略は避けるべきでしょう。

3. 「変数を教える」ためにメモリ空間をイメージさせる

 メモリ空間をイメージさせるために、もはや文法の説明は不要です。C 言語の説明を文法から始めるのは、きっぱり諦めましょう。C 言語のポインタの理解には概念の理解が絶対です。概念の説明をするために文法から説明するのは、少なくとも "C 言語のポインタを説明する" というミッションにおいて遠回りなのです。その理由は上で説明した通りです。メモリ空間をイメージさせるために大切なことは「文法の説明をしないこと」です。文法の説明無しに、方眼紙のようなメモリ空間をイメージさせるのはさほど難しくないはずです。しかし方眼紙状のイメージだけではイメージとして完結しないので、さらにもう一歩引き下がってステップを踏むのが効果的です。

4. 「メモリ空間をイメージさせる」ためにバイトという単位を教える

 バイト、バイト、バイト、バイト!!!ここから全ての説明を始めるのが良いです。「バイトと言っても、アルバイトとは違いますよ」とか小寒いおやじギャグを添えたりして、まずバイトを説明するのです!

 バイトとは、0 〜 255 のいずれか 1 つの整数値を表現できる単位です*2

5. バイトの解釈によって色んな表現が可能であることを説明する

 f:id:kura-replace:20120609233945p:image:right例えば、あるバイト x が 3 である場合、それをそのまま 3 と解釈することもできるし、「x は 100 からのオフセット値である」とするなら 103 と解釈することもできます。「x は 'A' から並ぶアルファベットの文字である」とするなら 'D' と解釈することもできます。

 「x は○○スーパーに並ぶ缶コーヒーの在庫数である」とするなら在庫数 3 と解釈することができます。しかし缶コーヒーの在庫数が 0 個であるか 42 個であるか 300 個であるかを表現するバイトは存在しません。なぜなら 0 〜 255 では表現しきれないからです。そこで、0 〜 300 個の缶コーヒーの在庫数を表現できるようにするために、1 つのアイデアを与えます。

f:id:kura-replace:20120609233944p:image:right

新しいバイト y を使って、x との組み合わせで数値を表現するというアイデアです。y が 0 のとき、x の値はそのまま缶コーヒーの在庫数と解釈することにします。y が 1 のとき、x の値は 256 ~ 511 個の缶コーヒーだと解釈することにします。バイトが 2 つあれば 65536 種類の値を表現することができます。バカみたいに簡単なハナシですが、バイト数が多ければそれだけ多彩な表現ができることを理解させましょう。

 あるバイト x をどう解釈するかは、プログラマが指定します。C でプログラムを書くということは、そのバイトが何を意味しているかをコンピュータに教える作業をするという事でもあるのです。

6. メモリ空間をイメージさせる

 コンピュータは計算を大量にしないといけません。従って、y が 1 のときは x の解釈は… という具合に 1 バイトずつ考えてゆくのは、とんでもなく大変です。そこで、もう一つアイデアが必要になります。それは例えば「x から始まる 2 バイトを数値として解釈する」という具合に、名前を消してしまうアイデアです。これを貫徹するためには、その 2 バイトが並んでいる必要があります。場合によっては 4 バイト、8 バイト... 何にせよ並ぶ必要があります。そこで、バイトをずらっと並べたものが用意されているわけです。これがメモリ空間です。*3

f:id:kura-replace:20120610230943p:image

7. 変数を教える

 メモリ空間にバイトが並んでいる理由が説明できたなら、変数をイメージさせるのは簡単でしょう。

f:id:kura-replace:20120611081901p:image

 箱のイメージではなく、数バイトのメモリを指す名前として変数をイメージさせることができました。次にこのイメージを使ってプログラムを作ることを説明すると良いと思います。ここで、少しだけソースコードを書きます。4 バイトで表現された数値を扱うために int という名前が用意されていることも含めて、教えることができるはずです。

int a, b;
a = 258;
b = 5;
printf( "%d, %d", a, b );  // 258,5
if( a < 200 ) {
  a = a + b;
} else {
  b = a - b;
}
printf( "%d, %d", a, b );  // 258, 253

 ここまでくれば C でプログラムを書くということがどういうことかを、何となくイメージさせることができます。

8. ポインタを教える

 僕らは普段の会話で「あれが欲しい」「それを見たい」という表現をします。マウスカーソルを動かしてアイコンをクリックします。具体的な対象の代わりに、指し示す表現を使っているわけです。「指す」ことを、プログラムの中でも実現するのがポインタです。

f:id:kura-replace:20120611090324p:image

「z の場所がどこであるか」は、「メモリ空間の何番目であるか」で表現できますよね。ポインタなんぞ、この程度に簡単なものでしかないです*4。次にポインタの文法を説明します。

9. ポインタの文法を教える

 ポインタの文法を説明するときに大切なことは、変数の宣言時初期化構文を見せないことです。

int a, b;
int * p;
a = 100;
b = 200;
p = & a;
printf( "%d", * p ); // 100
* p = b;
printf( "%d", * p ); // 200

 & 演算子が a の位置を表現する事、* 演算子で p が指すメモリを表現する事を説明します。

10. 配列ポインタと絡めて教える

 C 言語をいかにして教えるか、という議論において、配列ポインタを絡めるとややこしくなるからダメという意見が根強いですが、それは "変数は箱" という喩えを使っているからです。C 言語を扱うときに配列ポインタは切っても切れないのが事実なので、関係が強いことをアピールすべきでしょう。メモリ空間のイメージがあれば関係の強さもまた容易に理解させることができます。

f:id:kura-replace:20120611093423p:image

 文法の説明なぞ、二の次で良いのです。

int arr[3];
int * p;
arr[0] = 260;
arr[1] = 259;
arr[2] = 8;
p = a;
printf( "%d%", p[1] ); // 259

11. NULL を教える

 ここまでの説明パスを経ることで、4 バイトのメモリを、メモリ上の位置を表現する数値だと解釈すること*5ポインタであることを理解させることができているはずです。次に、ポインタが「今はどこも指してないよ」を表現するために NULL という定数値があることを教えます。こういう説明順序を踏まえることで↓このコードが意味的におかしいことをすんなり理解させることができます。

int a;
a = NULL;

12. その先

 以上が、プログラム初心者に C 言語を教えざるを得ないときにポインタを教えるための説明パスです。この先は省略しますけども、メモリ空間のイメージを用いて説明しているので、以下のつまずき箇所を説明するときのハードルもぐっと下がります。

・2 重ポインタ、3 重ポインタ

・矩形配列とジャグ配列

関数引数に n 次元配列を渡すときのコンパイルエラー

・int * から double * への型キャストが不適切な理由

文字列配列で表現されること

関数ポインタ

そんなわけで、ポインタの説明(というか間違った理解を補正するための補足説明の嵐)に時間と手間をかけるのはもう辞めようぜ!というお話でした。

*1:それを意図しているかは別として。

*2:7bit や 9bit の処理系もあるとか、そういう厳格性を問う議論はここでは不要でしょう

*3:バイトが並ぶっていう日本語は妙ですが、まぁそこは汲み取ってください

*4ポインタポインタ変数は違う、という指摘が入りそうですがそれはごもっとも

*5:4 バイトとは限らないとかそういう話は今は置いておきましょう

ko1kunko1kun 2012/06/11 10:20 素晴らしいです。わかりやすいです。新人研修の参考にさせていただきます。
私自身は、最初にC言語を学んだときはポインタがよく理解できなかったのですが、その後でアセンブリ言語(8086や68000など)を学んだら、すごくスッキリわかりました。変数とメモリの関連が把握できると簡単なんですよね。

kura-replacekura-replace 2012/06/11 10:56 ありがとうございます。メモリのハナシ抜きにポインタ構文の説明して理解されない場面を何度も見てきたので、文章にしてみました。研修の一助になれれば嬉しいです。

h_easth_east 2012/06/11 11:24 10のコード部分のtypoです。
> p = a;
> printf( "%d%", (*p)[1] ); // 259
p = arr;
printf( "%d\n", p[1] ); // 259

説明わかり易いです。

kura-replacekura-replace 2012/06/11 11:36 うぉ、凡ミスですね...。ありがとうございます!

おとなり日記