読者です 読者をやめる 読者になる 読者になる

サークルでポインタを教えてきた

C++ メモ プログラミング

僕の所属しているサークルの新入生向けのC++講座(C++を教えるのが最適かという質問はなしの方向で)で先日ポインタを教えてきた。その時にどうやって教えたのかやどういう反応が返ってきたかをメモとして残しておく。
よくある「この教え方がいい」みたいなのはよく見かけるが、こういう教え方をしたらこういう反応が返ってきたというのはあまり見かけない。そういった意味で有意義であると思う。したがってこの教え方がいいと主張するつもりは毛頭なく、もっといい教え方を考えるときの参考にしてもらえればうれしい。
教えた結果はどんな感じだったかというと受講者5人中3人が「誤っているswap関数を正しいswap関数に修正する」という課題を迷うことなく実装することができた。そのうち1人の話を聞いたのだが、理解しているという感じを受けた。ということだ。

・資料作成と方針
講座の資料を作成する際まず、最初にどうやって僕がポインタの概念を理解したのかを考えてみた。C言語で最初触ったときわりとすんなり理解できた。どうやらそれはHSPの時点で近い概念を理解してたかららしいという結論に至る。というのもpeekやpokeという関数や命令を使いメモリをいじってた経験があるのでポインタを使ってそれをするのも方法が変わっただけで大して変わらないんじゃないかと。さて、そのpeekやpokeで変数の値をいじるとき昔悩んでいたことはなんだったのだろうか?それは「メモリが手に取って見えない」だったように思う。メモリに書き込むという発想は割と理解しやすかった気がするが本当に書き込まれているのか不安な感覚があったような気がする。きっとここで初心者がつまずくかもしれない。と思いメモリエディタで実際にメモリをのぞかせて変数の値をいじらせることを考えた。紙の上で「メモリ上に並んでます」と言われても多分実感がわかないがこの方法なら自分の手でいじっているのできっと実感だろう。メモリを書き換えさせるとさらに実感がわくかもしれない。ゲームの改造風にできたら面白そうだな...とか考えた。そうすると解析対象のプログラムを作らなくてはいけない。これが結構面倒だった。というのも最適化とかデバッグ情報の関係でうまく意図したようにはならないのだ。唯一の救いはサークルで統一的に教えているため、環境が全員同一であるのでコンパイラにべったりな実装をしても問題ないということだろう。ということでそこら辺を考えつつまず以下のようなプログラムを作成した。

//Sample1
#include<iostream>
#include<string>
  
int main(){
  
    int HP=4;
    int MP=2;
    //攻撃力
    int AP=1;
    //防御力
    int DP=5;
  
    //変数の前に「&」がつくことに注意!
    std::cout<<"変数HPのアドレス:"<< &HP <<std::endl;
    std::cout<<"変数MPのアドレス:"<< &MP <<std::endl;
    std::cout<<"変数MPのアドレスとHPのアドレスの差:int(4byte)"<< &MP-&HP <<"個分"<<std::endl;
    std::cout<<"変数APのアドレス:"<< &AP <<std::endl;
      
      
    std::cout<<"各値"<<std::endl;
    std::cout<<"HP:"<<HP <<std::endl;
    std::cout<<"MP:"<<MP <<std::endl;
    std::cout<<"AP:"<<AP <<std::endl;
    std::cout<<"DP:"<<DP <<std::endl;
      
  
    std::string input;
    //プログラムをとめるためのダミー
    std::cout<<"Please Input something."<<std::endl;
    //最適化を防ぐために必要
    std::cin >> DP;
      
  
    std::cout<<"HP:"<<HP <<std::endl;
    std::cout<<"MP:"<<MP <<std::endl;
    std::cout<<"AP:"<<AP <<std::endl;
    std::cout<<"DP:"<<DP <<std::endl;
  
    std::cout<<"Please Input something."<<std::endl;
    std::cin >> input;
    std::cout<<"HP:"<<HP <<std::endl;
    std::cout<<"MP:"<<MP <<std::endl;
    std::cout<<"AP:"<<AP <<std::endl;
    std::cout<<"DP:"<<DP <<std::endl;
  
    return 0;
}

サークルではVS2010を使っているのだが、デバッグビルドではデバッグコードと思われるものが入っていたのでリリースビルドしないと説明しづらい状況になる。
これに加えコンピュータ内部の仕組みも少し教えれば理解が深まるだろう。そんなわけでプログラムはHDD等からメモリに移されて実行されるので変数や関数...は全部実行時にはメモリにおかれているはずだからそれを見てみようという導入にした。あと、解析に使うメモリエディタだがメジャーなうさみみハリケーンを使うことに。
あと外部の全く関係ないソフトからいじれるのだから、アドレスを使えばスコープを超えて書き換えられそうだよねとかいう話も入れた。

さて、この後すぐにポインタを導入してもいいのだが、ついついポインタの宣言やらなんやらで「アドレスの操作」であるということに目がいかなくなる。僕がHSPで触った時もアドレス(変数の先頭から何バイトかということだが)に対して操作するという意識があったのでこれを強調したい。要するにアドレス演算子&はアドレスを取得したい変数に作用し、参照外し演算子*はアドレスに作用するということを意識してほしかった。ここでは以下のようなサンプルを考える。

//Sample2
#include<iostream>
   
int main(){
   
    int HP=4;
    int MP=2;
   
      
    int input;
    std::cout<<"1を入力"<<std::endl;
    std::cin>>input;
  
  
    //変数の前に「&」がつくことに注意!
    std::cout<<"変数HPのアドレス:"<< &HP <<std::endl;
    std::cout<<"変数MPのアドレス:"<< &MP <<std::endl;
    //ポインタは足し算が出来る
    std::cout<<"(変数HPのアドレス)+1=(MPのアドレス):"<< &HP+1 <<std::endl;
       
       
    std::cout<<"各値"<<std::endl;
    std::cout<<"HP:"<<HP <<std::endl;
    std::cout<<"MP:"<<MP <<std::endl;
  
  
    std::cout<<"*(変数HPのアドレス)=(HPの中身):"<< *(&HP) <<std::endl;
    std::cout<<"*(変数MPのアドレス)=(MPの中身):"<< *(&MP) <<std::endl;
 
    //(変数HPのアドレス)+1=(MPのアドレス)なので両辺の*をとれば
    //*((変数HPのアドレス)+1)=MP が成り立つ
    std::cout<<"*(変数HPのアドレス+1)=(MPの中身):"<< *(&HP+input) <<std::endl;
   
    //書き換え
 
    //(&HP)=(HPのアドレス) ここで両辺の*をとれば
    //*(&HP)=HP
    //つまりHPに対する代入と等価:
    *(&HP)=8;
    //同様にこれはMPに対する代入と等価
    *(&HP+input)=9;
 
    std::cout<<"書き換え後の各値"<<std::endl;
    std::cout<<"HP:"<<HP <<std::endl;
    std::cout<<"MP:"<<MP <<std::endl;
 
   
    return 0;
}

教え方としては&でアドレスが取れて*はその逆関数のようなものと教えることに。

この後にポインタを導入する。
アドレスを入れる変数というだけなのであとはすんなり理解できるだろうと思ったからだ。

以上のような感じで基本操作が
1.アドレスをとる
2.アドレスから変数に戻す
3.アドレスに対して計算する
4.アドレスを保存する
の4つだけということを感じてもらいあとはメモリの知識と文法に過ぎないということをわかってもらえればなと。あとは定石通りにswap関数等を例として示せばよいだろう。

・実際にやってみて
0.
今回の講座の導入として最初に今までのプログラムで出来なかったこと(スコープをまたいだ変数の書き換えができない、配列が固定長...)をあげた。それを解決するための方法として今回の内容があるとすればわかりやすいのではないかとおもったからだ。ただ口頭で説明したためイメージがつかめてなかった感が若干ある。サンプルを示したほうがよかったかもしれない。

1.
最初のメモリ解析の部分ではある程度意図したとおりになった気がする。が、問題があったのはサンプルの写し間違いが致命的であるということ。これは僕の方針として自分の手で写したほうが体で覚えられると思うので基本的にサンプルは全部入力してもらっているのだがここは文法というよりメモリをいじってメモリを感じてもらうのが目的だったのでコピペさせてもよかったかもしれない。やってもらったことはアドレスの場所に行って書き換える、左右に移動して目的の変数を見つける等のことをしてもらった。

2.
&と*の説明はこれで納得してもらったように思える。特に問題はなかった。

3.
前の部分でアドレスの説明をしたのでポインタにはアドレスが入っていて中身を展開すると前で説明したものと同一になるということを説明した。戦略としてはポインタの中身はアドレスで操作対象はあくまでアドレスに過ぎないということを理解してほしかったからだ。
 問題点としてポインタの説明は用語が説明する僕のほうでも用語を混乱してしまった感がある。普段アドレスとポインタを同義で使ってしまうことも多いのでそれをきっちり区別して、あるいはそのことを伝えて教えないといけないなと思った。また、クラスのポインタの(*hoge).func()という説明をしたのだが、そもそもクラスの理解が不十分(というより忘れていたみたい)なのでそういうところの配慮も必要かもしれない。ここは次回に回して本質を先にすべきだった。ここはいろいろ反省する点が多い...

4.
関数でのスコープを超えた書き換えについてはメモリエディタでいじっていたので説明する方としても非常に説明がしやすかった。ここでの課題は前述のようにサンプルとして出した誤ったswap関数を正しいswap関数に直すということだが、3/5がまよわずできていた。また、全員がswap関数にポインタを渡すという段階まで至っていた。できなかった2人の話を聞くと一人はポインタの宣言時の*と参照外しの*の使い分け、もう一人は変数から関数の引数に変わったことでアドレスと値の区別ができないことが原因のような気がした。

・まとめ
ポイントとしては最初からポインタを持ってこずにアドレスの操作から説明を始めたこと。メモリを実際に触らせたことだろうか。多くの本ではポインタとアドレス、参照はずし等が一緒に混ざってきて混乱する。また、メモリも図で説明されることもあるがそれだけではわかったようなわかっていないような気になる。そこを避けられたとは思う。
 なお残る問題点はポインタの宣言等々の文法のややこしさに関する部分だ。これはいい説明方法が思いつかないのでもしこういう風にしたらいいんじゃないかとか思った人は教えてください。