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

お久しぶり。
ここまで長かったけれど、今度こそ数字を画面に出す。
今回は比較的コードが長い。
ま、軽くプログラムを読む練習になると思う。
注意:このページのソースコードを勝手に使うのはご自由に、といいたいところなのだが、blogの関係上、スペースを一部全角にしているので、それを直さないと動かない。気をつけるように。まあ、たぶん使わないと思うけれどね。

前回の復習と今回の概要

  1. 条件式は、「正しい」か「間違ってる」もの。
  2. 条件分岐は、「ある条件式が正しいとき」と「そうでないとき」で実行する命令をわけること。
  3. 複文は、複数の命令を一つの命令のように扱うこと。
  4. 再帰関数は、関数の定義の中でその関数自身を呼び出すこと。この呼び出しを再帰呼び出しという。
  5. 有効範囲は、変数や関数などの見える範囲のことを言う。それぞれ、宣言や定義を行ったブロックの内側ならば見える。ブロックは中括弧で囲まれた部分のこと。
  6. 有効範囲内ならば通常変数などを使えるのだけれど、「同じ名前」がある場合は近いものしか使えない(遠いものを表せない)。
  7. 関数の中で宣言されている変数は局所的であるという。関数の中でなければ大域的であるという。関数の定義は大域的でなければならない。

まあこんなところ。ブロックの説明が少しアバウトすぎるかな?


今回の概要は、実に短い。

  1. 数字を表示する関数print_numberを定義する。

もう少し詳細に書くと、

  1. 表示に使う関数はputcharのみ。
  2. 各桁を表示するために再帰関数を用いる。
  3. 今までに説明したことのない演算子(+や-、*のようなもの)を使う。もっとも、似たものは知っているはずなので別に難しくも何ともない。

こんなところ。では、始めよう。

まず、簡単なところから

putchar関数は、「指定した文字(1文字)を画面に表示する」関数だった。
なので、数字として簡単な場合について考えよう。
ちょっと先回りをするようで悪いのだが、0から9までの、1桁の数字が渡された場合について。
この表示、書く量が面倒なので、関数で作ってしまおう。
名前は・・・print0to9でいいかな?*1
引数はint(整数)で、受け取った値は0から9までの数字だったとしよう*2
では、早速書いてみる・・・のだが、引数が0から9までのどれかなので、「愚直に」条件分岐するしかない*3
ちなみに、引数をそのままputcharに渡すのはおかしいので注意。例えば数字の1と文字の'1'は違うものである*4
全部書くと長いので、0,1,9の場合だけ書いて、後は「...」という形で省略させてもらう。あまりにそのままだから、間違えようもない。

int print0to9(int n) {
 if(n == 0) { putchar('0'); }
 else if(n == 1) { putchar('1'); }
 ...
 else if(n == 9) { putchar('9'); }
 return 0;
}

elseの後にifが書いてある部分は「前回書いた条件分岐で正しくなかった場合に、また条件分岐をするよ」という意味。
今回は、この条件分岐の並びのうち一つしか正しくならないので、elseがなくても正常に動く。
だが、elseを明示することで「nが1だったらこうしてほしい、そうでないときでnが2だったらこうしてほしい、・・・」というこの関数で書きたいことを明示できる。
もしなかった場合、「同時にいくつか実行される可能性もあるんじゃ・・・?」と考えてしまう。ここではそんなつもりはないのだから、しっかりelseも書いた。
return命令は、とりあえず終わることを表しているだけで、特にこの関数がどんな値を返そうと(呼び出した箇所がどんな値になろうと)私たちは興味がない*5。基本的に、私たちは「この関数が数字を画面に表示する」ことさえわかればそれでかまわない。


ちなみに一応説明しておくと、nの値(についての私たちの予想)が正しい限り、この関数は正しく動く*6
では、全体を構築してみよう。

全体の構築・その1:単純だけど間違えているもの

タイトルを見てうんざりする人もいるだろうが、とりあえず見てほしい。
単純であれば間違いようがない、という考えは間違っている、という例である。


数字の表示は、大きい桁を左に、小さい桁を右に、である。
コンピュータでの文字の表示は左から右なので、大きい桁から順番に表示しなければいけない。
さて、簡単に思いつく方法は、値の一番大きな桁を上の関数で表示して、その桁を消して次の桁を表示して、という動きを繰り返せばいい、というもの。


例えば、数字の123というものを表示するとしよう。
123のもっとも大きな桁は3桁目、1である。
桁数を数えることはできそうだ。例えば、10より小さければ1桁、100より小さければ2桁、1000より小さければ3桁、などなど。
1を取り出すのは、例えば100で割った値を出せばいい。整数同士の割り算は、小学校でやった「余り」のある割り算だと思えばいい*7。そう考えると、100で割れば1が出てくる。


では残りの23を取り出すには?
面倒なやり方としては、1を取り出せたのだし、桁数もわかっているのだから、数字を桁に合わせて引けばいい。
だが、まあそんなことをしなくても、C言語には「余り」を出す演算子――足し算の+とか引き算の-のように、操作を表す記号――がある。なんと、「%」だ。
イメージがわかないだろうけれど、我慢してほしい。これを決めたのは私ではないのだ*8
というわけで、あとは残った23がまだ表示しなければいけない状態ならば、上と同じように処理すれば・・・
いい・・・の?


さて、この表示方法は「明らかに」間違っている。
いやいや、上から順に見ていくんだから間違えようがないじゃないか、と言われそうだが、そんなことはない。間違っているものは間違っている。
たとえば101を表示しよう。できるかな?


答えは、101は表示できない。11が表示されてしまう。
上のやり方を見ると、最初の表示で1を出した後、101から100を引いている。答えは1。
1を表示しようとすると、当然1を表示する。
あれ、2桁目の0は?

全体の構築・その2:回避策

途中の0が表示されない問題は、「表示する数字の桁数」をとっておかなかったことだ。
だから、これを回避するには、桁数を数えながら表示を進めればいい。
「今何桁目を表示しているよ」という情報があれば、101から100を引いた1(次に0を表示する予定)と、ただの1(1だけを表示する予定)に対する表示を変えることができる。
表示のチェックは1桁目、2桁目、・・・と進めていくので、「自分の表示する桁より上は、次の桁をチェックするやつが勝手に表示してくれる。」と考えよう。
こうすると、1桁目の表示担当は、「(数字が2桁以上なら)2桁目より上は2桁目担当に投げれば勝手に表示してくれるから、その後自分の桁を表示しよう」ということになる。
2桁目は2桁目で、「(数字が3桁以上なら)3桁目より上は3桁目担当に投げれば・・・(以下略)」ということになる。
ちなみに、「自分の表示する桁」より「表示しようとする数字の桁」が大きくなることはないとしよう。
1桁目から始めれば、「表示しようとする数字の桁」は必ず1以上なので、逆転することはない。


大きなプログラムの流れは、次のようになる。

  1. 今表示しようとしている桁と、表示しようとしている数字の桁を比べる。桁数が同じならば、自分の桁を表示するだけで終了。
  2. そうでなければ(表示しようとしている数字の桁の方が大きいので)、自分より一つ大きな桁「以降」を表示するように繰り返す(関数を呼び出す)。
  3. 繰り返した処理が終わったら自分の桁を表示して終了。

以上。わざと関数の再帰呼び出し風に書いている。

全体の構築・その3:プログラム・・・の前に

さて、これをC言語で書いてみよう。
といっても、結構細かな問題はある。

  1. 桁数から10とか100とかもってくるのはどうすればいい?
  2. 「自分の桁の数字」をどうやってとりだそう?

など。


1については実際は簡単で、別に桁数でなくても、「自分がどこを表示するか」と「自分より上の桁を指示する方法」さえわかれば問題ない。10とか100とかを桁数の代わりに使えばいい*9
ならば2はどうだろうか?


大まかには、「自分より上の桁」と「自分より下の桁」を全て消してしまえばいいことになる。
その1で少し話したが、「自分より下の桁」は「自分の桁」で割ればいい。1桁目ならば1(何も変わらないけれど)、2桁目ならば10、3桁目ならば100、といった具合に。
「自分より上の桁」は、「自分より一つ上の桁」で割った余りを使えばいい。1桁目ならば10で割った余り、2桁目ならば100で割った余り、3桁目ならば1000で割った余り、といった具合に。
この計算は同時に行えないので順番に行う。下の二つのどちらかを使えばいい。

  • (数字 % (自分の桁 * 10)) / 自分の桁
  • (数字 / 自分の桁) % 10

これで、自分の桁の数字が1桁で出てくる。なお、前者は「上の桁を消して、その後下の桁を消す」、後者は「下の桁を消して、その後上の桁を消す」という順番。
後者では、下の桁を消すと「自分の桁」が1桁目にくるので、余りを計算する箇所は2桁目を表す10を使っている。
なお、後者の方が計算としてはほんの少し早い・・・かもしれない*10


「自分の桁の数字」を返す関数を作る。必要なのは「表示する数字」と「桁」なので、次のように。

int place_number(int n, int place) { return (n / place) % 10; }

以上。placeは桁(を10の累乗で表したもの)、nは表示したい数字。
place_numberで一つの関数の名前である。名前にはアルファベットや数字、"_"*11を使うことができる。C言語では割と単語の区切りに"_"を入れる。
さて、前準備は以上、本来の関数を作ろう。

全体の構築・その4:プログラム

前々節で説明した方法を、そのままプログラムにすると次のようになる。

int print_number(int n, int place) {
 if(n < place * 10) { print0to9(place_number(n, place)); }
 else { print_number(n, place * 10); print0to9(place_number(n, place)); }
 return 0;
}

最初のifは、「placeの10倍(=自分の桁の上の桁)より小さい(=自分の桁で終わり)」かどうかで分岐する。
正しい場合(自分の桁で終わりの場合)、自分の桁を取り出して、その数字を画面に表示させる。
正しくない場合、自分の一つ上の桁以上を表示させて、それから自分の桁を表示させる。
分岐が終わった後に終了。


少し気がつく人ならば、自分の桁の表示がどちらにも(全く同じ形で)あることに注目するだろう。
だから、これを少しだけ短くできる。

int print_number(int n, int place) {
 if(n >= place * 10) { print_number(n, place * 10); }
 print0to9(place_number(n, place));
 return 0;
}

>=という見慣れない記号が出てきたが、「左側が右側以上である」ことを表す。両方が同じであるときも正しい。
上に出てきた<とはちょうど逆になるので、上に出てきたelseの場所でだけやっているprint_numberの再帰呼び出しだけをやって、その後に「どちらの場合でも」自分の桁を表示して終了している。


あとは、この関数をmain関数から呼び出せば使える。placeの引数を忘れないように。1桁目からだから、1を渡す。
main関数の例としては次の通り。

int main() { print_number(707, 1); putchar('\n'); return 0; }

これで707という数字が画面に表示され、改行されて終わる。連続でprint_numberを呼び出すと数字が続いてしまうので、putcharなどで区切りを入れないと読めない。
例えば、次のmain関数:

int main() { print_number(1,1); print_number(2,1); return 0; }

は、画面に12と表示する。12なんだか1と2なんだかわからない。
どうせいつもplaceの引数を1で呼び出すのだから、次のような関数を準備して使うのも一つの手だろう。

int print_number1(int n) { print_number(n, 1); putchar('\n'); return 0; }


補足

このプログラムは、いろいろと改善の余地がある。
例えば、このプログラムは10桁の数字(10億の位)の数字を出せない*12
これは、そもそもintという型の値はおおよそ20億(厳密には2の31乗から1を引いたもの)までしか正確に持てないことが原因である。
このプログラムは10倍の値をもってきてその値との大小で桁数をチェックしているので、10億を超える値については、(100億を表現できないので)桁数を確認できない。
それでも20億を超えない限り(15億あたりなど)は表現できるのだから、表示したいかもしれない。
というわけで、もっと効率よく表示するプログラムを考える。


今度の関数は、自分のすべきことは1桁目を表示することとする。
2桁目以降は「10で割った値(=1桁目を取り除いたもの)」で再度行うことで、全ての桁を表示させる。
以下のプログラムと、この説明で「どうして全ての桁が表示されるか」を考えてみてほしい。
まあ、何となくわかると思うが。

int print_number2(int n) {
 if(n < 10) { print0to9(n); }
 else { print_number2(n / 10); print0to9(n % 10); }
 return 0;
}

ifの条件が正しいときにnについて余りをとっていないのは、nが10未満(だから負の値でなければ1桁)だとわかっているからである。
この関数だと、placeという引数が不要になり、プログラムもすっきりしている*13
nが10未満の時に10で割った余りをとっても同じ値なので、print_number関数の時にまとめた方法と同じ方法でprint0to9の呼び出しを一ヶ所にまとめることもできる。
・・・してもしなくてもそんなに変わらないけれど。

今回のまとめ

まとめも何も、関数を作っただけだからまとめることはないのだけれども、所感を書いておく。

  1. if 〜〜 else if 〜〜 ・・・という書き方は「どれか一つだけ」を選ぶ場合によく使う記述である。
  2. intという整数は表現できる範囲がある程度決まっている。範囲を超えると正確でない値になるので、その範囲に注意する必要がある。
  3. 関数でよく使う形があるなら、それを表す関数を再度作るのも一つの手である。特に、「最初に渡したい」引数が完全に決まっている場合は、「最初に渡したい」値を見つけやすくできる(上に表面的にはその値を見る必要がなくなる)ので、よく関数が準備される。
  4. 処理が一見単純でも、本当に正しい保証なんてどこにもない。このようなプログラムは一見正しいことが多いので、正しくない例を見つけられるかが問題である*14
  5. やり方を思いついても、そのままプログラムにできるかはわからない。今回、途中途中で挙げた問題は、(特になれない頃は)プログラムを書いている途中で発見するような内容である。
  6. 簡単に考えられること≠プログラムが単純であること。プログラミングに慣れるとかなり近くなるのだが、そうでない場合、簡単な考えというのは余計な処理が多くなりがちである*15

もっとも、これらの内容(最初を除く?)は割と経験則なので、ここでのスタンス(「これを読んでも書くな」)とはかなりずれている。
しかし、プログラムの意図を読む(読めるように書く)のは重要である。
総じて、今回は言語の説明と言うより、プログラミングの細かな技術の説明、というニュアンスが強かったかもしれない。

次回予告

しばらく今回に向けて突っ走ってたので、あまり考えていなかったが・・・
次回は「繰り返し」について。
実は、今回のprint_number関数くらいだと繰り返しの方が書きやすく、しかも速かったりする。
一般的に、C言語では同じような操作を実行するならば繰り返しの方が速い。
・・・もっとも、無理に繰り返しにすると、面倒が生じることが多々あるのだけれども、ね・・・

*1:いい名前が思いつかない・・・

*2:例として書いている関数ではそれを確認していない。本当は確認したほうがいいのだが、面倒なのでしない。

*3:C言語にはswitch文という、もう少しこんな場合に楽にかけるものもあるのだけれど、本質的には何も変わらない。

*4:本当は、文字も「数字」の一種なので、計算でもっと簡単に出せる。特に'0'から'9'までの「文字」は通常並んでいるので、ある値を加えるだけでできるのだが、文字についての説明をかなり省いているのでここではその方法を用いない。

*5:行儀の悪いプログラミング作法ではある。値がいらないならばそれを明示すべきで、明示する方法は実際にある。が、説明していないので適当に0にしている。別に1でも-3500でもいい。普通0か1くらいだが。

*6:nの値が予想通りでなく、1から9以外の数字だった場合、この関数は何もしない。

*7:厳密には負の値について面倒な話がある。今回は0以上の値だけなので、これで正しい。

*8:私が決めろ、といわれてもイメージがわきやすい候補がなくて困るのだが。

*9:自分より上の桁は10をかけたものだから簡単。

*10:遅くなるのは特殊な場合なので、よほど問題が起こらない限りは後者を選べばよい。

*11:記号の名前は「アンダースコア」。「アンダーバー」と呼ぶ人もいるが、少なくとも記号として正確な呼び方ではない。

*12:一般のパソコンの場合。100京まで出せるようになっているものもあれば、1万すら出せないシステムもあるので、一概には限界をいえないが、多くの場合10億に入った時点でプログラムがまともに動かなくなる。

*13:しかも余計な計算がないため、わずかながらprint_number関数より速い。

*14:ある程度正しくない例を見つける方針は存在するが、それは完全ではないし結局経験に頼ることが多い。まだプログラミングが職人芸となっている所以だろう。

*15:print_numberのplace引数がいい例である。10億以上で表示できなくなる問題を作ってしまった上、よりプログラムを単純にしたら不要になる。