今回は大学や高専でC言語を習った人のための"再入門"講座です. 学校では,if文やfor文といった基本的な内容から,関数やポインタなど ちょっと難しい話も勉強したことと思います. しかし学校では本当に基本的なことしか教えてくれません. 演習や試験でよい点数をもらったからといって,「C言語をマスターした!」 と思ってしまうのは大きな誤解です(←昔の自分).

研究などで,画像処理やロボットの制御など,実際にちゃんとした目的 があって,なおかつある程度の規模のプログラムを作る場合は それなりのテクニックが必要です.たとえば,

  • バグの出にくいプログラムを作るテクニック
  • 拡張性・汎用性のあるプログラムを作るテクニック
  • 他人が読めるプログラムを作るテクニック
  • 楽にコーディングできるテクニック
こういうものが必要になります.いくつかのルールを 守っていれば,これらを両立することはさほど難しくありません. それでは見ていきましょう.

(公開:2005年05月16日)

きれいなプログラム・汚いプログラム

「美しい or きれいなプログラム」「汚いプログラム」という表現があります. きれいなプログラムは読みやすいだけでなくバグやエラーも発生しにくいものです. きれいなプログラムと汚いプログラムはどんなところが違うのか表1にまとめてみました.

表1. きれいなプログラム・汚いプログラム
コーディング箇所きれいなプログラム汚いプログラム
字下げしているしていない
コメント適切な箇所に簡潔に記述ない/無駄に書いてある
条件分岐少なくて簡潔多くて複雑
入れ子多くても3重構造4重構造以上
ループの終了条件終了条件が1つで単純gotoやbreakが多くて複雑
変数名英語名でわかりやすいわかりにくい/一文字
変数の数少ない多い
関数名英語名でわかりやすいわかりにくい
関数の引数少ない多い
関数の長さ短い長い
グローバル変数少ない/ない多い
定数マクロ定義/列挙型値が直接書かれている
関数群ヘッダとソースに分離一個のソースに全部記述

いくつ当てはまりましたか? 汚いプログラムの項目が5つ以上当てはまるようであれば,再入門の必要があるでしょう. さて,きれいなプログラムが何なのかを一言でいうと

きれいなプログラム=他人や未来の自分が見たときに読めるプログラム

これに尽きると思います. 汚いプログラムを書いてしまうと,その時点で理解していても何日か時間をおいて再び触れる時には いとも簡単に理解不能になります.フローチャートを並べたメモなどを残していても全く役に立ちません. それではきれいなプログラムを書くテクニックを見ていきましょう.

きれいなプログラムを書くには

まずは妙な自己流を捨て,世間一般に使われているような(暗黙の)ルールに従うことから始めます.


字下げ・改行をきちんとする

基本中の基本です.これができていないプログラムは誰も読みたがりません. 字下げをする理由は言うまでもなく条件文やループの入れ子構造を わかりやすくするためです.デバッグの時に苦労したくなければ きちんと字下げする癖をつけましょう. VisualC++では自動で字下げをするようにカーソル移動してくれるので活用してください.

条件文やループ文の改行位置については以下のような2種類が見受けられます.

// タイプ@
for ( i = 0; i<n; i++ ) {
    /* 処理 */
}

// タイプA
for ( i = 0; i<n; i++ )
{
    /* 処理 */
}  
括弧{の前後どちらで改行するかの違いですが,多くの書籍や人の書いたプログラムを見る限り, タイプ@のほうが圧倒的に多いです. 一般的に多く使われている書き方に準じていたほうが,他人に迷惑もかからないというものです. ちなみに関数の括弧については,以下のようにタイプAで改行するのが一般的です.

int func()
{
    /* 処理 */
}  

命名規則に従ってコーディングする

一般的に次のような(暗黙の)ルールがあります.

名前は単語をつなげてなるべく意味のある名前にします. つなぎ方は単語の先頭を大文字にするか,_(アンダーバー)でつなぎます[1]. また,変数についてはその性質によって次のような名前にすると後々苦労しません[2]

このようにするのは,言うまでもなく混同を避けるためです. 変数名は結構大事なもので,規模が大きくなるにつれその重要性は増します. a,b,cなどの一文字の変数名は絶対避けるべきですが,座標値を意味するx,y,zや 配列の添え字のi,j などは例外でしょう.逆に名前が長すぎるのも避けましょう.


定数の扱い方:マクロ定義と列挙型

【解説】

マクロ定義とは,コンパイラへの 命令のひとつである#defineのことです.たとえばソース中で

#deifne SIZE 100
としておくと,プログラム中で「SIZE」と書かれている箇所がコンパイル時に 全て「100」という文字に置き換わります. つまり文字の置換処理をやってくれるわけです.メッセージなどに応用するなら,
#define ERROR_MESSAGE "エラーです"
と定義しておいて,
printf(ERROR_MESSAGE); 
という感じで使えます.メッセージの内容を変更したければ#defineしている箇所のみを 変更すれば全体に反映されます.このようにマクロ定義することでわかりやすくなるだけでなく, 仕様変更に強いプログラムになります.

定義した数値自体に意味はなく,その定数名そのものに意味があり,なおかつそれが 何かの集まりであれば「列挙型」を使うのが良いです.具体例を以下に示します.


【例】ジャンケンの入力判定プログラム

ジャンケンですから取りうる入力値は,「グー」「チョキ」「パー」の3種類です. マクロ定義を使うアイデアなら

#define HAND_GU     0
#define HAND_CHOKI  1
#define HAND_PA     2 
と定義することで,次のようなプログラムが書けます.

int player = HAND_CHOKI; //プレイヤーの手をチョキにする

if ( player == HAND_GU ) printf("グーを出しました");  

これでも十分ですが,ここでHAND_GUというのは「グー」を意味する文字であることが重要で, 定義されている0という数値はさほど重要ではありません.そこで列挙型を使って次のように定数をまとめてみます.

typedef enum{ GU, CHOKI, PA } janken_hand;  
こうすることで,ジャンケンの手を意味する型としてjanken_hand型を定義したことになります. 使い方はマクロ定義したときと同じ感覚です.

janken_hand player = CHOKI; //プレイヤーの手をチョキにする

if ( player == GU ) printf("グーを出しました"); 

マクロ定義と違ってjanken_hand型はGU,CHOKI,PAの3種類の値しか取らないことを 明示することができるので,たとえば,

janken_hand get_hand();  
という関数を見たとき,「あー,これはジャンケンの手を返す関数で,GU,CHOKI,PAの3種類の値 しか返さないんだな」と瞬時に理解できます.これが列挙型を使うメリットです.


変数の集まりは構造体でまとめる

【解説】

変数が無駄に多い場合は,関係のあるもの同士をまとめることができないか考えてみることが 必要です.例えば色を表現する際には赤・緑・青の3色を使って表現しますが,これを

unsigned char red,green,blue;  
と3つの変数を置くのではなく,
typedef struct tagCOLOR{
    unsigned char R;
    unsigned char G;
    unsigned char B;
} COLOR; 
というような構造体でまとめるべきです.

変数を構造体でまとめることによって次のような効果が得られます.

また,余計な変数を宣言しなくて良くなるのでコーディングもかなり楽になります.

ちょっと一休み

さて,きれいなプログラムを書くテクニックについて述べましたが,これだけのルールを守るだけで

がほぼ両立できます. ここで言う「他人」は「未来の自分」も含まれます. 時間をおいて改めてプログラムを見たときに名前の付け方や定数宣言がきちんとなされていれば, 過去の自分に感謝したくなることうけあいです.バグの大半は汚いコーディングによるものなので, きれいなコーディングを心がけていれば自然とバグは少なくなります.

拡張性・汎用性のあるプログラムを書くには

拡張性・汎用性のあるプログラムを書くためには,関数が作れることが必要になります. C++においてはクラスを使うことによってさらにパワーのあるプログラムが組めるようになります. また大規模なプログラムを作る際にこうした部分を序盤できちんと作っておけば,後々 スムーズにコトが進められて楽ができます.


関数化ということ

プログラミングとは「関数を作っていく作業である」とよく言われます. main関数に全ての処理を詰め込んでしまうようではいけません. ある目的の処理をこなしている複数の行を,関数というひとつの処理単位に置き換えてください. 関数の長さは一目で見渡せる画面1ページ分が目安だとよく言われます. 長くなり過ぎないように上手に細分化してください. また,似たような処理はコピペで作らずに共通部分を関数にできないか考えましょう. この場合,差異のある部分を引数にすれば良いのです. 引数が多くなってしまうのであれば,構造体を使ってまとめられないか考えてみると良いでしょう.

関数化によるメリットは

ということです.汎用性のあるプログラムは関数化なくしてありえません.

関数を作ったら忘れる前に関数の仕様をコメントするようにしましょう. これをおろそかにすると,後々痛い目見ることになります.


【コメントの一例】
/*-------------------------------------------
【名前】GrayFilter
【機能】画像のグレイスケール化を行う
【引数】inimage    入力画像
        outimage   出力画像
        threshold  閾値
【戻値】成功:0,失敗:-1
【備考】特になし
-------------------------------------------*/  

クラスというデータ構造

クラスはCではなく,C++の機能です.詳しくは説明しませんが, クラスを使うことによって関連する複数のデータと関数をまとめることができます. 言うなれば,構造体の拡張版といったところでしょうか. クラスを使ったプログラミングはオブジェクト指向プログラミングというものに分類されます. クラスに興味のある人はC++について勉強されてください.

さて,クラスを勉強してわかった気になっても,実践で使うのはなかなか難しいです. まず何をクラスにするのか?が一番の悩みどころです. 文献[3]では,1つの指針として次のように述べています.

グローバル変数があって,そのグローバル変数を複数の関数で参照 しているなら,それらを1つのオブジェクトにする. (グローバル変数を複数の関数が参照するということは,それらが 関連する仲間であるからにほかならない)
つまりこれが意味するところは,関数化によっても駆逐できないグローバル変数をクラスという枠組みに 取り込むことで結果的になくすことができる,ということになります.また, クラスのアクセス制御という考え方を使えば,ある範囲ではグローバル変数的な使い方をしつつ, ある範囲からはアクセス不可能な変数を作ることが可能です(いわば「安全な」グローバル変数).

クラスを使ったオブジェクト指向プログラミングについては, 設計手法がいくつか提案されていて, 代表格のUML法(Unified Modeling Language)などは多数本が出版されています.

バグの原因はどこに?

コンパイルできるけど,挙動がおかしい…いわゆるバグですが, 経験上次のことが言えます.

  1. コンパイル時の「警告」にバグの要因が潜んでいることが多い
  2. コピペ改変でコーディングした箇所はバグが発生しやすい
  3. 計算が合わないときは,演算時の「型」に問題がある
  4. ローカルで宣言できる配列の大きさに限界がある
  5. 関数にはエラー判定を組み込むべき
それでは1つ1つ見ていきましょう.


コンパイル時の「警告」にバグの要因が潜んでいることが多い

コンパイル時の「警告」は『ローカル変数は 1 度も使われません.』といった危険度の低いものから 『値を返さないコントロール パスがあります』といった危険度の高いものまでいろいろあります(これらはVisualC++6.0の例). バグの予防策として,警告は必ず殲滅する癖をつけてください.


コピペ改変でコーディングした箇所はバグが発生しやすい

次にコピペ改変でのミスですが,これは僕を含めてみんなよくやります. 似たような処理だからといって,「コピー&ペーストして変数の部分だけ変更すればいいや」 という感じのノリでコーディングしてると,変更し忘れや変更ミスによってバグが発生します. バグ発生直前にコピペ作業をしていた場合は,真っ先にその箇所を疑ってください.


計算が合わないときは,演算時の「型」に問題がある

型の上限値・下限値や自動型変換について勉強が足りないと,ドツボにはまる ことになります.次のプログラムを見てください.

unsigned char data1 = 255;
unsigned char data2 = 2;
unsigned char sum;

sum = data1 + data2;

printf("sum:%d\n",sum); 
unsigned char型の値の範囲は0〜255なので,この計算結果sumは257にはならず,
sum:1 
となってしまいます.つまり上限から下限に一周することになります. 値の範囲には注意が必要です.

割り算はバグが発生しやすい箇所です. 以下は単純な1÷9の計算ですが,型やキャストの仕方によって計算結果は異なります.

printf("ans1:%lf\n",1/9);
printf("ans2:%lf\n",(double)(1/9));
printf("ans3:%lf\n",(double)1/9);
printf("ans4:%lf\n",1.0/9);
printf("ans5:%lf\n",1/9.0);
printf("ans6:%lf\n",1/(double)9);
printf("ans7:%lf\n",(int)(1.0)/(int)(9.0));
実行結果は以下のようになります.
ans1:0.000000
ans2:0.000000
ans3:0.111111
ans4:0.111111
ans5:0.111111
ans6:0.111111
ans7:0.000000
「1/9」と「1.0/9」が異なることに注目です.1は整数で,1.0は実数です. int型/int型=int型になるので,1/9で0.11という実数値を期待していた場合はこれではまずいですね. 数値演算においては自動的型変換が行なわれます. 型には優先順位があり,異なるデータ型間での演算を行なうときには より優先度の高い型の方に型合わせをしてから演算します[1]

型の優先順位:char<short≦int≦long<float<double

あと,割り算におけるバグの1つに「ゼロ割」があります. 割り算の時に0で割ることがないよう注意してください.


ローカルで宣言できる配列の大きさに限界がある

ローカルで宣言した自動変数はスタック上に領域を確保するので, あまり大きな領域(配列)を確保しようとすると,あふれてしまいその時点でプログラムが異常終了します. 大きな配列を使いたい場合は

のどれかで対処してください.


関数にはエラー判定を組み込むべき

バグと戦うためには,関数作りの際に処理の成否を出力するような仕組みをつくる癖をつけてください. 一般的によく使われているのは,戻り値を成功:0,失敗:-1とする方法です. メモリを動的に確保するような場合や,そのようにして確保したメモリを参照する場合は, ヌルポインタに対するエラー判定が必須になります.怠ると青い画面を見る羽目になりますよ….


役立つ本の紹介

最後は本コーナーでしめたいと思います. 先日所有しているC言語の本を数えたら洋書を含めて17冊(+2冊注文中)もありました. 買いすぎです.バカです.今回はそのなかでも堂々と薦められるものを紹介します.

Cプログラミング診断室―さらに美しく健康的なプログラムのために 改訂新版
Cプログラミング診断室
さらに美しく健康的なプログラムのために
藤原博文(著)/技術評論社/ISBN: 4774117870/2003年07月

Software Designという雑誌に10年以上前に連載されていたコーナー. 1993年に本になり,増刷を繰り返し一度絶版になった後,WEBサイトに全内容が公開されました. そして2003年に改訂版が出版されたというベスト・ロングセラーです. 徹底的に悪いプログラム(職業プログラマが書いたもの)を例に, どこが悪いかを指摘し改善方法を処方するという,「反面教師」な内容です. C言語上達の秘訣は,良いプログラム・悪いプログラムがどんなものかを知ることです. これを読んだ後で,後輩の書いたプログラムを見てみると,悪いプログラムの本質は10年経った今でも そう変わらんのだなぁと実感できます.書籍版のほうが読みやすいのでオススメ.


Web版
Cプログラミング診断室(Amazon)
新ANSI C言語辞典 新ANSI C言語辞典
平林雅英(著)/技術評論社/ISBN:4774104329/1997年05月

関数リファレンスに加えプログラミングに関する専門用語などが数多く収録されています. printf() [プリント エフ] といった感じで読みも書いてあるあたりに好感を覚えます. 各関数においてプログラミング例も書かれています. 付録には,プログラミング用語と英語名の英和対訳一覧,ヘッダ別の関数リスト(索引), 文字コード体系一覧,Cプログラミング・トラブル診断室,記号一覧など, とにかく必要以上にいろいろな情報が所狭しと載っています. 便利なことこの上ない本ですが,少々小さめの文字で凝縮して書かれているので, ほんと国語辞書みたいな本です.

新ANSI C言語辞典(Amazon)

美しいC++プログラミング見本帖 C++美しいプログラミング見本帖
〜クラスとメンバ関数手習い指南〜
柏原正三(著)/翔泳社/ISBN:479810776X/2004年10月

CからC++に移行する人向けの本です.入門書を読んでC++をはじめたはいいが, どこが悪いか誰も指摘してくれない!という状況で困っているひとに読んでもらいたい本です(経験者は語る). 内容は,C++のソースを書く導入部から,クラスの作り方,参照渡し,例外などをカバーしています. ただし,この本だけでC++の勉強をするのは厳しいので別途入門書を用意する必要があります. 本書はC++をはじめたC経験者づまづきそうな場所について配慮がなされていて, 各トピックの構成は以下のようになっています.

1.問題(バグ・コンパイルエラー)のあるプログラムの提示
2.問題点はどこにあるのか?
3.指南(解説)
4.まとめ
5.修正後のプログラム

いわゆる「反面教師」方式の構成ですが,"まとめ"のおかげで詳しい解説を読まずとも 要点をおさえることができます.個人的にプログラミング本らしくないカラフルな装丁が好きです.C言語版もあります.


C++美しいプログラミング見本帖(Amazon)

実用性のある入門書としては,次の本を推薦します.

新・C言語入門 シニア編
林晴比古(著)/ソフトバンクパブリッシング/ISBN:4797325623/2004年02月

新C++言語入門 シニア編〈上〉基本機能
林晴比古(著)/ソフトバンクパブリッシング/ISBN:4797316608/2001年05月

新C++言語入門 シニア編〈下〉クラス機能
林晴比古(著)/ソフトバンクパブリッシング/ISBN:4797316616/2001年05月

初心者に何を薦めるかについてはいろんな意見があるようですが,僕は「林晴比古さん」派です (対抗馬としては,マナちゃんこと高橋麻奈さんの「やさしい」シリーズですかね). 隅々まで書かれていて,見やすく,そしてわかりやすいので好きです. ビギナー編やスーパービギナー編などもありますが, C言語をある程度かじった人であれば臆せずシニア編を買ってもいいと思います. 結構高いですので,本屋で手にとってみて気に入ったら買ってみてください.

おわりに

長々と書いてしまいましたが,参考になりましたでしょうか? ここに書いた内容の半分は,僕がここ1〜2年で本やインターネットを頼りに独学で学んだこと, そして大学で後輩の指導にあたっているときに気づいたことです. 学校の授業ではif文やfor文という必要最低限のことしか教えてくれず,マクロ定義のことなんか誰も教えてくれませんでした. プログラミング上達の鍵は,本を読むことよりもダメだしをしてくれる誰かを見つけること, そしてうまい人のプログラムを読んでテクを盗むことです. 気づいたことがあれば適宜追加していこうと思います.今回はこのへんで!

参考文献

[1]藤原博文:『改訂新版 Cプログラミング診断室』,技術評論社,2003年
[2]真紀俊男:『プログラミングの禁じ手Web版 C言語編』,C MAGAZINE,ソフトバンクパブリッシング
[3]塚越一雄:『決定版はじめてのC++』,技術評論社,1999年