C言語超入門(?)第五回

今日も今日とて説明が多い。
このまま十回くらいまでで終われるのだろうか・・・?
何回まで続くのか、甚だ疑問である。

前回の復習と今回の概要

  1. 変数の宣言は、値を置く場所の確保。
  2. 変数への代入は、確保した場所に置く値を置き換えること。
  3. 変数の使用は、ただ名前を書けばいい。するとその変数が確保している場所の値に置き換わる。

前回説明した内容はこんなところ。
前回のまとめとずいぶん説明の語彙が違うと思うが、あまり気にしない方向で。


今回説明する内容は・・・前回の予告とずいぶん違うけれど。

  1. 条件式。端的に言えば、「正しい」か「正しくない」もの。YesかNoで答えられるもの。
  2. 条件分岐。条件式を書いて、その条件式が正しいときと正しくないときで、実際の命令を分ける。
  3. 複文。複数の命令を一つの命令として扱う。
  4. 再帰呼び出し。関数のなかで、「その」関数を呼ぶこと。
  5. 変数、引数、関数(それぞれの名前)の有効範囲(スコープ)。

・・・多分、これだけ説明したらその時点で前回の予告を無視することに。
だけど、これを全部終わらせたら今度こそ数字が画面に出せる!*1
さあ、がんばっていく、のは、私なんだよなあ・・・

条件式

条件式とは、YesかNoで答えられるもの。ドラクエの勇者でも答えられるもの*2
例えば、1+1=2は正しい*3。1+1=1は正しくない。
ちなみに、この二つの式をC言語で書くと怒られる。C言語では「=」は「代入」を表すものであって、「等しいこと(等号)」を表すものではないから。
1+1=2をC言語で解釈すると、「1+1に2を代入しろ」ということになる。意味がわからない。
C言語で正しく書くと、1+1==2、1+1==1と、「==」のようになる。


さて、それでは条件式でないものとはなんだろう?
これは例えば、3+4。言うまでもなく、答えは7であってYesやNoではない*4
ちなみに、Yesを1、Noを0とすると、条件式は「結果が1か0であるもの」ということになる*5


お、条件式の説明が終わったぞ、これならそんなに長くならないかも?
・・・次が長いだろうけど。

条件分岐

さて、上で挙げた条件式は「結果が常に同じ」条件式なので、あまり意味がない。
では、前回説明した変数や、その前の引数が関わってきたらどうなるだろうか?
例えば、変数xがあったとして、x > 0は正しいと断言できるだろうか?否、状況によるとしかいえない。
引数yがあったとしたら、y == 1は正しいのだろうか?今度はさっぱりわからない。
関数を呼ぶ側が実引数として何を与えたのかわからないのだから*6
というわけで、「この結果が正しければこうしたい」「正しくなければああしたい」というタイプの書き方ができないと、プログラムを書く側は困るわけである。
少なくとも、わざわざ関数というものを書くときに、面倒すぎる。
もちろん、これから先利用価値はどんどん上がる。


さて、簡単に条件分岐の書き方を説明。

if(条件式) 「正しいときの命令」 else 「正しくないときの命令」

ifとかelseとかは、C言語で決まった言葉。
ちなみに、intやcharみたいなのも同種で、このような「C言語で決められた言葉」を「予約語」という。
予約語は名前に使えない、とかいろいろ制限はある。
しかしまあ、あまり気にしなくてもいいし、まともな神経をしていれば名前になんて使わないだろう。
ちなみに、elseと「正しくないときの命令」は、書く必要がないならば書かなくてもいい。


条件分岐を使った例を挙げてみよう。

if(x == 0) x = 1; else x = x * 2;

xが0だったらxに1を代入して、そうでなかったらxにx * 2――変数xに2をかけたもの――を代入する。
ちなみに、elseの後ろに出てくるx * 2は、代入される前のxの値に2をかけたもの、の意味である。


さて、次の例にいこう。今度はelseがないが、先ほど言ったとおり、なくても正しい。

if(x > y) y = x; x = x + 1;

この一連の命令はどう動くだろうか?
ぱっと見ると、「xの値がyの値より大きかったら、yにxの値を代入して、xにx+1を代入する」と見える。
が、残念ながらこれは不正確。
正確に意味を書くとこうなる。
「xの値がyの値より大きかったら、yにxの値を代入する。(xの値とyの値がどうだったとしても)その後にxにx+1を代入する。」
つまり、予想とは「xにx+1を代入する」という操作の場所が違う。


これは、ifによる条件分岐が「それぞれ一つの命令だけ」実行できるからである。
しかし、当然のことだが複数の命令を実行したい(こともある)。
そこで出てくるのが、複数の命令を一つとして扱う方法。
一つの命令を「文」と言ったりする関係で、「複文」と言ったりする。

複文

関数を書くときに、内容である命令の前後を「{ }(中括弧)」で囲っていた。
そして、この中では複数の命令が書けていた。
中括弧は「複数の文を書く」ことを許す構文である*7
ちなみに、「複文」は「文」とは言うけれど、セミコロンは不要*8


では、この書き方でさきほどの例を書き直してみよう。
最初の解釈はこちら。

if(x > y) { y = x; x = x + 1; }

それで、その次の解釈を、明確に括弧をつけていくとこのようになる。

if(x > y) { y = x; } x = x + 1;

このように、「複文」という名前はついているけれど、中にある「文」は1つでもいい。
もちろん、0でもいい。
だから、「単一の」文(命令)だけが書ける場合、自分の意図を明確にするために複文の形にしたほうがいい。
別にコンピュータにしてみれば、前に書いた例と、すぐ上の例は何一つ変わりはしない。
だが、人間が見るときは(特になれていない人ほど)すぐ上のように囲んであった方がわかりやすい。
・・・まあ、「面倒だ」という異論もあるので、あなたが気に入ったようにした方がいいだろう*9


うあ、既に長い・・・でも続けて説明するよ。

再帰関数・再帰呼び出し

再帰関数については、特に面倒な説明もない。
関数の定義の中で、その関数自身を呼び出す関数のことを再帰関数といい、その呼び出しを再帰呼び出しという。
しかし、何もせずに関数を呼び出すと、大体永遠に関数を呼び出し続ける*10ので、条件分岐を使う。
では、次のプログラムに行こう。
定義している関数は、0から引数として渡された値までの和を足したものを出す*11

int sum(int n) {
 if(n == 0) { return 0; }
 else { return sum(n - 1) + n; }
}

sumという関数の中で、else側にてsum関数をもう一度呼び出している。
しかし、引数の値が少し違って、「nの値(ここでは元の関数の引数の値そのまま)から1を引いたもの」を引数にしている。
sum関数を呼び出した結果として期待している内容は、「0から渡した値までの値の和」である*12
なので、else側で書いているreturn命令には「nの一つ前までの和(0からn-1までの和)にnを足したもの(=0からnまでの和)」をくっつけているわけである。
ちなみに、ifの条件式が正しい(=nが0である)ときは0にしている。0から0までの和は0に決まっている。


説明の中で初めて関数の「結果」を使っているが、「return命令に書いた内容が呼び出した場所の値に変わる」という関係上、上のような使い方も割と一般的である。
まあ、残念ながら、目的とする関数(画面に数字を出す)は「結果」が不要なので、上のような使い方はしないのだけれども。


さて、再帰関数が出てくると、引数が何度も出てくることになる。
例えば、上のsum関数でsum(5)を呼び出すとどうなるだろう?
まず、nが5としてsum関数が実行される。
そして、5は0ではない(そりゃそうだ、などと言わない)ので、else側が実行される。
return命令の中でsum関数が呼び出される。このとき引数はn-1、nは5なので引数は4になる。
そして、nは4になる。


・・・さて、今回4になったnと最初に呼び出した時の(5だったはずの)nは同じだろうか?
まあ、同じだと困る。sum(4)を呼んだ後、その結果にn(当然5だと思っている)を足すのだから、4だと予想に合わない。
というか、この先も再帰呼び出しがあるので、最終的に0になっているはず。


困るのだから、当然そうなっているはずがない。
だから、呼び出し毎に引数nは新しく場所が作られていることになる。
当然、nといったら「そのとき呼んだ関数での引数n」である。
こうなると、どの変数が見えているのか、ということで、変数の「見える範囲」が大事になってくる。
この「見える範囲」が有効範囲と呼ばれるものである。

引数・変数・関数の有効範囲

有効範囲(英語でscope、よくカタカナでスコープと書く)は、「見える範囲」=「使える範囲」のこと。
この外側では、変数や引数、関数が見えない(ので当然使えない)。
さて、有効範囲について、それぞれ簡単に書いていこう。

  • 引数の有効範囲は「関数が始まってから終わるまでの、定義における命令の中」。再帰呼び出しした場合、その「呼び出された関数」は「呼び出した関数」と別のもの、という扱いになる。
  • 変数の有効範囲は「宣言したブロックの中」でかつ「書いた後」。ブロックとは、中括弧で囲まれた(複文のときか関数の定義のとき)範囲。どの中括弧にも囲まれていないとき、宣言された変数を「大域的である」といい、書いた場所以降どこでも使えることになる。逆に、関数の中で宣言された変数は「局所的である」といい、その関数の外側では見えない。
  • 関数の有効範囲は「宣言または定義以降全ての場所」。ちなみに、関数は通常関数の中で定義できない*13。なので、変数での言い方をすれば、常に「大域的」である。

最後に、同じ名前の変数などがあるとき。
これは、「最も近い」変数になる。例えば、大域的な変数と、(見える範囲の)局所的な変数があった場合、局所的な変数が優先される。
つまり、次のプログラム・・・

int x;
int f() {
 int x;
 x = 1;
 return x;
}

の場合、代入やreturn命令で使っているxは、最初に宣言しているxではなく、その直前に宣言しているxになる。


最後に、「同じブロックでは同じ名前のものは定義(変数の場合は宣言)できない」ことが決まっているので、ほとんど「距離的に近い」が上で言う「近い」に当たる。
ちなみに、関数は全て大域的なので、同じ名前のものは二度定義できない。
組み込み関数(C言語に最初から作られている関数)と同じ名前にならないように注意しなければいけない。

今回のまとめ

今回やったことは、結構幅が広い。あえて言えば、「プログラムに構造が書けるようになった」というもの。

  1. 条件式とは、「正しい」か「正しくない」のどちらかと判断できるもの。
  2. 条件分岐とは、条件式を伴って、その条件式が「正しいときに行う命令」と「正しくないときに行う命令」に分岐するもの。
  3. 複文とは、中括弧で複数の命令を囲んだもので、一つの命令として扱われる。
  4. 再帰関数とは、定義の中で自分を呼び出す関数。自分を呼び出す部分を再帰呼び出しという。
  5. 有効範囲とは、名前が使える範囲。スコープと呼ぶ。大雑把には、「宣言された場所と同じ範囲」で「宣言された場所以降」である範囲。関数の中で定義された変数を局所的であるといい、そうでないとき大域的であるという。

最後の局所的・大域的の説明が上と少し異なるが、C言語では宣言や定義以外の命令を大域的に書くことがいけないので、結果的には同じになる。

次回予告

今度こそ三度目の正直ということで、数字を画面に表示する。
今度こそは書けるので。今度こそする。本当に。いや、嘘じゃないよ?

最後に

補足ではないけれど、今回はこれまたC言語普通の解説と違うやり方をしている。
普通は、「命令の一種」である「繰り返し」を先に書く。
が、なぜかここでは「再帰関数」を先に書いている。
理由は割と単純で、「再帰関数のほうが楽に書ける」から。
ま、呼び出しは遅い(ほかにもいろいろ問題がある)ので、いわゆる「実用的な」プログラミングでは「再帰関数」を極力使わない。
でも、この解説はなにせ「実用的」からかけ離れたスタンスでやっている。
「これを読んでもプログラムは書くな。」だから。
だから「書きやすい」ほう、「楽な」ほうを優先して使った。
・・・まあ、引数の説明の面倒さはあるけれどね。


あー、本当に長い・・・

*1:前回も言ったが、printf関数を使えば一瞬で終わる。しかし、実際にそれをもっと簡単な機能から作ろうとすると、ここまで説明しないといけない。

*2:YesとNoではなく「はい」と「いいえ」だが。

*3:小学生のような問答をしたいわけではないので。

*4:ドラクエの勇者にこの答えを求めたらおそらくプレイヤーが激怒するだろう。

*5:これだと、1-1は条件式になる。C言語では、1-1は条件式としても扱え、結果が0になるものは全て「正しくない」ことを表す。一方、「結果が0でないもの」は全て「正しい」ものであるとしてある。

*6:分からなくてもいいように引数という機構があるのだし。

*7:ただし、関数の定義では中括弧をなくして一つの命令を書くことは不可である。仮にできたとしてもあまりうれしくないし。

*8:C言語には「空文」という、何も書かないでセミコロンを書いた「文」が存在する。もちろん何もしない命令(?)なのだが、複文の直後にセミコロンを書いたら空文があると見なされるだけで、複文の終わりとは判断されない。

*9:実際、簡単な計算をするだけの短いプログラムを書くときには、作ってそれ以上何もしないことが多いので、別に見やすかろうが見にくかろうが気にする必要はない。こうなると「面倒」という意識の方が強くなるし、私も複文にしないことがある。

*10:多くの場合、コンピュータが文句を言ってプログラムが止まる。C言語というカテゴリの中では別に永遠に動いても問題ではないが・・・

*11:もちろん、普通はこんな計算方法はしないが、わかりやすいのだから仕方がない。ただ、この関数に負の値を渡すとこれまた永遠に呼び出し続ける。

*12:少なくとも私はそう言った。

*13:C言語を実行するために必要な「処理系」(プログラム)によっては定義できるようにしてあるものもある。だが、標準的ではないので注意。