「よくわかるC言語」,今回は連載3回目です。1回目はBorland C++コンパイラ*1のオプションを指定して,C言語のコードの1行から複数のアセンブリ・コード,つまりマシン語が出力されることを確認しました。2回目は浮動小数点数の計算に誤差が出る理由などを説明し,変数の性質を学びました。さて今回は,プログラムの構造を説明し,「C言語のプログラムはどのように書けばよいのか」を大まかに理解してもらうことを目標にします。カメラのズームをいったんぐっーと引くように,ワイドな視界でC言語のプログラミングを眺めてみましょう。

基本は構造化プログラミングにあり

 皆さんは「構造化プログラミング」という言葉をご存知ですか? 1960年代後半,エドガー・ダイクストラ*2が中心となって提唱したプログラミングの考え方です。現在,主流となりつつあるオブジェクト指向プログラミングも,実は構造化プログラミングをベースとしています。

 構造化プログラミングの目的は,大規模なプログラムを効率よく作成し,プログラムのミス(バグ)を少なくすることです。そのための具体的な方法としてダイクストラは,プログラムに必要な手続きをいくつかの単位に分け,メインの処理に大まかな処理を,サブルーチンに細部の処理を記述せよと主張しました。また,プログラムは「順次」「反復」「分岐」の三つの基本構造で記述できるという構造化定理を証明し,結果として「goto文は不要である」と主張しました。

 「goto文は不要」と言われても,goto文自体を見たことがない方もいるでしょう。goto文というのは,プログラムの途中で,指定した行番号やラベルにジャンプできる制御文のことです。例えばgoto文を実装している初期のBASIC言語で書かれたプログラムは,goto文でプログラム中の行番号やラベルに飛んで,そこに記述してある処理を数行実行したあと,またgoto文でどこかへ飛んでいく――という構造になっていることが多かったのです(図1)。これでは,どこから来てどこへ行くのかわからないスーパーマンのようなもので,いつどのコードが実行されるのか,ロジックを追うことが難しくなります。「実行直前に他の場所に飛んでしまうので,永遠に実行されないコードがあった」なんてことも古いプログラムではよくあったのです。

図1●BASICにおけるgoto文のイメージ
図1●BASICにおけるgoto文のイメージ

 そういった複雑さを読み解くことに,迷路やクイズのような楽しさを感じる方がいらっしゃるかもしれませんが,一般的に「わかりにくい」プログラムほど「迷惑」な話はありません。そこで以降は,ダイクストラの教えに従って,C言語で,

●プログラムを複数の単位に分けて作成する
●順次・反復・分岐の論理構造を作る


方法を解説していきましょう。

関数という単位で複数の単位に分ける

 C言語は,printf関数*3を始めとする汎用性の高い関数をライブラリ関数として標準で装備しています。しかし,装備している関数を使うだけで,すべてのプログラムを作成できるわけではありません。

 リスト1のCのプログラムを見てください。文字型の変数cに「文字」として0から9までの数字を代入し,printf関数に%c ,%xと変換仕様を指定して,文字と16進数を画面表示させるプログラムです(図2)。文法上は何の問題もありません。でも,何か冗長な感じがしませんか?先の構造化定理の中で,使っているのは「順次」,すなわちmain関数の上から下に向かって処理が実行されている部分だけですね。

リスト1●0から9までの「文字」と,その16進数を表示するプログラム。文字が増えればコードは長くなり,冗長である
リスト1●0から9までの「文字」と,その16進数を表示するプログラム。文字が増えればコードは長くなり,冗長である

図2●リスト1を実行した結果
図2●リスト1を実行した結果

 これをリスト2のように書き直してみるとどうでしょう? outxという名前の自作の関数を作り(1),main関数から呼び出すようにしてみました(2)。main関数の内部では,繰り返しを行う制御文for(後述)を使って,変数cの値が文字としての‘0’から‘9’以下の間,処理を継続させています。

リスト2●リスト1を関数という単位で作り直したCのプログラム
リスト2●リスト1を関数という単位で作り直したCのプログラム

 リスト2のポイントは,

●main関数が処理の制御を行っている
●outx関数が画面への値の出力を行っている


の2点です。main関数とoutx関数の二つだけですが,プログラムを複数の単位に分けたわけです。

 このような「関数化」のメリットは何でしょう。まず「コードの量が少なくなる」という点が挙げられます。コードの量が多ければ多いほど,タイプミスなどが原因でバグが出やすくなりますから,関数化すれば間違える確率が下がります。リスト1のように「printf("%c:%x ",c,c);」を何度も書くより,「outx(c);」を1行だけ書く方が,間違えにくいですよね。

 また,処理を関数としてうまく分割してやることで「プログラムがわかりやすくなる」ことも挙げられます。「この関数ではXXXの処理を行う」とシンプルなコメントで説明できる関数を作れば,メンテナンスも容易になるでしょう。

 それでもまだメリットを実感できない方は,main関数に500~600行とコードがズラズラ記述してあるプログラムをイメージしてください。筆者ならとてもそんなコードを読みたくありません。小さなプログラムなら,1人暮らし用の小さなワンルーム・マンションのように,トイレと風呂で一つの部屋(関数),それ以外で一つの部屋,で済むこともあるでしょう。しかし家を建てる時に1階を全部ワンルームにして,キッチンも子供部屋も客間も寝室も,あろうことかトイレもバスも仕切りなしに,ぜーんぶ1部屋に収めたら,メチャクチャになりますよね。たくさんの家族が住む2世帯,3世帯住宅のような大きなプログラムだと,それに応じた部屋数(関数)が必要になることは,十分納得いただけると思います。

 とはいえ,どの部分を関数として作成するかの判断には,ある程度のプログラミング経験が必要です。初心者の方は,最初のうちは目的の処理をザッーと書いて,そのあとで同じような処理を書いてあるコードを見つけて関数にしていけばいいでしょう。それだけでも,ずっと「わかりやすい」良いプログラムになるはずです。