この記事ではC言語でプリプロセッサを使うときに気をつけるべき項目を記述します。プリプロセッサは C 言語と違う文法を持っているのでバグの温床となりやすいです。可能な限りプリプロセッサを使わないでください。通常はプリプロセッサが必要になるのは、定数の defineとヘッダファイルの include くらいです。関数マクロは inline 関数で置き換えられる場合がほとんどです。プリプロセッサの中でも特に条件コンパイルは注意が必要です。#if がネストすると 2 のべき乗で組み合 わせのパタンが増えるので、単体テストがほぼ不可能になってしまいます。
各ルールごとにその理由とルール違反のソースコード例&ルール適合のソースコード例を書いています。お使いのパソコンやスマホの画面サイズに寄っては、ソースコードが横にはみ出てしまうことがあります。その場合、スライドバーは表示されませんがソースコードのところをドラッグして横にスライドすることができます。
ヘッダファイルは多重インクルード防止のマクロを定義する
ヘッダファイルの include 順を考慮しなくてすむように、各ヘッダファイルには多重インクルード防止のマクロを定義してください。マクロ名は「FILENAME_H_INCLUDED」の形式を推奨します。すくなくとも最初の一文字目 は”_”は使わないでください。C言語の規格上、予約語となっていることが多いためです。
違反コード
/** * @file header_NG.h */ // ヘッダ内コードをここに書く. /* endof header_NG.h */
適合コード
#ifndef HEADER_OK_H_INCLUDED #define HEADER_OK_H_INCLUDED /** * @file header_OK.h */ // ヘッダ内コードをここに書く. #endif
プリプロセッサが削除する箇所も、正しく記述する
C 言語の規格上、プリプロセッサが読み飛ばす箇所も、文法的に正しく書かなければなりません。また、文法上正しくないものをコード内に残しておくとバグのもとになります。プリプロセッサが削除する箇所も正しく記述してください。不要でしたら思い切って削除してください。バージョン管理システム(Subversion や Git など)を使っていれば、コードを削除してもいつでも元に戻せます。
違反コード
void func_NG(void) { #if 0 覚書. flag が 1 なら hoge()をコール、 flag が 0 なら piyo()をコールする。 // ↑↑↑C 言語で無いのでNG. #endif if (flag == 1) fuga(); else piyo(); }
適合コード
#if 0 /* * 覚書. * flag が 1 なら hoge()をコール、 flag が 0 なら piyo()をコールする。 */ #endif if (flag == 1) fuga(); else piyo(); }
#line は使用しない
プリプロセッサの#line を使うと行番号、ファイル名を変更できますが、デバッグ情報などの整合取れなくなるので、使用しないでください。
違反コード
// test.c void func_NG(void) { #line 100 "test.c" #error エラー。 /* * 100行目でないのに、エラーメッセージに"test.c l.100"といったような * メッセージが出るはず。 */ }
適合コード
// test.c void func_OK(void) { //#line 100 "test.c" #error エラー。 // エラーメッセージに、正しく"test.c l.5"といったようなメッセージが出るはず. }
関数マクロは使用しない、変わりに inline を使う
関数マクロは結局のところただの文字列置換なので、c 言語の関数と同じように使うと想定できない挙動をして不具合がおきることがあります。より安全で同等のパフォーマンスの得られる inline 関数を使用してください。 また、inline 関数はマクロ関数と同様に呼出し箇所すべてに埋め込まれますので、ファームウェアサイズを大きくしないためにも数行程度の大きさにしてください。さらにinline 関数はグローバル関数でなく static 関数にしてください。(static にしないとコンパイラがインライン展開してくれないことが多いです)
違反コード
#define POW(x) ((x) * (x)) void func_NG(void) { int pow_val; int i = 2; pow_val = POW(++i); // pow = (++i) * (++i); と展開され、インクリメントが 2 回行われてしまう. printf("pow_val = %d¥n", pow_val); // pow_valの値がどうなるかは未定義. }
適合コード
// inline関数利用. static inline int pow(int x) { return ((x) * (x)); } void func_OK(void) { int pow_val; int i=2; pow_val= pow(++i); // 期待通りにインクリメントは一回だけ行われる. printf("pow_val = %d¥n", pow_val); // pow_val = 9. }
マクロ定義は括弧で囲む
演算子の優先度の違いによる不具合を防ぐため、マクロの定義は必ず括弧で囲んでください。剰余算と加減算、論理積と論理和、ビット AND とビット OR を混ぜて使う場合にとくに問題が起きやすいです。 マクロはネストして定義されることも多く、一見してどのような計算式に展開されるのか分からないため、演算子の優先度によるバグは発見しづらいので特に注意が必要です。
違反コード
#define CONDITION_A 1 // 条件コンパイルマクロ A. #define CONDITION_B 0 // 条件コンパイルマクロ B. #define CONDITION_C 0 // 条件コンパイルマクロ C. // 条件 A か B のどちらかが有効のときの条件 D も有効になる(つもり). #define CONDITION_D CONDITION_A || CONDITION_B // 全体を括弧で囲んでいない. void func_NG(void) { #if (CONDTION_D && CONDTION_C) /* CONDITION_C が 0 なので、ここにはこないつもり */ printf("?? CONDITION_D and CONDTION_C is enable¥n"); #else printf("!! CONDITION_D and CONDTION_C is disable¥n"); #endif /* * 上記コードは期待に反して、 * "?? CONDITION_D and CONDTION_C is enable¥n" が出力される。 * * 【理由】 * 「#if CONDTION_D && CONDTION_C」は、 * 「#if CONDITION_A || CONDITION_B && CONDTION_C」と展開される。 * 演算子の優先度は「||」よりも「&&」のほうが高いので、これは * 「#if CONDITION_A || (CONDITION_B && CONDTION_C)」と同じ意味になる。 * なので、CONDTION_C が 0 なのにもかかわらず、 * 期待に反して「#if CONDTION_D && CONDTION_C」は有効(1)になる。 */ }
適合コード
#define CONDITION_A 1 // 条件コンパイルマクロ A. #define CONDITION_B 0 // 条件コンパイルマクロ B. #define CONDITION_C 0 // 条件コンパイルマクロ C. // 条件 A か B のどちらかが有効のときの条件 D も有効になる #define CONDITION D (CONDITION_A || CONDITION_B) // 全体を括弧で囲んでいる void func_NG(void) { #if (CONDTION_D && CONDTION_C) /* CONDITION_C が 0 なので、ここにはこない */ printf("?? CONDITION_D and CONDTION_C is enable¥n"); #else printf("!! CONDITION_D and CONDTION_C is disable¥n"); #endif /* * 上記コードは期待どおり、 * "!! CONDITION_D and CONDTION_C is disable¥n" が出力される。 * * 【理由】 * 「#if CONDTION_D && CONDTION_C」は、 * 「#if (CONDITION_A || CONDITION_B) && CONDTION_C」と展開される。 * 演算子の優先度は「||」よりも「&&」のほうが高いが、括弧がついているので * 先に「||」のほうが評価される。 * なので、期待どおり「#if CONDTION_D && CONDTION_C」は * 無効(0)になる。 */ }
ダブルスラッシュ形式のコメントの末尾には半角ピリオド「.」をつける
Shift_JIS 文字には、2 バイト目が「0x5c」になっているものがあります(「能」「予」「十」など)。この 0x5c は「¥」 (バックスラッシュ)を表し、これは C 言語ではエスケープを意味します。つまり、その次の文字が無視されてしまいます。例えばダブルスラッシュ形式コメントの末尾がダメ文字「能」だった場合、コメント終端の改行文字がエスケープされることになります。結果、その次の行までコメントと認識されます。このような 2 バイト目が「0x5c」になっている文字を俗にダメ文字と言います。行の末尾がダメ文字にならないようにコメントを書けばいいのですが、ダメ文字を全て覚えるのは大変です。「能」や「表」などよく使う文字も含まれています。そこで、「ダブルスラッシュコメントの末尾は半角ピリオドにする」と一律に決めます。これにより必ずコメントの末尾がダメ文字になるのを避けることができます。
ダメ文字一覧 (2Byte 目が 0x5c:バックスラッシュになるもの)
― ソ Ы 噂 浬 欺 圭 構 蚕 十 申 曾 箪 貼 能 表 暴 予 禄 兔 喀 媾 彌 拿 杤 歃 濬 畚 秉 綵 臀 藹 觸 軆 鐔 饅 鷭 偆 砡
違反コード
/* * 引数 a を引数 b で割った値を返す。 * 引数 b が 0 のときは 0 除算になってしまうためエラーとして-1 を返す */ int div_func(int a, int b) { if (b == 0) // b が 0 の場合は 0 除算になるため計算不可能 return -1; /* * 上記の if 文で 0 除算ガードをしているが、 * ダブルスラッシュコメントの末尾がダメ文字の「能」で終わっている。 * このためコメント末尾の改行文字がエスケープされてしまい、結果、 * 次の行の return -1 までコメントアウトされてしまう。 * 具体的には次のように解釈される。 * ダブルスラッシュ行の末尾に 「return -1;」 が来てしまう * * if (b == 0) // b が 0 の場合は 0 除算になるため計算不可能 return -1; * * そのため、b が 0 の場合も意図に反してここに来てしまう。 * 結果、0 除算が発生してハングする。 */ return a / b; }
適合コード
/* * 引数 a を引数 b で割った値を返す。 * 引数 b が 0 のときは 0 除算になってしまうためエラーとして-1 を返す */ int div_func(int a, int b) { if (b == 0) // b は 0 の場合は 0 除算になるため計算不可能. return -1; /* * 上記のダブルスラッシュコメントはダメ文字の「能」のあとの「.」を入れている。 * このためダメ文字は改行でなく「.」をエスケープする。 * よって b が 0 のときは期待通り次の行の return -1 を実行する。 * * 結果、期待通り b が 0 の場合はここまでこない。 */ return a / b; }
以上、プリプロセッサとマクロのコーディング規約でした!
コメント
独学で組み込みの勉強をしている者です。
ダメ文字というものを初めて知りました。
独学でやっているとそういった経験にもとづくノウハウ(?)的なものに疎く、記事全般、非常に勉強になりました。
コメントありがとうございます!
なにかのご参考になれば幸いです!
もし不明点やリクエストあれば、コメントかTwitterにてご連絡頂けますと嬉しいです!