L.W.Cannon R.A.Elliot L.W.Kirchhoff J.H.Miller J.M.Milner R.W.Mitze E.P.Schan N.O.Whittingson
Bell Labs
Henry Spencer
Zoology Computer System University of Toronto
David Keppel
EECS, UC Barkeley CS&E, University of Washington
Mark Brader
SoftQuad Incorporated Toronto
豊田 英司 (訳)
東京大学大学院数理科学研究科
向 修一 (訳) 浅沼 伸彦 (訳)
東京大学理学部
この文書は the Indian Hill C Style and Coding Standards を アップデートしたもので、最後の 3 著者 (Spencer, Keppel, Brader) による 変更をくわえたものである。 C プログラムのコーディング基準が記述されている。 本文書の目的はコーディングスタイルを記述することであり、 機能による組織化 functional organization は範囲外である。
この文書は、 AT&Tのインディアンヒル・コミュニティのための C言語についての共通のコーディングに関する規範と勧告を 作成するために、 インディアンヒル・ラボによって組織された委員会の 文書を手直ししたものである。 この文書の対象とするのはCのコーディング・スタイルである。 よいスタイルは、設計を一貫性のあるものにしたり、移植性を高めたり、 誤りを少なくしたりするために役立つであろう。 This work does not cover functional organization, or general プログラムの機能的構成法や、 goto の使用をめぐるような雑多な問題は扱わない。 個々の部分はシステム固有の事情に左右されながらも、 我々1は C のスタイルについて議論した過去の文献 [1,6,8] を、 C を使ったあらゆるプロジェクトにふさわしいような 統一した規範にまとめようとした。 やむをえないことであるが、この規範はすべての状況をカバーするものにはなっていない。 経験と知識に裏打ちされた判断力のほうがより重要である。 普通でない状況に遭遇したプログラマは、 経験豊かなプログラマに相談するか、 経験豊かなプログラマの書いたコード (このルールに従っているものが望ましい) を研究すべきである。
この文書に書かれている規範は おのずといつでも要求されるようなものではなく、 個々の団体やグループでプログラムの一環として この文書の一部なり全体なりを採用することができる、 というようなものである。 したがって、あなたの団体で他の人々が似たようなスタイルで書くというのは ありそうなことになる。 この規範の目標は究極的には移植性を高め、 メンテナンスの手数を減らし、そして何よりプログラムを分かりやすくする ことである。
ここで選択されたスタイルには、 恣意的に決めているようなところもいくらかある。 複数のスタイルが混在しているコードは、 悪いスタイルよりも取り扱いにくい。 すでに存在するコードを修正するときは、 この文書の勧告をただ機械的に適用するよりは、 そのコードのスタイル(字下げ、空白の入れ方、コメントの入れ方、 命名法などの慣習)にあわせたほうがよい。
「明快なのがプロフェッショナルである。明快でないのは素人だ」 --- Sir Ernest Gowers.
footnote:
- 1.
- この文書の主張は著者のすべての意見を反映しているわけではない。 これはいまだに進化している文書である。 コメントや提案を pardo@cs.washington.edu または {rutgers,cornell,ucsd,ubc-cs,tektronix}!uw-beaver!june!pardo に送ってほしい。
ひとつのソースファイルはいくつかの空行で区切られるべき いくつかのセクションからなる。 ソースファイルの行数の最大値に制限はないが、 1000 行を越えるファイルは扱いにくいものである。 エディタが作業ファイルを作れなくなるかもしれないし、 コンパイルがおそくなるかもしれない。 たとえば何行にもわたる星印2 は、スクロールするのにかかる 時間に比べてもたいした情報を与えないので、勧められない。 79桁より長い行はすべての端末でうまく表示できるわけではないので、 できるならば避けるべきである。 インデントが深くなって長すぎる行ができてしまうのは、 プログラムの構成が悪いせいである。
footnote:
- 2.
- 訳注: 星印とはコメントのこと。'*' をコメントの行頭にかくように勧めているため。
ファイル名はピリオドを含まない base name と、 ピリオドおよびサフィックス3 からなる。 サフィックスはなくてもよい。 base name のはじめの文字はアルファベット、 その後の文字は(ピリオド以外は)数字かアルファベットの小文字に すべきである。 Base name は8文字またはそれ以下、 サフィックスは3文字またはそれ以下4にすべきである。 この規則はプログラムが使ったり作ったりするファイルにも 適用される(例:"rogue.sav")。
いくつかのコンパイラやツールはファイル名のサフィックスが 決まったものになっていないと動作しない5:
- *
- Cのソースファイルは .c でなければならない
- *
- アセンブラのソースファイルは .s でなければならない
- *
- リロケータブルオブジェクトファイルは .o
- *
- インクルードファイルは .h 。 他言語を使う環境では言語名と .h を使うほうがよいであろう (例: "foo.c.h" または "foo.ch")
- *
- Yacc のソースファイルは .y
- *
- Lex のソースファイルは .l
C++ はコンパイラに依存したサフィックスをつけないといけない: .c, ..c, .cc, .c.c, .C など。C のプログラムは大部分 C++ のプログラムでもあるので、 この問題には明快な答はない。
最後に、 make のためには "Makefile" ("makefile" ではなく) を、 ディレクトリやディレクトリツリーの概観を与えるためには "README" というファイル名を使うのが慣例である。
footnote:
- 3.
- 訳注: サフィックスは MS-DOS や Windows では拡張子 extension と呼ばれている。
- 4.
- 訳注: ピリオドを入れて数える流儀では4文字以下。
この直後の原文の注釈: 8 + 1 + 3 に RCS による ",v" をつけると 14 文字なので Version 7 ファイルシステムに収まる。 MS-DOS では 8 + "." + 3 文字しか許されない。
- 5.
- 訳注: これは UNIX に依存した話である。 MS-DOS や Windows では通常アセンブラのソースには .asm を、リロケータブルオブジェクトファイルには .obj を用いる。
プログラムファイルのなかのセクションの配置として、 次のようなものを提案する:
- 1.
- ファイルのはじめには、このファイルには何が書かれているのかを記述する プロローグをおく。 オブジェクト(関数や外部変数の宣言・定義など)の名前の羅列より、 このファイルの中のオブジェクトの目的の記述のほうが役に立つ。 プロローグには作者、バージョン管理情報、レファレンスなどが 入ってもよい。
- 2.
- すべてのインクルードファイルの読み込みは、 この次にくるべきである。 自明でない理由によっってインクルードファイルを使うときは、 その理由をコメントすべきである。 多くの場合、 stdio.h のようなシステム・インクルードファイルは 他のインクルードファイルよりも先に読み込まなければならない。
- 3.
- ファイル全体に適用されるすべての #define と typedef が次に来る。 通常は「定数」マクロをはじめにおき、 ついで「関数」マクロ、 そして typedef および enum をおく。
- 4.
- 次にグローバル(外部)データ宣言がくる。 通常の順番は: extern 宣言、static ではないグローバル変数、static なグローバル変数。 もしひとつのグローバル変数(たとえばフラグ)のとるべき値として、 一群の #define を作るときは、 その #define は該当するグローバル変数のデータ宣言の直後に書くか、 構造体のメンバーの宣言の直後に 1レベル深くインデントして書くべき6である。
- 5.
- 関数は最後に来る。 関数はなにか意味のある順番に配置するべきである。 似た関数はまとめて置くべきである。 「普遍的な順に」(抽象化の度合いの近いものをまとめて) 並べるほうが、 「深さの順に」(なるべくその関数を呼び出しているところの近くに) 並べるよりも好まれる。 もし本質的に無関係な関数を大量に定義するときは、 アルファベット順を検討しよう。
footnote:
- 6.
- 訳注: 4 節参照。
ヘッダファイルとは、Cのプリプロセッサによってコンパイルの前に 他のファイルから読み込まれるファイルのことである。 stdio.h のようないくつかのものはシステムレベルで定義され、 標準入出力ライブラリを使うすべてのプログラムに読み込まれる。 ヘッダファイルは機能別に構成されなければならない、すなわち、 異なったサブシステムの宣言は別のヘッダファイルに入れるべきである。 また、コードをある機種から他の機種に移植するときに変更が予想されるような 宣言は、[機種に依存しないものとは] 別のヘッダファイルにすべきである。
ライブラリのヘッダファイルと同じ名前のヘッダファイルを作るのは避けること。 もし #include"math.h" がカレントディレクトリで そのファイルを見つけられなければ、 システムの math.h が読み込まれてしまう。 そのようなことが起こるのを期待してそんな記述をするのならばコメントすること。 ヘッダファイルの位置を絶対パスで指定してはならない。 [カレントディレクトリではなく] 指定した場所から読み込ませたいならば <name> 構文またはカレントディレクトリからの相対パス指定を使え。 自分で作ったライブラリのヘッダファイルを読み込ませるのは、 Cコンパイラの「インクルード・パス」オプション (多くのシステムでは -I)を使うのがもっともよい。 こうしておけば、ソースファイルを書き直すことなくディレクトリ構造を 再編成することができる。
関数や外部変数を宣言するヘッダファイルは、 その関数や変数を定義するファイルでもインクルードすべきである。 こうしておけばコンパイラが常に型チェックができるので、 外部宣言と定義が常に矛盾しないようにできる。
[どのモジュールからもその変数が見えるようにと] 外部変数の [宣言ではなく] 定義をヘッダファイルで行うのはだいたいいつも悪いアイデアだ7。 たいがいそれはコードのファイル分割がうまくできていない証拠である。また、typedef や初期化付きデータ定義のようなオブジェクトは、 1度のコンパイルに2回現れるとうまくいかない。 いくつかのシステムでは extern をつけていない初期化なしのデータ宣言を 繰り返しても問題になる。 インクルードファイルがネストされていると宣言が繰り返される ことがありえる。これはコンパイルの失敗につながるだろう。
ヘッダファイルはネストしてはならない。 したがってヘッダファイルのプロローグでは、 このヘッダが機能するために他に #include しなければならない ファイルをあげておくべきである。 いくつかのソースファイルが多数の同じヘッダを インクルードしなければならないというような極端な場合は、 共通する #include をひとつのインクルードファイルにまとめるのも よいだろう。
ヘッダの二重読み込みが起こらないように各々の .h ファイルを次のように書くのが普通である。
この手の二重読み込み防止機構を、 特にネストしたヘッダを作るために利用してはならない。#ifndef EXAMPLE_H #define EXAMPLE_H ... /* example.hの内容 */ #endif /* EXAMPLE_H */
footnote:
- 7.
- 訳注: この場合定義はどれかひとつのファイルにして、 ヘッダでは extern 宣言だけを行うべきである。
プログラム全体についての問題や「より大きな描像」の文書のために、 "README"というファイルをおくのが普通である。 たとえば、条件コンパイルのフラグとその意味の一覧や、 機種依存個所の一覧を書いておくのがふつうである。
「もしコードとコメントが一致しないならば、きっと両方間違いだろう」 --- Norm Schryer
コメントは 何が 起こっているのか、 どうやって 実現しているのか、 パラメータは何を意味するのか、 どのグローバル変数を参照し変更するのか、 そしてすべての制限とバグを記述すべきである。 しかしコードから明らかなことは書かないこと。 そんな情報はコードが変わればすぐ意味を失ってしまう。 コードと矛盾するコメントは書かないほうがましである。 短いコメントは 「合計を n で割る」のように どうやって ではなく、 「平均の算出」のように 何を しているのかを記述するものにすること。 C はアセンブラではない。 だから1行ごとに細々と書き込むよりは、 3-10行程度のまとまりごとに何をしているかをコメントしたほうが有益である。
見苦しいコード [を書かねばならないような場合、これ] を正当化するようなコメントを書かねばならない。 この正当化というのは、見苦しくないように書くとこういう悪いことがある、 というようなものでなければならない。 ただ実行速度が速くなるから、というだけでは 凝った技を使う8 充分な理由にはならない。 そうしない場合よりどれだけ効率がよくなったかを 明示 しなければならない。 コメントは [この修正で生じた] 好ましくない挙動や、 どうしてこれが [その損失と比べても]「よい」修正と 呼べるのかを説明しなければならない。
データ構造、アルゴリズム等を記述するコメントはブロックコメント に書くべきである。 ブロックコメントは行頭に /* を書いてはじめ、 途中の行はすべて2桁目に * を書き、 最後は 2-3 桁目に */ で終わらせるべきである。 または途中の行を行頭の ** ではじめ、 最後は行頭に */ としてもよい。
/* * これはブロックコメントである。 * コメントの本文はタブや空白で同じように字下げすること。 * 最初と最後の行には他に何も書かないこと。 */
/* ** こうしてもよい */
grep '^.\*' とやるとソースファイルからブロックコメントだけが 抜き出せることにも注目されたし9。 すでに [別のファイルに] 書いた議論 [を挿入した場合] や著作権表示のように、非常に長いブロックコメントは、 途中の行の2桁目の * を省略してもよい。 関数の中のブロックコメントはふさわしい位置に、 その対象となるコードと同じ字下げをして書き込むこと。 一行だけのコメントはその次の行と同じ字下げで書くこと。
非常に短いコメントをコードと同じ行に書いてもよい。 この場合はタブで遠く離して書くこと。 もしコードのブロックに複数のそういったコメントがあるなら、 同じ字下げ位置に揃えること。if (argc > 1) { /* コマンド行から入力ファイル名を受け取る */ if (freopen(argv[1], "r", stdin) == NULL) { perror(argv[1]); } }
if (a == EXCEPTION) { b = TRUE; /* 特別な場合 */ } else { b = isprime(a); /* a が奇数の場合のみ動作 */ }
footnote:
- 8.
- 原文: hack
- 9.
- いくつかのプログラム開発支援パッケージでは、 コメント行の内容に応じて他の文字を目印に使っていることがある。 特に関数の前のコメントで `-' を付けた行は関数の目的を 1行で要約しているものと扱われることが多い。
グローバルな宣言は、1行目からはじめること。 すべての外部データ宣言は extern キーワードを前につけなければならない。 もし外部変数が明示的サイズで定義された配列ならば、 サイズが常に配列から読み取れる (例: '\0' で終わっているリードオンリーな文字列定数)のでない限り、 extern 宣言の際にもそのサイズを明示しなければならない。 サイズをもう一度書くのは、他人が書いたコードから部品を切り取って 使うときに特に役に立つ。
ポインタを示す `*' は、型の次ではなく 変数名の前に書くこと。
ではなくchar* s, t, u;
前者では `,t' `u' がポインタのように見えるが、そうではない。char *s, *t, *u;
関係のない宣言は、たとえ同じ型でも、別の行に書くべきである。 その名前から意味が明白な定数マクロ以外は、 宣言されたオブジェクトには必ず役割をコメントすること。 変数名、初期化の数値[あれば]、コメントは同じ位置に揃うように タブを入れること。空白よりはタブが望ましい。 構造体や共用体の宣言は、 各要素を各々1行に書き、内容を表すコメントを付けること。 `{' 記号は構造体タグと同じ行に[1つ空白をあけて]、 `}' 記号は独立の行の1桁目に書くこと。
これらの #define は、構造体の中の type の宣言の直後におくことがある。 この場合、`#'のあとに充分タブを用いて、 構造体のメンバーの宣言より深く字下げする。/* * 訳注: type という名前を見て恐怖に駆られる Pascal プログラマへ: * C では type は予約語でも何でもない。 */ struct boat { int wllength; /* water line length in metres */ int type; /* 下を見よ */ long sailarea; /* sail area in sqare mm */ }/* defines for boat.type */ #define KETCH (1) #define YAWL (2) #define SLOOP (3) #define SQRIG (4) #define MOTOR (5)
訳注: こういう意味だろう:
struct boat { int wllength; /* water line length in metres */ int type; # define KETCH (1) # define YAWL (2) # define SLOOP (3) # define SQRIG (4) # define MOTOR (5) long sailarea; /* sail area in sqare mm */ }
しかし具体的な数値が重要ではない場合(そうでなくてもではあるが)、 enum 型を用いるほうがよい。
enum bt { KETCH=1, YAWL, SLOOP, SQRIG, MOTOR }; struct boat { int wllength; /* water line length in metres */ enum bt type; /* 船の型 */ long sailarea; /* sail area in sqare mm */ }
初期値が重要な意味を持つような場合は かならず 明示的に 初期化すること。 Cのデフォルトの0による初期化を期待して何もしないならば、 最低限そのことはコメントしておくこと。
初期値が重要な意味を持つような場合は 必ず明示的な初期化をすること。 Cのデフォルトの 0 による初期化を期待して何もしないならば、 最低限そのことはコメントしておくこと。 空の大括弧 ``{}'', による初期化をしてはいけない。 構造体配列の初期化では構造体毎に大括弧でくるむ。 long 型の変数の初期値は、大文字の L で型を明示すること。 小文字では、たとえば ``2l'' (long の「に」)と ``21'', (「にじゅういち」)とは紛らわしい。
int x = 1; char *msg = "message"; struct boat winner[] = { { 40, YAWL, 6000000L }, { 28, MOTOR, 0L }, { 0 }, };
単体で完結していない、 大きなプログラムの構成部品のファイルでは、変数や関数はなるべく static にして、ファイルに固有の、 ローカルなものにする。 特に変数は、余程の理由がない限り外部からは見えないようにする。 逆に他のファイルに含まれる変数を参照するときは、ファイル名も含めて コメントしておく。もしデバッグの際に、見たい static なものを デバッガが隠してしまうようなら、それらを STATIC と宣言して、 適当に #define STATIC とすると良い。
非常に重要な型は、たとえただの整数型でも typedef してやると、 コードが読み易くなる(もちろんなんでもかんでもそうしたら めちゃくちゃだ!)。構造体を、宣言時に typedef すると 良いこともある。その時は型名は構造体名と同じにする。
typedef struct splodge_t { int sp_count; char *sp_name, *sp_alias; } splodge_t;
関数の返り値の型宣言は必ずする。可能ならばプロトタイプ宣言にしよう。 よくある間違いは、数学関係の外部関数を使うとき、 double 宣言を忘れというものだ。コンパイラは int を返すものと思い、無意味な浮動小数点のビット列ができることになる。
``C は、プログラマは常に正しいという思想のもとに設計されている。'' --- Michael DeCorte
関数の前にはブロックのコメント文を短く書き、 何をする関数なのかを、また分りにくい場合にはどう使うかも書くこと。 副作用や、自明でないデザインパターンを使った場合はそのことも書いたほう が良い。 コードから分かるようなことはいらない。
返り値の型は行頭から 1 タブ10 空けて書き、そのまま改行する。返り値が無い時は void11 を明示する。つまり、デフォルトのint は使わない。返り値の説明は 前述のブロックコメントの中に入れるか、短ければ型宣言の後にタブで区切って 書いても良い。関数名と引数のリストは行頭から書き、それだけで一行を使う。 返り値を渡すための引数は普通先にする (左に書く)。 パラメータの宣言、局所変数の宣言、 コードは全てタブで頭下げする。開き大括弧 ({) には一行を使い、行頭に置く。
全ての引数は型宣言する。デフォルトの int は使うな。 一般的には関数内で使われる全ての変数の説明を書くべきである。 これはブロックコメント文の中でも良いし、宣言の後に書いても良い (一つの変数に一行を使っているならば)。よくあるループカウンタの ``i'', 文字列ポインタの ``s'', 一文字を入れる int 型の ``c'' などは非明示でも 良いだろう。ある一群の関数の引数や局所変数が大体同じなら、 同じ名前にすれば分かりやすい。逆に、同じ名前で違う事をさせるのは、 関連性のある関数の間ではやめよう。引数の順番も似た様なものにしよう。
引数や局所変数の説明はタブを使って揃えると良い。局所変数の宣言と 関数のコード本体の間には空行を入れる。
引数の数が可変長の関数の定義、使用には気を付けなければならない。 というのは C の場合、可搬性の強い保証ができなくなるからである。 固定長の引数を取るインターフェースを作るか、どうしても可変長に したければライブラリマクロを使おう。
関数の中で、グローバルではない extern な変数や関数を使いたいときは、 逐一関数内で extern 宣言をすること。
より高レベルでの宣言をオーバーライドするような 局所変数の使い方はやめよう。特にネストされたブロックの中ではいけない。 C では全く合法的なことだけれども、いずれ問題を引き起こすかもしれない。 lint にも -h オプションつきでは怒られるだろう。
footnote:
- 10.
- タブとして 2/4/8 個のスペースを入れるエディタもある。 できれば本当のタブコードを使おう
- 11.
- void 型のないコンパイラでは #define void か #defin void int とすれば良い。
int i;main(){for(;i["]<i;++i){--i;}"];read('-'-'-',i+++"hell\
o, world!\n",'/'/'/'));}read(j,i,p){write(j/p+p,i---j,i/i);}
--- 「おぞましいコメント」、不明瞭な C コードコンテスト12 、1984 年
匿名希望
積極的に空白や空行を入れよう。インデントや空白はコードのブロック構造を 反映せねばならない。例をあげれば、関数のコードの終りと、次の関数のコメントの 間には最低 2 行の空行を入れる、等だ。
長い条件文は途中で改行する。
これは次の様に書換えるほうが良いだろう。if (foo->next==NULL && totalcount<needed && needed<=MAX_ALLOT && server_active(current_input)) { ...
同様に、複雑な for 文にも複数行を使った方が良い。if (foo->next == NULL && totalcount < needed && needed <= MAX_ALLOT && server_active(current_input)) { ...
特に ?: 演算子を使う時等、他の複雑な表現でも改行は必須。for (curr = *listp, trail = listp; curr != NULL; trail = &(curr->next), curr = curr->next ) { ...
予約語の後に、開括弧で始まる式が続くときは、間に空白を入れねばならない。 ただし、例外は sizeof 演算子で、このときは空白を入れてはいけない。 引数のリストでのコンマの後にも、空白を入れれば、 見易い。 しかしながら、引数のあるマクロ定義では、マクロ名と開括弧の間に 空白を入れてはいけない。C プリプロセッサが引数リストを認識しなくなる からだ。c = (a == b) ? d + f(a) : f(b) - d;
footnote:
- 12.
- 訳注:この大会は実在する。毎年 Usenix で行われている。
/* * 空が青いかどうかを、夜ではないことを調べて決定する。 * 問題点:時々しか正しく判定しない。 * FALSE を返すべきときに TRUE を返すことがある。 * 雲や蝕や日の長さを考慮すべし。 * その他:'hightime.c' の 'hour' を使用。 * 古いバージョンとの互換性を保つため int 型を返す。 */ int /* true or false */ skyblue() { extern int hour; /* 現在時刻 */return (hour >= MORNING && hour <= EVENING); }
/* * ポインタ nodep で示されるリンクリストの終要素を見付け、 * そのポインタを返す。 * 存在しなければ NULL を返す。 */ node_t * tail(nodep) node_t *nodep; /* リストの先頭へのポインタ */ { register node_t *np; /* NULL に先行 */ register node_t *lp; /* 一つ前を保存 */if (nodep == NULL) return (NULL); for (np = lp = nodep; np != NULL; lp = np, np = np->next) ; /* VOID */ return (lp); }
一行には一つのステートメントとするべきである。複数書く時は、 強い関連性があるときのみにする。
for や while ループのボディは空でも一行を使い、 コメントアウトして、空ボディは意図的で、コードが欠けている訳では ないことを示す。case FOO: oogle (zork); boogle (zork); break; case BAR: oogle (bork); boogle (zork); break; case BAZ: oogle (gork); boogle (bork); break;
while (*dest++ = *src++) ; /* VOID */
条件文で、非零であることのテストは省かない。つまり
よりもif (f())
とする方が良い。たとえ FAIL が、 C では false となる 0 であったとしてもである。 このように明示すれば、後で誰かが、「失敗の時は 0 ではなく -1 を返す ことにしよう」と決めたときに、問題が少ない。 更に、比較する値が変化しないときでも、明示するべき例もある。 つまり、 ``if (!(bufsize % sizeof(int)))'' よりも ``if ((bufsize % sizeof(int)) == 0)'' と書く方が良い。 このことで、本質的な条件がブーリアンではなく、数値的なることが 示されるからである。また、絶対に 条件を省いてはいけない場合として、 strcmp (等しいときに 0 を返す)の使用がある。 これは非常によくトラブルの原因になることを 知って欲しい。これに対するおすすめの解決策は、STREQ というマクロの使用である。if (f() != FAIL)
#define STREQ(a, b) (strcmp((a), (b)) == 0)
しかしながら、ノンゼロの明示的な比較は分岐、その他の関数・表現で、 以下の条件を満たすときは、しばしば省かれる。
``bool'' という型をグローバルな インクルードファイルで typedef するのはよくある。 特別に名前付けをすれば、可読性が飛躍的に向上する。たとえば
あるいはtypedef int bool; #define FALSE 0 #define TRUE 1
typedef enum { NO=0, YES } bool;
などとすれば良い。気を付けなければならないのは、 真として定義したもの (TRUE, YES...) と等しいかどうかを比較しては いけないことである。かわりに、「偽」と等しくないかどうかを 調べなければいけない。一般的には、関数は偽なら 0 を返すことに なっているが、真の時の返り値はノンゼロとしか 定義されていないからである。
ではなくif (func() == TRUE) { ...
とする。可能ならば、前述の例の様に、 関数や変数を、真偽が明瞭なような名前を再定義して、 比較がいらないようにすれば、なお良い。if (func() != FALSE) { ...
代入文の「埋め込み」が行なわれることがある。埋め込みがコンパクトさと 可読性の面から、最良である場合はある。
++ 、 -- 演算子は代入文の働きをするのでしばしば関数呼び出しで副作用を起こす。 ランタイムの性能向上の手段としての埋め込みも、悪い訳ではない。 しかし、作為的な埋め込みは、 引き換えにコードの維持を大変にし易いということはよく覚えておこう。 例えば、while ((c = getchar()) != EOF) { /* process the character */ }
をa = b + c; d = a + r;
とすれば 1 サイクル稼げるが、こうしてはいけない。長い目で見れば、 性能の差はコンパイラの最適化の進化により縮まるが、 手間の差は人間の記憶が薄れるにつれ広がるだけである。d = (a = b + c) + r;
goto 文は、コードの構造がしっかりしていればそう要らないし、 またあまり使うべきでない。 goto 文が便利なのは主に入れ子になった switch、 for、 while の内部から抜け出す時である。しかし、可能ならば 返り値が成功か失敗で示される関数で内部をばらして、goto を 避けるほうが良い。
goto の行き先のラベルには一行を使い、コードよりもタブを一つ左にする。 また使用に際してそのことを、 どうすると何が起るのかを含めてコメントアウトする (ブロックの前に書くのが良いだろう)。 Continue はループの最初の方に置き、乱用しない。 Break は比較的問題が少ない。for (...) { while (...) { ... if (disaster) goto error;} } ... error: /* 後処理 */
宣言がプロトタイプではない関数への引数は明示的にキャストせねばならない 時もある。例えば、関数が 32 ビットの long 期待しているところに 16 ビットの int を渡すと、スタックの配置が狂うかも知れない。 似た様な事例はポインタや、整数、浮動小数点型に関して起こる。
ブロック文とは大括弧 { } で囲まれた文である。 大括弧の清書法はいろいろある。 (あれば)自分の環境で一般的な方法にならえば良いし、 好きな方法を選んでも良い(一度決めたものをころころ変えてはいけない)。 他人のコードに手を加えるときは、絶対にそれに従うこと。
control { statement; statement; }
これは「K&R13 」流と呼ばれている。これからスタイルを確立する人は、これをお勧めする。K&R では if-else 文の else 、それに do-while 文の while は閉括弧と同じ行に書く。 他の流儀では、閉括弧に一行をとるものが多い。
ロック文内に幾つか(多過ぎず)ラベルがあるときは、 ラベルは別行立てにする。 switch 文において意図的に break を書かずに、次の case に制御を渡す時は、 その旨コメントしないと、後でコードの維持に泣くことになる。 lint 風に書くのが良いだろう。
switch (expr) { case ABC: case DEF: statement; break; case UVW: statement; /*FALLTHROUGH*/ case XYZ: statement; break; }
今の例で最後の break は厳密には不要だが、後々 case が足された時のためにつけておくべきである。 default は、もし使うならば最後に置く。このときは break はいらない。
If-else 文で、 if と else のどちらかでブロック文を使うなら両方ともブロック文にすること。 これを fully bracketed (あえて訳せば「完全括弧付け」) になっているという。
If-if-else 文が入れ子になっている時も、ブロック化をするべきである。 以下の例では、 (ex1) の後の文をブロック化しないと、意味が変ってしまう。if (expr) { statement; } else { statement; statement; }
if (ex1) { if (ex2) { func_a(); } } else { func_b(); }
Else if を使って繰り返し比較するものがある時は、 それを条件文の中のなるべく左に置く。
こうすれば一種の switch 文の拡張とも考えられる。 また、この様なインデントにより条件と処理が一対一であることが はっきりするので、ネストより優れている。if (STREQ (reply, "yes")) { /* yes に対する処理 */ ... } else if (STREQ (reply, "no")) { ... } else if (STREQ (reply, "maybe")) { ... } else { /* その他の時の処理 */ ... }
Do-while 文ではボディは常に大括弧で括る。
次のコードはちょっとあぶない。
CIRCUIT が定義されていない場合、 ``++i;'' は expr が偽の時のみしか実行されない! この例の教訓は、マクロ名を大文字とし、 コードを fully-bracketed にすることの意義深さにある。#ifdef CIRCUIT # define CLOSE_CIRCUIT(circno) { close_circ(circno); } #else # define CLOSE_CIRCUIT(circno) #endif... if (expr) statement; else CLOSE_CIRCUIT(x) ++i;
If 文の中で break, continue, goto, return を使って、制御を外に移してしまうことがある。 この時は else は書かず、後のコードもインデントしない。 それにより、それ以後はもはや条件判定と無関係なことがはっきりする。
if (level > limit) return (OVERFLOW) normal(); return (level);
footnote:
- 13.
- 訳注:Kernighan & Ritchie による巻末文献 [6] のこと
単項演算子はオペランドとの間に空白を入れてはいけない。 二項演算子は、 `.' と `->' を除いて、一般的には左右のオペランドと空白で分つ。 複雑な式は対応関係を目で追うのが難しいが、それでも 内側ではあまり空白を入れず、外にある演算子のまわりにはスペースを 入れるようにすれば、いくらか読み易い。
読みにくい式は、複数行にまたぐと良い。改行は、 その付近で最も優先順位の低い演算子の近くにする。 C ではどのような順で式が評価されるかについては曖昧なので、 適当に括弧を使おう(使いすぎると、今度は読みにくくなる。良く考えて書こう)。
カンマ演算子は for 文等で、複数の初期化や演算をするには有用 だが、普段はなるべく避ける。 また、例えば ?: 演算子のネストの様な、複雑な表現は誤りの元なので、できるだけ避ける。 ただし、マクロによっては(例えば getchar )、 ?: 演算子もカンマ演算子も有用な場合がある。 ?: 演算子では、論理式は括弧に入れ、返り値は二つとも同じ型にする。
もちろんプロジェクト毎に命名法は異なる。 ここでは一般的なことについて述べる。
グローバルなものには ( enumも含めて)、 どのモジュールに属するのか分るように 共通の接頭辞を付けるのが普通である。 あるいはひとまとめに構造体に入れても良い。 typedef で名前の後に ``_t'' と付けるのはよくある。
一般的なライブラリと競合するかも知れない様な名前は付けない。 システムによっては予想以上に多くのライブラリを インクルードするし、自分のコードも そのうち拡張することになるかも分らない。
数値定数をそのままコードに書いてはいけない。 その代わり #define を使って、コードが読み易くなるような、意味のある名前を付けてあげよう。 具体的な値を一箇所で定義するメリットは他にもある。 値を変更する必要のある時、一箇所だけ変えれば良いようにすれば、 特に大きなプログラムの場合、管理がずっと楽になる。 また、定まった離散的な値のみをとる変数は enum 型として 宣言すれば、型チェックが余分にできることが多いこともあり、良い。 もし値をコードに直截書くなら、最低限、コメントをして、 数値の由来が分かる様にしておこう。
定数はちゃんとその用法に合せて定義しよう。 例えば、float 型には 540 として暗にキャストさせるのではなく、初めから 540.0 と書く。 0 や 1 は何かの定義ではなく、それそのものが欲しい場合もある。 例えば
として for ループで配列をさわる。この 0 には全く問題はない。しかし、for (i = 0; i < ARYBOUND; i++)
というのはいけない。なぜなら front_door はポインタなので、比べるならば NULL とでなければいけないからである。 NULL は標準入出力ライブラリのヘッダファイル stdio.h や、最近は stdlib.h 等で定義される。 1 や 0 の様な単純な数値でも、 TRUE と FALSE で言い換えられる様な時は ( YES と NO でも良い。適宜都合良く)、こちらを使おう。door_t *front_door = opens(door[i], 7); if (front_door == 0) error("can't open %s\\n", door[i]);
文字定数は、数値ではなく、char のリテラルで定義する。 非表示文字は、移植性が落ちるのであまりよろしくないが、 (特に文字列の中で)どうしても必要ならば、 エスケープして、3 桁の 8 進数で書く。1 桁ではいけない。 例 '\007'。 とまれ、処理系に依存するコードだということを認識しなければ ならない。
マクロでは、定義式中に現われる引数をそれだけで括弧に入れないと、 複雑な式がマクロの引数として渡された時に、演算子の優先順位の関係で おかしな動作をすることがある。 引数関係の副作用の解決法は、副作用の無い様な式を引数にすること くらいである(何にせよ、悪いことではない)。 また、できればマクロで引数を評価するのは一回だけにしよう。 関数と厳密に動作が等しいマクロを書くのは不可能な場合もある。
getc や fgetc など関数でもあるマクロもある。 その様な場合、マクロの変更が自動的に関数に反映されるよう、 マクロは関数の実装として使うべきだろう。 マクロを関数に、或はその逆に置き換える時は注意が必要である。 引数は関数では値渡しだが、マクロでは置換である。 マクロを安心して使うには、定義に細心の注意が必要なのだ。
グローバルなもの(変数でも関数でもその他もろもろ)は 局所的にはオーバーライドされている可能性があるので、 マクロからの呼び出しは避ける。 引数の値が、直截代入したり、あるいは代入演算子の左辺に置かれる等して 変更される可能性のある時は、コメントすること。 ポインタの参照先に代入するのは構わない。 参照変数以外の引数は取らないマクロ、長いマクロ、関数の エイリアスのマクロには、定義時に空の引数リストを付ける。例:
#define OFF_A() (a_global+OFFSET) #define BORK() (zork()) #define SP3() if (b) { int x; av = f (&x); bv += x; }
マクロはを使えば関数呼出しと復帰にかかるオーバーヘッドを 節約できるが、マクロが長くなると相対的な節約量は小さい。 その時は関数にしよう。
マクロがセミコロンで終ることをコンパイラが約束していないと 困る場合もある。
もし、 SP3 呼出しの後のセミコロンを省くと、 else は知らぬ間に SP3 の定義にある if を受けることになる。 しかし、セミコロンを付けると else はどの if ともマッチしない! SP3 の安全ではある書き方は、#define SP3() if (b) { int x; av = f (&x); bv += x; }if (x==3) SP3(); else BORK();
というのがある。ただ、 do-while を手で書くのは面倒で、しかもコンパイラやツールによっては ``while'' の条件が定数なことで噛みついてくる。 ステートメント宣言のためのマクロは役に立つだろう。#define SP3() \\ do { if (b) { int x; av = f (&x); bv += x; }} while (0)
としておいて、 SP3 を#ifdef lint static int ZERO; #else # define ZERO 0 #endif #define STMT( stuff ) do { stuff } while (ZERO)
と宣言するのだ。この STMT の様なマクロで、細かいタイプミスが知らぬ間にプログラムに 影響を与えるのを防げる。#define SP3() \\ STMT( if (b) { int x; av = f (&x); bv += x; } )
マクロで予約語を使う時は、全体が括弧に入っていないといけない。 これは キャストや、 sizeof、 それからさっきの例等は除く。
条件付コンパイルは機種依存性がからむ時やデバッグ、それに コンパイル時にオプションを渡す時などに有用である。 しかし、 様々な制限が互いに重なり、予期せぬ動作をすることは少なくないので、注意しよう。 機種依存性を #ifdef する時は、 機種が特定されていなければデフォルトマシンとせずに、 エラーになる様にする。 (インデントして ``#error'' としておいて、古いコンパイラでも問題が起きないようにしておくこと)。 また最適化を #ifdef するときは、 デフォルトはコンパイル不可能なプログラムではなく、 最適化ナシにしておこう。最適化をしていないコードで必ず テストしておくこと。
#ifdef 部分のテキストは、たとえ false でもプリプロセッサが スキャンするかも知れない。たとえ #ifdef COMMENT みたいなコンパイルされない部分でも、 どんなテキストにしても良い訳ではない。
#ifdef はソースファイルではなく、ヘッダファイルに書いておく。 プログラム全体で使われる様なマクロの定義には #ifdef を使おう。 例えば、メモリ割り当てをチェックするヘッダファイルをこんな風に するのはありそうだ ( REALLOC と FREEの定義は省く)。
#ifdef DEBUG extern void *mm_malloc(); # define MALLOC(size) (mm_malloc(size)) #else extern void *malloc(); # define MALLOC(size) (malloc(size)) #endif
条件付コンパイルは一般に機能面でのチェックを行うべきで、 機種や OS 依存は避けた方が良い。
上のコードは二つの点で良くない。 より良いコード法のある 4BSD があるかも知れないこと、 それと今のが最善である 4BSD ではないシステムがあるかも知れないこと、 である。 そうではなく、 TIME_LONG や TIME_STRUCT などの define タグ を使って、適当なものを config.h などで定義すれば良い。#ifdef BSD4 long t = time ((long *)NULL); #endif
``C Code. C code run. Run, code, run... PLEASE!!!'' --- Barbara Tongue
enum では、最初の定数はノンゼロにするか、 あるいはエラーを示すものにする。
こうすると、 初期化していない変数はしばしば自発的に「エラー」になってくれる。enum { STATE_ERR, STATE_START, STATE_NORMAL, STATE_END } state_t; enum { VAL_NEW=1, VAL_NORMAL, VAL_DYING, VAL_DEAD } value_t;
返り値がエラーでないか、たとえ失敗しない筈の関数であっても、 チェックをせよ。 close() や fclose() は失敗が定義されていて、実際失敗することがあることは 知っておこう。それまでの全てのファイル操作が成功していても、である。 関数ではエラーはチェックして、返り値に反映するか、 きれいにプログラムを終了するようにしておこう。 デバッグ用やエラーチェック用のコードは十分書いておき、 完成してもそのままにしておくのが良い。 「不可能な」エラーも調べること。[8]
assert を使って関数が各々正しく定義された値を渡されていること、 そして途中の結果におかしいところがないことを確認せよ。
なるべく少なく #ifdef を使って、デバッグコードを埋め込もう。 以下の例では ``mm_malloc'' をデバッグ時のメモリ割り当ての関数としておけば、 MALLOC と書けば適切な関数が呼び出される。 しかも #ifdef の氾濫でコードが見苦しくなるのが避けられ、 またメモリ割当ての様子も、 デバッグ後とデバッグ中のみの余分なメモリの確保の差がはっきりする。
#ifdef DEBUG # define MALLOC(size) (mm_malloc(size)) #else # define MALLOC(size) (malloc(size)) #endif
配列等境界は、オーバーフローのしようがない場合様なでもチェックする。 データの書き込み先のサイズが可変の時は、それを関数に maxsize という名前の引数として渡す。 書き込み先のサイズが不明かも知れないならば、 「境界のチェック無し」を意味する定数を定義しておく。 範囲からはずれている場合には、関数はエラーを示す値を返すか、 プログラムを停止するようにしておこう。
/* * 引数:'src' はコピー元のナルで終わる文字列、 * 'dest' はコピー先。'maxsize' は 'dest' のサイズで、 * 不明な時は UINT_MAX をとる。 * 'src' も 'dest' も UINT_MAX よりも短くなければならず、 * 'src' は 'dest' 以下の長さを持つ * 返り値:成功すれば 'dest' のアドレス、失敗は NULL * 失敗でも 'dest' は変更される。 */ char * copy (dest, maxsize, src) char *dest, *src; unsigned maxsize; { char *dp = dest;while (maxsize-- > 0) if ((*dp++ = *src++) == '\\0') return (dest);
return (NULL); }
結局は、倍速だが間違った答えを返すプログラムは、無限に遅いのと同じだし、 ぶっとんだり正しいデータを壊したりするプログラムもまあ似た様なものだ。 それが真実だ。
``Cはアセンブラのパワーと、アセンブラの 移植性を兼ね備えている。''
--- 作者不明、Bill Thackerの発言の改作
移植性の高いコードのメリットは言うまでもない。 この章では、移植性の高いコードを書くためのいくつかのガイドラインを与える。 ここでは、「移植性」とは、同じソースファイルを 異なったマシン上で、ヘッダファイルの挿入やコンパイラに与えるフラグの変更 だけで、コンパイルし実行できることを意味する。 ヘッダファイルにはマシンによって異なる#defineやtypedefが 含まれることになる。 一般的に、新しい「マシン」とは、異なるハードウェアや、 異なるオペレーティングシステム、異なるコンパイラ、 またはそれらの組み合わせを意味する。 文献[1]にはスタイルと移植性の両方について有用な情報が含まれている。 以下に、移植性の高いコードをデザインするときに考慮すべき落とし穴や、 やっておいた方がよいことを列挙する。
マシンによってはある型に対して2つ以上のサイズが可能なことがある。 使われるサイズは、コンパイラやコンパイル時のフラグに依存することがある。 次の表に大多数のシステムにおける「安全な」型のサイズを示す。 符号なしの数は符号ありの数と同じビット数である。
型 pdp11 VAX/11 68000 Cray-2 Unisys Harris 80386 シリーズ ファミリ 1100 H800 char 8 8 8 8 9 8 8 short 16 16 8/16 64(32) 18 24 8/16 int 16 32 16/32 64(32) 36 24 16/32 long 32 32 32 64 36 48 32 char* 16 32 32 64 72 24 16/32/48 int* 16 32 32 64(24) 72 24 16/32/48 int(*)() 16 32 32 64 576 24 16/32/48
型 最低 少なくとも ビット数 〜以上の大きさ char 8 short 16 char int 16 short long 32 int float 24 double 38 float any * 14 char * 15 any * void * 15 any *
int *p = (int *) malloc (sizeof(int)); free (p);
もしもヌルでない「魔法の[原文:magic]」ポインタが必要なら、 何か記憶領域を割り当てるか、ポインタを機種依存として扱うこと。((int *) 2 ) ((int *) 3 )
extern int x_int_dummy; /* in x.c */ #define X_FAIL (NULL) #define X_BUSY (&x_int_dummy)
#define X_FAIL (NULL) #define X_BUSY MD_PTR1 /* MD_PTR1 from "machdep.h" */
この例には多くの問題がある。 スタックが上がったり下がったりするかも知れない (それどころか、スタックなど存在しないかも知れない)。 引数は渡されるときに広げられるかも知れない、 例えば char は int, として渡されるかも知れない。 引数は左から右へ、右から左へ、あるいは任意の順番でプッシュされうる。 レジスタに渡される(プッシュ自体されない)こともありうる。 評価の順番もプッシュされる順番と異なるかもしれない。 コンパイラによっては、いろいろな(非互換な)呼び出しの慣習を持ちうる。c = foo (getchar(), getchar());char foo (c1, c2, c3) char c1, c2, c3; { char bar = *(&c1 + 1); return (bar); /* c2を返さない事が多い */ }
s = "/dev/tty??"; strcpy (&s[8], ttychars);
代わりに、x &= 0177770
を使えばどのマシンでもちゃんと動く。 ビットフィールドにはこのような問題はない。x &= ~07
上の例では、 b の添え字がインクリメントされていないことしか分からない。 a の添え字の i の値は、 インクリメントの前のものかもしれないし、後のものかも知れない。a[i] = b[i++];
2番目の例では、 ``bar->next'' のアドレスは ``bar'' に値が代入される前に計算されるかも知れない。struct bar_t { struct bar_t *next; } bar; bar->next = bar = tmp;
3番目の例では、 bar が bar->next. の前に代入されることが考えられる。 これは「代入は右から左に行われる」というルールに反するように見える が、合法的な解釈である。 次の例を考えてみよ。bar = bar->next = tmp;
``i'' に代入された値は、代入が右から左へ進んだとしたときの型になって いなければならない。 しかし、 ``i'' に ``(long)(short)new'' という値が代入されるのは、 ``a[i]'' に値が代入されるより先かも知れない。 コンパイラにはいろいろあるのである。long i; short a[N]; i = old i = a[i] = new;
が#define FOO(string) (printf("string = %s",(string))) ... FOO(filename);
になるのは、一部の場合だけである。 ところで、トリッキーなプリプロセッサはいくつかのマシンで偶然に マクロを壊すことがあるので気をつけること。 例えば次の2種類のマクロを考えてみよ。(printf("filename = %s",(filename)))
2番目の LOOKUP は、2種類の違った展開方法があるので、 コードが不可思議な壊れかたをする可能性がある。#define LOOKUP(chr) (a['c'+(chr)]) /* 意図通りに動く。 */ #define LOOKUP(c) (a['c'+(c)]) /* 時々壊れる。 */
footnote:
- 14.
- このコードはコンパイルに失敗するか、ポインタを作るのに失敗するか、 ポインタの比較に失敗するか、あるいはポインタの逆参照に失敗する可能性もある。
- 15.
- ライブラリによっては読み出し専用の文字変数を書き替えようとして 元に戻すものがある。 このような壊れたライブラリのせいで移植ができないことがある。 改善されてきてはいる。
最近の新しいCコンパイラは、ANSIで提唱されている標準Cをサポートしている。 可能な限りいつでも、コードを標準Cの枠内で書き、関数プロトタイプや定数格納、volatile格納といった機能を利用すること。 標準Cはオプティマイザによりよい情報を与えることによって、プログラムのパフォーマンスを向上させる。 標準Cは全てのコンピュータが同じ入力言語を受け付けることを保証し、 機種依存性を隠したり機種依存の可能性があるコードに警告を発したりする 機能を提供することで、移植性を向上させる。
古いコンパイラに移植しやすいコードを書くこと。 例えば、 新しい(標準の)キーワード、例えば const や volatile をグローバルな.hファイルで、条件付きで#defineせよ。 標準的なコンパイラはプリプロセッサシンボル __STDC__16 をあらかじめ定義している。 void*型は単純に動作させるのが難しい。 なぜなら古いコンパイラは void は理解しても void* は理解しないからである。 新しい (機種とコンパイラに依存する) VOIDP 型を新たに作るのが一番簡単で、 これは古いコンパイラではふつう char* である。
#if __STDC__ typedef void *voidp; # define COMPILER_SELECTED #endif #ifdef A_TARGET # define const # define volatile # define void int typedef char *voidp; # define COMPILER_SELECTED #endif #ifdef ... ... #endif #ifdef COMPILER_SELECTED # undef COMPILER_SELECTED #else { NO TARGET SELECTED! } #endif
ANSI Cでは、プリプロセッサ命令のための'#'は 行の中で空白を除く最初の文字でなくてはならないことに留意。 もっと古いコンパイラでは、行の先頭の文字でなくてはならない。
staticな関数が順方向に宣言を持つ場合、その順方向の宣言は 記憶領域クラスを含む必要がある。 古いコンパイラでは、クラスは ``extern'' でなくてはならない。 ANSIコンパイラでは、クラスは ``static'' でなくてはならないが、 グローバルな変数の場合は ``extern'' と宣言しなくてはならない。 したがって、staticな関数の順方向宣言には、 適切に#ifdefされた FWD_STATIC のような#defineを使うべきである。
``#ifdef NAME'' は、 ``#endif NAME'' ではなく、 ``#endif'' または ``#endif /* NAME */'' で終わるべきである。 なお、コードを見れば明白であるので、 このコメントは短い#ifdefには付けるべきでない。
ANSIの trigraphs は、文字列 ``??'' を含むプログラムを不可解に停止させてしまうことがある。
footnote:
- 16.
- コンパイラの中には __STDC__ を0に定義しておいて、ANSI C規格に部分的に従っていることを 示しているものもある。 残念ながら、どのANSI機能が提供されているかを 知ることはできない。 したがって、そのようなコンパイラは故障品である。 ルール 「無理やりやらされているのでなければ、壊れたコンパイラで物を書くのはやめよ」 を見よ。
ANSI Cのスタイルは、2つの注意すべき例外を除いて、標準Cと同じである。 その例外とは、記憶領域修飾子と、引数リストである。
const と volatile は同じ結合ルールを持っているので、 それぞれの const や volatile は、別々に宣言しなければならない。
int const *s; /* YES */ int const *s, *t; /* NO */
プロトタイプ化された関数は、引数の宣言と定義を 1つのリストにまとめている。 引数について、関数のコメントの中で説明しておくべきである。
/* * `bp': 乗ろうとしているボート。 * `stall': 乗り場のリスト。NULLには決してならない。 * 乗り場の番号を返す。 0 => 空きがない。 */ int enter_pier (boat_t const *bp, stall_t *stall) { ...
関数プロトタイプはコードを堅牢にし、速く動かすために 使うべきである。 残念ながら、プロトタイプ化された宣言
は、定義extern void bork (char c);
このプロトタイプは c がそのマシンに最も自然な型、例えばバイトとして渡されると言っている。 プロトタイプ化されていない(後方互換の)定義は、 c が常に int17 を渡されることを示唆している。 もし関数が格上げ可能な引数を持つのなら、呼び出し側と呼び出される側は同じようにコンパイルされなければならない。 両方ともが関数プロトタイプを使うようにするか、 そうでなければどちらもプロトタイプを使うことはできない。 この問題は、プログラムがデザインされたときに引数が格上げされれば 避けられる。 例えば、 bork は int の引数をとるように定義できる。void bork (c) char c; ... と互換性がない。
上の宣言は、定義がプロトタイプ化されていれば動作する。
残念ながら、 プロトタイプ化された文法は、ANSIでないコンパイラが プログラムをはねつける原因となる。void bork (char c) { ...
プロトタイプとも古いコンパイラとも動作する外部宣言を書くのは 簡単である18。
PROTO は二重かっことともに使わなければならないことに注意。#if __STDC__ # define PROTO(x) x #else # define PROTO(x) () #endifextern char **ncopies PROTO((char *s, short times));
結局、 片方のスタイル(すなわち、プロトタイプあり)だけで書くのが最善かも知れない。 プロトタイプなしのバージョンが必要になったら、自動変換ツールを使って 生成できる。
footnote:
- 17.
- このような自動的な型の格上げを widening と呼ぶ。 古いコンパイラでは、wideningのルールは 全ての char と short の引数が int として渡され、 float の引数が double として渡されることを要求している。
- 18.
- PROTO を使うことは「マクロ置換で文法を変えてはいけない」というルールに 違反することに注意。 これよりよい解決法がないのは残念である。
Pragma は、管理された方法で機種依存コードを導入するのに使われる。 当然ながら、pragmaは機種依存性として取り扱われるべきである。 残念ながら、ANSIのpragmaの文法では、 マシンに依存したヘッダに隔離することができない。
pragmaには2つのクラスがある。 最適化 は無視して差し支えない。 may safely be ignored. システムの動作を変更するようなpragma (「必須pragma[原文:required pragmas]」)は、無視できない。 必須pragmaは、pragmaが選択されていない場合にコンパイルが中断するように、 #ifdefを付けておくべきである。
コンパイラによって、与えられたpragmaの使い方が異なることがある。 例えば、あるコンパイラは ``haggis'' を最適化の合図に使い、 別のものでは、与えられた文に達したときにプログラムを終了させる という指示をするのに使うかも知れない。 したがって、pragmaを使うときには、 常に機種依存の#ifdefで囲まなくてはならない。 pragmaは常に非ANSIコンパイラでは#ifdefを使って除外しなくては ならない。 必ず #pragma, の中の`#'記号はインデントすること。 そうしないと古いプリプロセッサはそこで止まってしまう。
#if defined(__STDC__) && defined(USE_HAGGIS_PRAGMA) #pragma (HAGGIS) #endif
```#pragma'コマンドはANSI規格に書かれていて、 実装で定義されたあらゆる種類の働きをさせる。 GNU Cプリプロセッサでは、`#pragma'は最初にゲーム`rogue'を起動しよう とする。それが失敗すると、ゲーム`hack'を起動しようとする。もしそれが 失敗すると、ハノイの塔を表示するGNU Emacsを起動しようとする。もしそれも 失敗すると、致命的なエラーを出す。 いずれの場合も、プリプロセッサは動作を停止する。''
--- GNU C 1.34のCプリプロセッサのマニュアル。
このセクションには様々なすべきこととすべきでないこと [原文:do's and don'ts]が書かれている。
代入をどうしても組み込んで使うときは、テストを明示的にして 後で「直され」ないようにすること。abool = bbool; if (abool) { ...
while ((abool = bbool) != FALSE) { ...
while (abool = bbool) { ... /* VALUSED */
while (abool = bbool, abool) { ...
LintはCのソースファイルを検査して、その中の型の不一致や、関数定義と呼び出しの矛盾、プログラムのバグの可能性などを発見し報告するCプログラムチェッカ[2][11]である。 全てのプログラムにlintを使うことが強く勧められる。 そしてほとんどのプロジェクトは、プログラムにlintを公式な受け入れ手続きの一部として使うことを要求することが期待される。
注意すべきこととして、lintの最善の使い道は、プログラムが公的に受け入れられる前に越えなければならない障壁としてではなく、コードに変更や追加を加えたときに使う道具としてのものである。 lint は問題が起こる前に不明瞭なバグを発見し、移植性を保証する。 lintからのメッセージの多くは実際に何かおかしいという事を示している。 面白い話を1つ。 `fprintf' の引数が1つ足りないプログラムがあった。
作者には何の問題も起きなかった。 しかし、一般ユーザがコマンドラインでミスをするたびに、そのプログラムはコアを吐いた。 多くのバージョンのlintはこのバグを捕捉できる。fprintf ("Usage: foo -bar <file>\\n");
多くのオプションは学ぶ価値がある。 いくつかのオプションは合法なものにも文句を付けるかも知れないが、それらは沢山のヘマも見つけだす。 -p19 はライブラリルーチンの一部に対してのみ関数呼び出しの型の一致を調べるので、検出率を最高にするためには、プログラムを-p付きと-pなしの両方でlintすべきであることに留意。
lintはまたコード中の特殊なコメントも認識する。 これらのコメントはlintがコードに文句を言うのをだまらせたり、 特殊なコードにコメントしたりするのに使われる。
footnote:
- 19.
- フラグの名前は異なることがある。
もう1つの便利なツールがmake[7]である。 開発の際に、 makeは最後にmakeが使われてから変更のあったモジュールだけをコンパイルし直す。 また、他の作業を自動化するのにも使える。 広く使われる表記法に、次のようなものがある:
これに加えて、コマンドラインから (``CFLAGS''のように) Makefileの値を定義したり (``DEBUG''のように) プログラムの値を定義したりできる。
all 常に全てのバイナリをメイクする clean 全ての中間的なファイルを削除する debug テスト用のバイナリ'a.out'または'debug'をメイクする depend 一時的な依存関係を作る install バイナリやライブラリなどをインストールする deinstall ``install''の取り消し mkcat マニュアルページをインストールする lint lintを走らせる print/list 全てのソースファイルのハードコピーを印刷する shar 全てのファイルのシェルアーカイブを作る spotless make cleanを行い、バージョン管理[原文:revision control]を使ってソースファイルを捨てる 注:Makefileはソースファイルだが削除しない source spotlessが行ったことを取り消す tags ctagsを走らせる(-tフラグを使うことを勧める) rdist ソースを他のホストに配付する file.c 名前を指定したファイルをバージョン管理で調べる
個々のプロジェクトではここに書かれた他に規範が追加されることがある。 次に挙げる論点はそれぞれのプロジェクトのプログラム管理グループから 提示されるべきものである。
Cのプログラミングスタイルに一通りの規範が示された。 最も重要なものをいくつか挙げると:
規範は何でもそうだが、有用なものには従うこと。 これらの規範のいずれかに従おうとして問題が起きたなら、 単に無視してはいけない。 地元の大御所か、自分の組織の経験あるプログラマに尋ねてみること。
footnote:
- 20.
- 訳注:"K&R Style" とも言う。複数の文をまとめる{を、ifやforなどのキーワードと同じ行の末尾に書く記法。
- 21.
- なるべく文語訳聖書に近い用字用語をこころがけたら ルビなしでは読めないような文章になってしまった。 HTML でルビを書くわけにもいかないのでカッコがきされているのはルビと思われたし。
- 22.
- 恐らくは: lest の文語訳聖書での訳語。