わさっきhb

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

関数呼び出しで4択

いきなりですが問題です.____に当てはまるものを選びましょう.

関数呼び出しにおいて,実引数と仮引数が同じ変数名であっても,それらは別物とみなされる.なぜなら,____からである.
① 有効範囲が異なる
② 仮引数は変数でなくてもよい
③ 関数形式マクロで定義しても適切に動作する必要がある
④ externをつけていない

試験問題を印刷する日の朝のことです.上のように,問題文を作ってから,見直しました.
正解はというと,「① 有効範囲が異なる」です.消去法で解けます.仮引数は変数でないといけません.関数形式マクロにおいて実引数・仮引数という名称はそぐいません.そして,仮引数にexternをつけるわけにもいきません.
なのですが,「有効範囲が異なる」というのも,キズがあるなと思うようになりました.
というのは再帰です.試験では,他の大問で,以下のコードを日本語で説明する出題を入れました.

int gcd(int n, int m)
{
  int r = n % m;
 
  if (r == 0) {
    return m;
  } else {
    return gcd(m, r);
  }
}

解答としては,最大公約数を求める関数gcdを定義していること,再帰呼び出しを使用していることの2点を押さえ,あとは引数と戻り値のことを書いてあれば正解です.*1
この出題と,冒頭の4択問題の「有効範囲が異なる」を,照らし合わせてみましょう.
変数mに着目します.この関数の第2引数のmと,2番目のreturn文の直後で,再帰呼び出しをしているgcd(m, r)の中のmは,同じ変数です.有効範囲も同じです.仮引数としての宣言から,関数定義の終わりまでです.
ですがこの2つの変数を,再帰呼び出しという観点で見ると,違いが発見できます.
例えば,この関数の外から,gcd(24, 18)と呼び出したとき,変数nとmのための領域がメモリ上で確保され,それぞれ24と18が代入されます.ローカル変数rは6で初期化され,これは0でありませんので,elseの中のgcd(18, 6)を再帰的に呼び出します.
そこで新たに,変数nとmのための領域が確保され,18と6が代入されます.
そうしたとき,1回目の呼び出しで,18が格納されるmのメモリ領域と,2回目の呼び出しで,6が格納されるmのメモリ領域は,異なります.それぞれの変数が使えなくなるタイミングも,異なります---2回目の,6の入ったmが,先に使えなくなります.
この違いを,「有効範囲」と別の四字熟語で表すなら,「生存期間」となります.授業では,自動記憶域期間と静的記憶域期間を解説していまして*2,仮引数は必ず,自動記憶域期間です.有効範囲の最初に,メモリ上で確保され,その範囲を終えると,使えなくなります.
何が同じで何が違うかを整理すると,こうなります.すなわち,gcd関数1回分の処理として見たとき,変数mは,有効範囲が同じであり,同じ物(オブジェクト)です.
しかし,再帰呼び出しをする際の実引数のmと,呼び出された際の仮引数のmはというと,メモリ上でも異なる場所に「変数mのための領域」がつくられるので,別物(別オブジェクト)となります.別物となるのは,それらの変数mに,異なる値が格納されることからも確認ができます.「仮引数と実引数は,有効範囲が異なる」と書いてみると,これは再帰呼び出しをする際にも,成立しています.
なのですが,問題文に立ち返りまして,「有効範囲が異なる」の選択肢は「生存期間が異なる」に置き換えればいいのか,「有効範囲や生存期間が異なる」にしたらいいか…
いろいろ考えまして,まったく別の問題を作りました.以下の出題を,試験で問いました.

int dosomething(void);と宣言したら,____と書いて呼び出すことができる.
① dosomething(1 + 2) + 3
② int a = dosomething();
③ int a = dosomething(void);
④ dosomething(*)

形の上では関数プロトタイプの話ですが,むしろ,仮引数にvoidと書いたとき,呼び出す際には引数なしになるのが肝心なところです.4つの選択肢の中で,唯一,引数を書いていない「② int a = dosomething();」が正解というのは,多数決*3対策を兼ねています.
授業では,ライブラリ関数のgetcharを使用したコードを見せた際,「getchar()のように,カッコとカッコ閉じが連続する書き方に,慣れてくださいね」と説明していました.試験では,ほどほどの正答率になりました.


関連:

議論も終わったようですし,そもそもSlashdotでコメントを書く性分ではない*4のですが,授業では再帰にそれなりの力を入れて解説してきました.
思い浮かぶ理由を挙げると,上のとおり,「有効範囲と生存期間が異なる」例となること,それと関連しますが,変数がメモリ上にどのように確保され使えなくなるかが視覚化しやすいこと,それから,関数の連鎖的な呼び出しの一形態であること*5,そして,ツリーの探索や自己参照構造体など,再帰的な対象を理解するための足がかりとなること,などです.
「反復で書けばいいことを,再帰で書く必要はない」という主張は,ごもっともです.それを確認するには,試験の1問だけを見てケチをつけるのではなく,授業で何を学習しているかの把握が不可欠です.Computer Science Teacher: Recursion – Love it or hate it?の画像のコードについては,printNumber(n-1)とSystem.out.println(n)を逆にするとどんな出力になり,なぜ変わるかの説明ができてやっと,再帰の動作を理解したと言っていいように思っています.

*1:nとmはintですが,呼び出しにおいては正の値でないといけません.宣言よりも,引数の取り得る範囲が狭くなることについては,授業の内容を超えていますので,模範解答には「正の整数」と書き,解説で「ただし関数定義の上あたりに,引数はいずれも正でなければならないことを,コメントとして書くべきであろう.」というエクスキュースを入れました.

*2:字数は多いし,「きおくいききかん」は言いにくいので,「auto変数」「static変数」も使用しています.

*3:多数決の法則 | 電験3種合格の裏技

*4:読んではいます."All your base are comprised of us."(http://it.slashdot.jp/comments.pl?sid=651434&cid=2757373)には,画面の前で大笑いしました.

*5:関数呼び出しの関係を有向グラフで表したとき,再帰呼び出しが自己ループになるのも,興味深いところです.