バグを生まないためのルール 以下に、このコーディング規則のいくつかの例を紹介する。これらのルールは、バグの発生件数の削減に役立つだろう。
●ルール1
if文、else句、switch文、while文、do文、for文に続くコード・ブロックを、常に中括弧「{ }」でくくる。これらの文や句に続くコードが1文だったり、何もなかったりした場合でも、中括弧でくくるべきである(図1)。 理由は、次の通りである。例えば、if文に記述した条件が成立したときに処理すべき内容が、当初は「A」という1文で記述できていたとしても、その後改変を加えて「A」と「B」の2文になったとする。このとき、最初の「A」を中括弧でくくっていなければ、「B」を追加すると同時に中括弧の記述を忘れると、後から加えた「B」という処理が、if文の条件が成立するか否かにかかわらず常に実行されてしまう。つまり、新たなバグを生み出してしまうことにつながる。このようなたぐいのバグは、常にコード・ブロックを中括弧でくくるようにしておけば、未然に防げる。
図1 句の記述作法 if文、else句、switch文、while文、do文、for文に続くコード・ブロックを常に中括弧でくくる。
●ルール2
次の条件に当てはまる場合は、常にconst修飾子を使って宣言する。 変数の初期化後、その値を変更することがないならば、その変数を宣言するときにconst修飾子を付ける。 関数に値を参照(ポインタ)で渡し(call-by-reference)、その値を関数内で変更することがないならば、その関数の宣言時にその引数にconst修飾子を付ける。例「char const *p_data」。 構造体や共用体のメンバーのうち、変更できないメンバーにはconst修飾子を付ける。例えば、メモリー・マップトI/O方式を採る周辺機器のレジスタにアクセスするための構造体メンバーなどである。 #defineで定義する数値定数のデータ型を厳密にする場合、const修飾子を使って宣言する。 これらの理由は、コンパイラの機能を使ってデータをリード・オンリーにして、意図しない書き込みからデータを保護するためである。
●ルール3
関数や変数を、それが定義されているモジュールの外に公開する必要がない場合は、staticキーワードを使って宣言する。 理由は次の通りだ。C言語のstaticキーワードにはいろいろな意味がある。モジュール・レベルでは、グローバル変数や関数を、staticキーワードを付けて宣言すれば、ほかのモジュール内の関数による不用意なアクセスから保護できる。このようなstaticキーワードの使い方は、カプセル化にもつながる。
●ルール4
次の条件に当てはまる場合は、常にvolatile修飾子を使って宣言する。 任意の割り込み処理ルーチンからアクセスされるグローバル変数を宣言するとき。 2つ以上のタスクからアクセスされるグローバル変数を宣言するとき。 メモリー・マップトI/O方式を採る周辺機器のレジスタ・セットにアクセスするためのポインタ変数を宣言するとき。例「timer_t volatile const *p_timer」。 理由は次の通りである。コンパイラは、並列実行される複数のスレッドによって変更される可能性のある変数やレジスタであっても、それらに対する読み出し/書き込みコードが不要だと判断するとコード自体を削除するように最適化する場合がある。 例えば次のような場合だ。あるスレッド「A」が、ある条件のときに変数の値を変更するとしよう。別のスレッド「B」はその変数をポーリングしていて、値が変更されたときに何らかの処理をする。このとき、スレッドBの中に変数の値を変更するようなコードが書かれていなければ、コンパイラはその変数の値は変更されないと判断してしまって最適化する場合がある。すると、コンパイル後のオブジェクト・コードでは、変数をポーリングするコードが削除されてしまったりする。volatile修飾子を適切に使うと、このようなコンパイラの最適化機能が無効となり、コード全体にわたるような発見が難しいバグの排除につながる。
●ルール5
コメントをネストさせない。そして、たとえ一時的であっても、コード・ブロックを無効にする用途(コメント・アウト)にコメントを使わない。一時的にコード・ブロックを無効にするには、プリプロセッサの条件コンパイル機能を使う(図2)。例「#if 0 … #endif」。 理由は、ネストしたコメントを使ったり、コメントでコード・ブロックを無効にしたりすると、コンパイルして生成される最終的な実行モジュールの中に、無効にしたはずのコードが入り込んでしまうリスクが生じるからだ。
●ルール6
整数値のビット長やバイト長が問題になるときは、charやshort、int、long、long longといったデータ型を使わず、固定長データ型を利用する。C言語の標準で規定されている符号付きおよび符号なしの固定長整数型は、表1の通りである。 理由は次の通りだ。ISOが策定したC言語標準「ISO/IEC 9899」では、charやshort、int、long、long longといったデータ型の長さ(ビット長)について、処理系ごとに定義することが許されており、移植性の問題を引き起こす。1999年に改定された「ISO/IEC 9899:1999」でもこの問題の潜在的な原因は解決されていないが、表1に示したビット長が一意に定義されているデータ型が新たに追加された。これらの新しいデータ型は、「stdint.h」というヘッダー・ファイルで定義されている。
●ルール7
「&(論理積)」、「|(論理和)」、「~(ビット反転)」、「^(排他的論理和)」、「«(左シフト)」、「»(右シフト)」といったビット演算子を、符号付き整数値に適用しない(図3)。 理由は、C言語標準が、例えば「2の補数を使う」というように、符号付き整数型データの表現形式を規定していないことにある。符号付き整数値に対してビット演算を実施した結果については、処理系に依存しているのが現状だ。
●ルール8
符号付き整数と符号なし整数を組み合わせたり、比較したりしてはいけない。この原則を守るために、符号なし10進定数(即値)の末尾には「u」を記述する(図4)。 理由は次の通りである。符号付き整数型変数に格納されたバイナリ・データの取り扱いの詳細については、C言語標準では処理系に依存するとされている。しかも、符号付き整数と符号なし整数を組み合わせた結果引き起こされるバグは、実際に演算するデータの内容に依存するため、発見が難しい。
●ルール9
インライン関数を利用して記述できる内容ならば、パラメータ付きマクロを使わない(図5)。 理由は次の通りだ。プリプロセッサ命令の#defineを利用することにはリスクが伴う。特にパラメータ付きマクロに伴うリスクは大きい。図5に示した例文のように括弧が広範囲に及ぶことが問題で、「MAX(i++, j++)」のような呼び出しに対して、意図しない2重のインクリメントが実行される可能性を排除できない。 ほかにも、符号付きデータと符号なしデータの比較といった、マクロの間違った使用法に対するリスクがある。マクロの場合、関数と異なり、コンパイラによる引数のデータ型チェックが働かない。なお、「inline」はもともとC++言語のキーワードだったが、「ISO/IEC 9899:1999」でC言語標準にも追加された。
●ルール10
変数を宣言するときに、「,(コンマ)」を使わない(図6)。
理由は次の通りである。変数宣言を、それぞれ独立した文で記述する手間は少ない。それに対して、コンパイラや、コードのメンテナンス担当者が、図6のような記述が意図する内容を誤解してしまうリスクは高い。



