わさっきhb

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

値渡しと参照渡しの違い(3)

(3)の続きです.すみません.
(2)の続きです.

コードで違いを確認しよう

先生「値渡しと参照渡しの違いを示すには,あとは…」
学生「具体的なプログラム,ですか?」
「必要ですねえ」
「覚えてきました」
「授業でやった,二つの値を交換する関数,ですか?」
「はい」
「まあ覚えたのを書き出すのでは,能がないので…」
「…」
「今から言う関数を,書いてくれますか?」
「え,あ,はい」
「関数の中で,引数の値を1,増やしてください」
「えっと,はい」
「それで,増やした値を,printfで出力してください」
「…それだけですか?」
「うん,そういう関数を書いてください.まずは値渡しから」

値渡しで1増やす

学生「えっと…引数の型は,どうしたらいいでしょう?」
先生「intにしてください」
「はい.では…これでいいでしょうか?」

void ichi(int a)
{
  a = a + 1;
  printf("%d\n", a);
}

「まあ,仕様は満たしますね.ですが」
「ですが?」
「コードとしてよろしくないところがあるので,修正しておきましょう.まずは関数名です」
「1増やす,で "1fuyasu" にしようとしたのですが」
「それはあかんね」
「最初が数字という名前はまずいので,"ichi" にしました」
「うーん,この関数は,離散数学の授業あたりで教わってないかな?」
「えっと…」
「引数の値を1増やす関数は,後者関数,英語でsuccessorというんですよ」
「じゃあsuccessorにすればいいのですか?」
「まあ,長いから,succのほうがいいね」
「ああ,そういえば,離散数学の授業では,S(x)という書き方をしていました」
「ふむ.…あ,そうだ,後で参照渡しと比較するので,関数名を書き分けておきましょう」
「…どうすればいいでしょうか」
「値渡しを英語で何というか,知ってますか?」
「はい,もう一つのプログラミングの授業で教わりました…call by valueです」
「うん.まあ,pass by valueという言い方もあるけどね.これを使って,succ_by_valueにしますか」
「はい」
「あともう一つ."a = a + 1;" は,Cらしくないね」
「インクリメントを使うほうがいいんですか?」
「そうだよ」
「あれ,どうも好きになれないんです」
「なんで?」
「a++と++aで,意味が違うんですよね?」
「うんまあ授業でそう言うたけど…ここでは,a++;でも++a;でもいい.そういうときの慣例は,a++;のほうですね」
「iのforループで,i++と書きますけど」
「そう,そういうこと.改めて,関数を見直すか」
「はい」

void succ_by_value(int a)
{
  a++;
  printf("%d\n", a);
}

「ここから,何が言えますか?」
「何が言えるか,ですか…」
「これが値渡しの関数の例ですね」
「はい」
「なんで?」
「えっと…ポインタを使っていないから」
「消去法か.不十分かな」
「値を受け取るから」
「だいぶ近くなった.『int型の値を受け取って,その値を関数の中で使うことを意図した関数』と言えばいいんだけど,まあ長いか」
「長いですね」
「ともあれメモっといてください」
「あ,はい」
「ここで質問ですが,実引数がchar型とかdouble型とか,int以外だったら,どうなりますか?」
「…どうなるんですか?」
「授業では,関数のところで,ただし値渡しや参照渡しよりも前に,説明してるんだけど」
「忘れました」
「答えを言うと,実引数はcharでもdoubleでも,問題ありません.しかし関数を呼び出すときに,仮引数の型であるintに変換されて…暗黙の型変換ってやつね,それから仮引数aにコピーされます」
「ちょっと長かったので,メモ,とれませんでした.…実引数と仮引数の型が違っていたら,仮引数の型に変換してから,仮引数に代入する,ということですか?」
「僕が言ったのはかなりちがうけど,パラフレーズとしては,それでいいよ」
「ぱらふれーず??」
「言い換えのことです.オウム返しに,言われたことを答える,書かれていることを引き移すのではなく,自分の言葉で表現し直すことを,パラフレーズというんです」
「あ,はい,これもメモしておきます」
「うん,パラフレーズという言葉そのものは,レポートでは書かないようにね」
「もちろんです」
「あとは,値渡しの特徴は何でしたっけ?」
「仮引数の値を変えても,実引数には影響しないことです」
「そうだね.この関数から,なぜそれが言える?」
「えっと…」
「ここで押さえておくのは,仮引数はauto変数だということ」
「はい,auto変数ということは…」
「生成と消滅があるんですね」
「あ,そうです.関数呼び出しのときに,変数aの領域がスタックに作られて」
「そうそう」
「関数を終了するときに,消滅する,のですね?」
「そうです,そして?」
「消滅のときに,変数aの値は呼び出し元に戻りません.…戻らないので,『仮引数の値を変更しても,実引数には影響しない』」
「よし,これで値渡しは十分でしょう」

参照渡しで1増やす

先生「参照渡しの後者関数を書いてもらいますが」
学生「が?」
「コードを書く前に,どんな方針で書けばいいか,言ってもらいましょうか」
「はい…今書いたコードを使っていいですか?」
「いいですよ」
「えっと…関数名は,succ_by_…」
「参照渡しの英語が出てきませんか?」
「リファレンス,でしたっけ?」
「うん,call by referenceまたはpass by referenceですね」
「では,succ_by_referenceですか,あれ,途中のrは重ねるんでしたっけ?」
「動詞のreferだったら重ねますが,referenceは,重ねませんよ.なのですが,referenceって長くて打ち間違いやすいので,succ_by_refでいいかな」
「はい,わかりました」
「それで,関数はどないしましょ」
「先ほどの関数」
「succ_by_value?」
「そうです,その中のaを,*aに置き換えます」
「仮引数の宣言は?」
「あっと,はい,int aも,int *aにします」
「それでいい?」
「…何か足りませんか?」
「まあいいや,書いてみてください」
「はい」

void succ_by_ref(int *a)
{
  *a++;
  printf("%d\n", *a);
}

間接演算子とインクリメント演算子

「おかしいですね」
「あれ? …あ,*a++;のところですか?」
「そうです」
「カッコをつけて,(*a)++;なら,いいですか?」
「ならいい,じゃなくて,そうしないといけません」
「そういうものなんですか?」
「これは,理由を言えるようにしましょう」
「えっと…」
「カッコをつけない*a++と,カッコをつけた(*a)++との違いを言ってください」
「あ…」
「まず(*a)++から説明してみましょう.ヒントは,オペランド
「えっと,それでは…(*a)++の場合,インクリメントオペランドの」
「違うよ.インクリメント演算子だ」
「あ,はい.インクリメント演算子オペランドが(*a)」
「そこはカッコを言っても言わなくてもいいかな.ま,続けてください」
「つまり,ポインタ変数aの指し示す先の値で,それを1増やすことになります」
「それでいいのかな?」
「え!?」
「あ,質問の仕方が悪かったな.なので,意図どおりの動作なのですね?」
「え…はい,そうです」
「じゃ次は*a++」
「こちらは…ポインタ変数aに,オペランド*と++が両方ついていまして」
「うーん,見ためはそうだけど,演算子の優先順位がありますね」
「はい,それで,単項演算子はたしか,すべて同じ優先順位と授業で聞きました」
「まあ実のところ,すべての単項演算子が同じじゃないんだけどね」
「そうなんですか?」
「規格としては,後置のインクリメントとデクリメントの演算子だけ,優先順位が高いです」
「…」
「ということで,*a++は,*(a++)と同じと思ってください」
「あ,はい」
「でもこれで処理の説明は終わりませんので,終わりまで作ってくれますか」
「はい.*(a++)とみた場合…まずaの値を1増やしてから」
「違うよ,それだと,*(++a)となるから」
「そうですね,では…」
「ここで,授業で言ってないけど,ヒントを.演算子の優先順序は,式の構成を決めるものであり,式の中の評価順序を決めるものではないのです」
「…」
「後置のインクリメント演算子は」
「あ,式のあとで評価されるんですよね.だから,*(++a)は」
「おいおい,*(a++)だ」
「そうでした,*(a++)は,*aとa++の二つの式になります」
「それでは不十分だな」
「おかしいですか?」
「*(a++)という式の値,つまり評価値は,何になりますか?」
「*aを求めてから,a++しますから…」
「おかしいね.評価値としては,後置++の処理を抜かしてください.評価値と関係ないところで,1増やすのです」
「えっと…」
「理解しにくい?」
「いえ,ええ,なんかすっきりしないのです」
「一つの式には,一つの値が決まりますね」
「はい」
「それを評価値と言います.その一方で,一つの式の中に,複数の変数の値の変更ができますね」
「はい,++や--が入っていれば」
「それだけじゃなく,式の中に,複数の代入演算子を書いてもいい」
「あ,そうです」
「だから,*(a++)という式の中に…僕が言いすぎかな,続きを言ってくれる?」
「aの値を1増やすa++と,++を除去してできる式*aがあって,この*aが評価値になります」
「*aと書いたときのaは,1増やす前ですか? 後ですか?」
「後です」
「いや」
「あ,間違いです.1増やす前です」
「後で1増やす,ですからね.さて,*aって,何ですか?」
「aはポインタ変数ですから…aを指し示す先です」
「それで満足ですか?」
「…満足か,と問われると,何かおかしいですね」
「*a;という式文は,どう見ますか?」
「えっと,代入がないのですが」
「うん,*a++;から,後置の++を取り除いたんです」
「そこはわかるんですが,*a;の意味が…」
「意味を考えて戸惑うのは,当然ですね.これ,意味ないですから」
「はい…」
「aの指し示す先の値を取り出すだけで,その値は変わらないんです」
「…」
「あとは,『意図どおり』という言葉を使って,結論を言うと?」
「意図…あ,はい! *a++;では,『指し示す先の値を1増やす』という意図のとおりには,動作してくれません」
「はい,なので,*a++;ではなく(*a)++;ですね.また脱線が長くなったなあ」

改めて,参照渡しで1増やす関数を確認

先生「コードを見直しましょう」
学生「はい」

void succ_by_ref(int *a)
{
  (*a)++;
  printf("%d\n", *a);
}

「参照渡しの説明に即して,この関数を説明してください」
「はい.まず仮引数はポインタ変数となっています」
「そうですね」
「呼び出す側でも,intのポインタとしないといけません」
「intのポインタの具体例は?」
「intの配列や,配列のどこかを指し示すポインタ,それと配列ではなく普通のintの変数に,&をつけたものです」
「そうですね,レポートとして書くときは,面倒でも,intの後ろに『型』をつけてくださいね」
「わかりました」
「それと,最初の『intの配列』は,『intの配列変数名』かな」
「はい,そうです」
「さて…関数の内部は,どうなっていますか?」
「はい,値が変わるのは(*a)++;だけですが」
「そうですね」
「これは,aの指し示す先の値を,1増やします」
「aの指し示す先は,どこにありますか?」
「はい,関数の内部ではなく」
「関数の内部というのは厳密さを欠くんだけどね,まあ口での説明では,いいでしょう」
「関数の外,つまり呼び出す側の変数か,配列か,指し示す先の値を変更します」
「では最後に,この関数の処理を終えると,値はどうなりますか?」
「えっと,仮引数のaは使えなくなりますが,さっきの」
「先ほどの,ね」
「はい,先ほどの代入は有効ですので」
「ふむ,面白い表現だな.続けてください」
「関数呼び出しの前後で値が変わります」
「それが参照渡しの効果ですね?」
「はい」
「関数の説明としてはそれでいいですね.なのですが,一つだけ,小さなところですが,押さえておきましょう」
「あ,え?」
「実引数がint以外の型のポインタだと,どうなりますか?」
「それは…呼び出すときに,エラーになります」
「まあこれは特殊な話かな.実行時ではなくコンパイル時に,型の不一致がわかりますし,エラーではなく警告になります」
「そうなんですか?」
コンパイル時,のほうはいいかな?」
「はい,関数の型がわかりますし,呼び出すときにも型が決まります」
「うん.引数の型ね.で,ポインタの型の不一致は,警告になるんですよ」
「エラーにできないんですか?」
コンパイラオプションでできるかな.まあ我々としては,警告はエラー扱いとして,行番号を見てデバッグしたほうがいいね」
「警告は出ますが,実行ファイルができて,実行もできるんですね?」
「そうです,さてこのsucc_by_refで,doubleのポインタを引数にしたら,どうなりますか?」
「intのポインタに,doubleのポインタを代入…できるんですか?」
「無理やりに型変換すると思ってください」
「doubleをintに,ですか?」
「いえ,doubleのポインタを,intのポインタに,変換します」
「変わります?」
「アドレスは変わりません.ですが,何のポインタかというのが変わります」
「ということは…」
「そんなときに,(*a)++;は,どうなりますか?」
「aの指し示す先を,intとして,今ある値を1増やします」
「でも,呼び出すときには,その指し示す先は,doubleですね」
「はい,それでどうなるものかと…」
「まあかなりのところ,処理系に依存しそうですね.一番自然な解釈は,(*a)++;でどんな処理をするかはそれでよくて,関数呼び出し後には,そのdoubleの値のメモリ内容が書き換わるわけで,どんな値になるかというと,intとdoubleの内部表現形式によるけど,double型として1増えるというのは起こりにくい,といったところかな」
「つまり,意図どおりに1増えないのですね」
「そうです.参照渡しで値を変えるときは,暗黙のポインタの型変換というわけにはいけない,そんなことをしようとすると不具合が起こる,と考えるといいでしょう」

おしまい

先生「ではレポートにして,改めてこちらまで持ってきてくれますか」
学生「わかりました.では明日お伺いします」
「了解.明日は部屋にいるようにします」