初級プログラマーを卒業したい人のために
計算機序論II,
www.kameda-lab.org
2005/09/22
■ 一般的な心掛け
これが最も重要である.そのためには,プログラムを書く前に,何を
計算するのか,また,どのように計算するのかをあらかじめ良く考えてお
くことが必要である.後述するように,流れ図や構成図を書くのも有効で
ある.
次に,わかりやすいプログラムを書くことを心掛けよう.他人にわか
りにくいプログラムは,結局,自分にもわかりにくい.プログラムを書い
た瞬間はわかっているつもりでも,時間が経てば経つほど,わけがわから
なくなってしまうのである.
- 他人にもわかりやすいプログラムを書く
- あとで変更しやすいプログラムを書く
- デバッグしやすいプログラムを書く
また,C言語は(きめ細かい処理ができるという点で)高機能である(※).
記憶領域や実行処理に関して,プログラム中で細かく指定できる(または,
指定しなければならない).そのため,以下のことに気をつけないと,わ
けのわからないバグをいっぱい入れ込んでしまうことになる.
- できるだけ変数や関数を構造化する
- メモリの管理には細心の注意を払う
- BASICやFORTRANの流儀は忘れよう(C言語では悪いスタイルの代表と
もなる)
※) 見方を変えると,ユーザにこのような細かい記述を強いるのは低
レベルのプログラミング言語だと言えるかもしれない.JAVAなどに比べて,
細かい制御が可能である反面,いちいちプログラムとして細かく指定しな
ければならないことも多い.
■ 具体的なアドバイス
□ 一般的なことから
○ いきなり書き始めない.流れ図や構成図を書いてみる
当然のことであるが,良く考えないでいきなり書き始めたプログラム
はすぐにグチャグチャになり,たいていは破綻する.まずは,「流れ図」
や「構成図」を書きながらプログラムの全体像を考えよう.
流れ図として,「フローチャート」と呼ばれる書式があるが,フロー
チャートを徹底して使うことには賛否両論があるため,この授業では,フ
ローチャートを毎回書くことは要求しない.ただし,それがどのようなも
のかを知っておくことは重要である.フローチャートの説明は[こちら].
関数どうしがどのように関係しているか,どのような入出力をとるか
等は常に分かりやすい形で整理しておく必要がある.なぐり書きでも良い
から,プログラム構成図として書いておけば,設計やデバッグに役に立つ.
構成図にはあまり決まった書式がない.
○ 注釈をつけよう.また,変数名や関数名も注釈の一種である.
C言語では,言語の性質上,書かれたプログラムからその実行内容を理
解するために,多大な労力が必要となる.他人に渡さない場合でも,自分
で思い出したり,デバッグするために,注釈は必須だ.
注釈と同じ意味で重要なのは,変数名や関数名である.長さを気にせ
ず,分かりやすい変数名を付けよう(※).タイプするのがめんどくさいか
ら短い名前にするなどはもってのほかである.そのくらい苦にならないぐ
らいのタイピング技術は身につけておこう.一週間も練習すれば,すぐに
速くなる.
また,その意味がわかりやすいように,大文字・小文字の区別を
利用するのも一法である。(必須ではないが,良い目安にはなる).
マクロ名,パラメータ名: 全部大文字
局所変数名: 全部小文字
大域変数名: 特に決まりはないが,頭に"the"を付けるなど,局所変数と区別
できるようにすると良い.例,int theXsize, theYsize;
関数名: 全部小文字または単語の先頭文字だけ大文字
複数の単語を組み合わせる場合には,'_'(アンダースコア)を入れるか,また
は,単語の先頭文字を大文字にする場合が多い.
例)
target_position
targetPosition
TargetPosition
※) 良く見掛けるサンプルとして,昔(メモリや表示デバイスが非力だっ
た時代)のプログラミングスタイルそのまま,無理に8文字以内の変数名を
使っているものがある.例えば,file_count_in_use を flcntu 等とする.
これでは,何の変数なのか一目ではわからない.困ったことだが,これに
限らず,今では良くない(とされている)サンプルプログラムを載せている
教科書はちまたに溢れている.
-----------------------------------
悪い例
-----------------------------------
int f1(int a) {
if (a < 0) {
printf("Input must not be negative! (%d is invalid)", a);
exit (1);
}
if (a == 0) {
return 1;
} else {
return (a * f1(a-1));
}
}
-----------------------------------
関数名・変数名を改良
-----------------------------------
int kaijyou(int seisuu) {
if (seisuu < 0) {
printf("Input must not be negative! (%d is invalid)", seisuu);
exit (1);
}
if (seisuu == 0) {
return 1;
} else {
return (seisuu * kaijyou(seisuu-1));
}
}
-----------------------------------
注釈を付加
-----------------------------------
// 階乗を求める関数: 引数は自然数または0をとる
int kaijyou(int seisuu) {
if (seisuu < 0) {
// 引数が負ならば警告をプリントして終わる
printf("Input must not be negative! (%d is invalid)", seisuu);
exit (1);
}
if (seisuu == 0) {
// 0! = 1
return 1;
} else {
// seisuu! = seisuu * (seisuu - 1)!
return (seisuu * kaijyou(seisuu-1));
}
}
□ 変数と関数の扱いについて
○ 大域変数の使用は必要最小限にとどめよう
個々の関数の中で局所変数の宣言をするのがめんどくさい等,とんで
もない理由で大域変数を使う人が多い.プログラムの見通しを良くし,デ
バッグを簡単にするためには,大域変数を極力使わないようにするのが正
しい.気持としては,以下の優先順位で変数のタイプを考えよう.
局所変数 > 関数内での static 変数 >> ファイルにまたがった大域変数 (extern宣言) ≧ ファイル内での大域変数
------------------------------------
悪い例 (もっとも初歩的な誤り)
------------------------------------
int i;
int main() {
for (i=0; i < 100; i++){
inner_function(...);
}
}
int inner_function() {
for (i=0; i < 1000; i++){
inner_function2(...);
}
}
....
------------------------------------
□ 関数の長さ,モジュール性
○ 関数は短くしよう(特に,長いmain関数は諸悪の根元)
関数はできるだけ短くする方が良い.一つの関数の中でたくさんの処
理を書くと,制御構造がわかりにくくなり,プログラムの見通しが非常に
悪くなる.また,デバッガを使う際にも,関数単位でデバッグすると簡単
なことが多いため,短い関数の集まりにしておくと有利である.さらに,
以下の点も大きな理由となる.
○ 共通部分を洗い出して,できるだけモジュール化しよう.
ほとんど似た処理を行う部分があちこちに散らばっていることがある.
ちょっとした変更を行ったり,バグの修正をするときなどに,あちこちを
修正しなければならないことになる.このような部分を洗い出して,各々
を共通の小さな関数として外部に取りだそう.このようにモジュール化す
ると,修正は一カ所で良いことになる.また,効率化を図る場合にも,何
回も呼ばれる箇所を重点的に調べれば良いので,考えやすい.
------------------------------------
悪い例
------------------------------------
void draw_all () {
...
switch (flag) {
case 1:
for (...){
... //xを1づつ増やしながら計算
break;
}
case 2:
for (...){
... //yを1づつ増やしながら計算
break;
}
case 3:
for (...){
... //x, yを1づつ増やしながら計算
break;
}
case 4:
for (...){
... //xを1, yを-1づつ増やしながら計算
break;
}
....
}
------------------------------------
改善例1
------------------------------------
void draw_all () {
....
switch (flag) {
case 1:
dx=1, dy=0;
break;
case 2:
dx=0, dy=1;
break;
case 3:
dx=dy=1;
break;
case 4:
dx=1, dy=-1;
break;
....
}
draw_inner(dx, dy);
}
// xとyをdx, dyづつ増やしながら計算
void draw_inner (int dx, int dy) {
....
for ( ... ) {
....
}
}
------------------------------------
改善例2
------------------------------------
void draw_all () {
....
switch (flag) {
case 1:
draw_inner(1, 0);
break;
case 2:
draw_inner(0, 1);
break;
case 3:
draw_inner(1, 1);
break;
case 4:
draw_inner(1, -1);
break;
....
}
}
// xとyをdx, dyづつ増やしながら計算
void draw_inner (int dx, int dy) {
....
for ( ... ) {
....
}
}
------------------------------------
□ C言語を使う上での注意
○ 構造体をうまく使おう
構造体の便利さは言うまでもない.強く関係する変数をまとめて扱う
ことによるメリットは大きいため,普段から,大いに利用しよう.ただし,
ここでは,普段はあまり説明されない側面から,さらにその利用を促す.
関数に多くの引数を渡さなければならないときがある.例えば,線分
を一本引くだけでも,本当は,始点,終点,色,太さ,終点の形,点線か
どうかの他,様々な設定が必要になる.このようなものを一つ一つ引数と
して与えるのは面倒である.一つの方法としては,大域変数として値を入
れておく方法もあるが,初級・中級プログラマにはこの方法は勧められな
い.そこで,構造体を使うメリットが出てくる.
例えば,皆が使っているXウィンドウシステムのライブラリXlibでは,
変数の値やポインタをgraphics context(gc)という構造体のメンバ変数と
して格納している.描画などの関数では必ずgcを引数としてとることによっ
て,種々の値(例えば,線の太さ等)をそこから知ることができる.
プログラミングの際にはこの構造体を新しく作ったり,既にあるものをコ
ピーした後,変更したい部分のみに新しい値を代入して,使えば良い.
(参考) man XDrawLine としてみよう
/*
* Data structure for setting graphics context.
*/
typedef struct {
int function; /* logical operation */
unsigned long plane_mask;/* plane mask */
unsigned long foreground;/* foreground pixel */
unsigned long background;/* background pixel */
int line_width; /* line width */
int line_style; /* LineSolid, LineOnOffDash, LineDoubleDash */
int cap_style; /* CapNotLast, CapButt,
CapRound, CapProjecting */
int join_style; /* JoinMiter, JoinRound, JoinBevel */
int fill_style; /* FillSolid, FillTiled,
FillStippled, FillOpaeueStippled */
int fill_rule; /* EvenOddRule, WindingRule */
int arc_mode; /* ArcChord, ArcPieSlice */
Pixmap tile; /* tile pixmap for tiling operations */
Pixmap stipple; /* stipple 1 plane pixmap for stipping */
int ts_x_origin; /* offset for tile or stipple operations */
int ts_y_origin;
Font font; /* default text font for text operations */
int subwindow_mode; /* ClipByChildren, IncludeInferiors */
Bool graphics_exposures;/* boolean, should exposures be generated */
int clip_x_origin; /* origin for clipping */
int clip_y_origin;
Pixmap clip_mask; /* bitmap clipping; other calls for rects */
int dash_offset; /* patterned/dashed line information */
char dashes;
} XGCValues;
○ マクロを効果的に使おう
プログラムを読みやすくするマクロは積極的に使おう.パラメータの
値を定義しておいたり,簡単な計算を簡潔に記述したりすると,プログラ
ムの可読性が格段に上がる.計算速度を速くするために関数でなくマクロ
を使う場合もあるが,よほど単純なものでない限り,マクロで何もかも書
くのは難しい(※).マクロを使ったためにバグ取りが非常に難しくなる場
合もあるので,注意すること.
(※) ここでは説明しないが,inline関数を使う手がある.
------------------------------------
例
------------------------------------
...
#define XSIZE 512
#define YSIZE 512
...
#define PRINT_INT(s) printf("The value of " #s " is : %d\n", s)
....
int hoge = 5;
....
draw_rectangle(0, 0, XSIZE, YSIZE);
draw_rectangle(0, 0, XSIZE/2, YSIZE/2);
draw_rectangle(0, 0, XSIZE/4, YSIZE/4);
....
PRINT_INT(hoge);
....
実行--> The value of hoge is : 5
------------------------------------
(参考) 間違った使い方:
------------------------------------
// この定義自体は良く使われる
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
// この使い方は間違っている.なぜか?
MAX(x++, y);
------------------------------------
○ gotoは使わないようにしよう
C言語は豊富な制御構造を持っているが,大抵の場合は,for, while,
do 等を使えばきれいに処理の流れを記述できるはずである.多少複雑な
ことがしたい場合には,それらの制御構造の中でbreak, continue等の使
用を考えよ.これらは構造化されているため,プログラムの流れが比較的
わかりやすい.それに対し,gotoを使うと,どのような条件の時にどこが
実行されるか,大変わかりにくいものになる.非常に特殊な処理を考える
のでない限り,エラーが起きて,それを処理させる部分だけにとどめてお
こう.
○ 可能な限り,mallocした関数でfreeしよう
メモリを確保したまま解放し忘れることがある.少量であれば特に問
題はないが(プログラム終了時に解放される),大量の繰り返しループの内
側でこのようなことをやってしまうと,プログラムがメモリを食い尽くし
て,異常終了する.そのためには,できるだけ,mallocしたのと同じ関数
内でfreeするように心掛ければ良い.
○ usleepを使おう(意味もないループで時間かせぎをしない)
描画やキーボード入力を遅くしたいときに,意味のない計算をさせて
時間かせぎをさせる方法がある.しかし,これをやると,CPUがその計算
に長時間占有されて,ネットワークやファイル入出力などの処理にもしわ
寄せが来るため,極力避けるべきである.また,CPUの速さが変わった場
合に,無駄な計算の量を変えなければならないため,いちいち手間がかか
るという欠点もある.
そのため,時間通りにプログラムをコントロールしたり処理を中断さ
せたい場合には,組み込み関数 usleep を使おう.一応,1μsec(1マイク
ロセカンド)単位で時間を指定できるが,実際に有効なのは10msec(10ミリ
セカンド)ぐらいの単位からである(※).
※) これはOSの割り込み(コンテクストスイッチ)の間隔に依存する.
興味のある人は詳しく調べてみよう.
○ snprintf, sscanf を使おう
C言語には文字列の処理をする関数がいくつかあるが,(慣れても)扱い
がめんどくさい代物である.効率は多少悪くなるが,snprintf, sscanf等
を使い,バグの入りにくいプログラムにしよう.
ちなみにsprintf, strcpy, getsなどはバグ(セキュリティホール)の温床
になるので、使用しないほうがよい(理由はmanを参照すればわかるが
どうしても分からない人はTAに聞いてみよう)。
------------------------
ややこしい例(かつセキュリティホールになりかねない)
------------------------
// 2つの文字列で"&"を挟んだ文字列を作る (但し,128文字以内(※)とする)
// e.g., string_append("AAAA", "BBBB") --> "AAAA&BBBB"
.....
char *string1, char *string2;
int length1;
char string3[128];
....
length1 = strlen(string1);
strcpy(string3, string1);
strcpy(&string3[length1], "&");
strcpy(&string3[length1+1], string2);
....
------------------------
こんなにわかりやすくなる(かつ安全)
------------------------
// 2つの文字列をくっつけた文字列を返す
....
#define MAXSTRINGLENGTH 128
char *string1, char *string2;
char string3[MAXSTRINGLENGTH];
....
snprintf(string3, MAXSTRINGLENGTH, "%s&%s", string1, string2);
....
------------------------
※) 128文字以上の文字列を扱えるようにするためには,可変長の
string3を作れば良い(ここでは説明しないが,難しくないので,各自考え
てみよ).
Yoshinari Kameda: 2004/09/05-
Yuichi Nakamura: Tue Aug 12 11:33:52 JST 2003