組込みソフト向けC言語コーディング規約|演算と式

どうも、ながやすです。

この記事では演算と式について記載します。一般的に「キャスト」「浮動小数点」「シフト」によくバグが入り込みます。キャストせずしないですむように、設計時にデータ型を慎重に選択してください。また特に理由がなければchar型やshort型でなくint型を使ってください。int型は「計算機にとって自然な型」です。
例えばループカウンタでint型でなくchar型やshort型を使っているのをときどき見かけますが、実はむしろサイズが大きくなり、かつ、処理速度は遅くなることがほとんどです。整数の変数には特に理由がない限りint型を使用してください。(配列や構造体は除く。)

スポンサーリンク

浮動小数点式は等号”==”, 不等号”!=”での比較をしない

浮動小数点は誤差を含んでいるので、等号・不等号での比較では予期せぬ動作となる恐れがあります。浮動小数点の比較は、その値が誤差を含めて期待する範囲内かを判断してください。

違反コード

void func_NG(double dValue)
{
    /* 誤差のため dValue が 1.0 ぴったりとは限らない */
    if (dValue == 1.0) {
        // 略.
    }
}

適合コード

#define LIMIT_ERROR 0.001 // 許容誤差の上限値.

void func_OK(double dValue)
{
    /* dValueと1.0の誤差がLIMIT_ERROR以下かどうかチェック */ 
    if ((fabs(dValue) -1.0) < LIMIT_ERROR) {  // fabs:絶対値を求める関数.
        // 略.
    }
}

volatileやconstをはずすキャストはしない

volatileやconstをつけてある場合は、必要があってつけています。それらを無効にした場合に正常に動作しない可能性があるので、キャストでそれら無効にしないでください。

違反コード

void func_NG_0(void)
{
    volatile int **dbl_ptr;
    int *ptr;

    ipp = (int **) &ip; // NG. volatile修飾子をはずしている.
}

void func_NG_1(void)
{
    const static int cnst_value = 10;

    (int) const_value = 11; // NG. const修飾子を無理やりはずしている.
}

適合コード

void func_OK_0(void)
{
    volatile int **dbl_ptr;
    volatile int *ptr;

    ipp = &ip; // OK.
}

void func_OK_1(void)
{
    static int cnst_value = 10;

    const_value = 11; // OK.
}

シフト演算子の右辺の項は、0以上でかつ左辺の項のbit幅未満にする

シフトされる変数のビット幅より大きくシフトしたときは、どのような値になるかは未定義です。0になるとは限りません。発見しづらいバグになりますので、変数のビット幅より大きくシフトしないでください。

違反コード

void func_NG(void)
{
    unsigned char ucValue;
    unsigned int  uiValue;

    uiValue = ucValue <<  0;  
    uiValue = ucValue << 11;  // ucValueのビット幅(8)より大きい.
}

適合コード

void func_OK(void)
{
    unsigned char ucValue;
    unsigned int  uiValue;

    uiValue  = ucValue <<  0;  
    uiValue  = (unsigned int) ucValue << 11;  // 明示的に32bit幅の unsigned intにキャスト.
}

int型よりbit幅の小さい型(unsigned char, unsigned short) のデータをbit反転 " ~ " および左シフト"<<"させるときは、期待する結果の型に明示的にキャストする

int型よりbit幅の小さい型は、計算途中でコンパイラに勝手にint型へキャストされます(汎整数拡張)。そのため、int型よりbit幅の小さい型でビット反転や左シフトをするときは必ず明示的に元の型にキャストしなおして使ってください。

違反コード

void func_NG(void)
{
    unsigned char uc_port;
    unsigned char uc_port_inv;

    uc_port      = 0x5a; 
    uc_port_inv  = (~uc_port) >> 4; 

    // 0x5aを普通にビット反転すると0xa5なので
    // それを4bit右シフトすると0x0aが表示されそうだが,
    // 実際は0xfaが表示される.
    printf("uc_inv : 0x%02x\n", uc_inv); 

    //  uc_port_inv =  (~uc_port) >> 4; 
    //  -----------------------------------
    //  uc_port       ⇒ 0x0000005a (unsigned charからintへの汎整数拡張)
    // ~uc_port       ⇒ 0xffffffa5
    // ~uc_port >> 4  ⇒ 0x0ffffffa (負数なので符号ビットが右bitへ伝播)
    //  uc_port_inv   ⇒       0xfa
}

適合コード

void func_OK(void)
{
    unsigned char uc_port;
    unsigned char uc_port_inv;

    uc_port      = 0x5a; .
    uc_port_inv  = (unsigned char) (~uc) >> 4; // 暗黙的な汎整数拡張対策.

    // 明示的に(unsigend char)とキャストすることにより,
    // 期待通り0x05が表示される.
    printf("uc_inv : 0x%02x\n", uc_inv); 
}

構造体や共有体の比較に関数memcmp()を使用しない

構造体のメンバ間には、データのアラインメントの都合により隙間が空くことがあります(パディング)。そのため、構造体同士をmemcmpで比較すると、各メンバの隙間のゴミ値まで比較してしまい、予期せぬ動作となる恐れがあります。構造体同士を比較する場合は、それぞれのメンバ同士を比較してください。

違反コード

// member_1とmember_2の間に3Byteのパディング(隙間)がある恐れあり.
typedef struct {
    char member_1;
    int  member_2; 
} STRCT;

void func_NG(void)
{
    STRCT strct_a;
    STRCT strct_b;

    strct_a.member_1 = strct_b.member_1 = 'z';
    strct_a.member_2 = strct_b.member_2 = 0xaa;

    // 構造体のパディング(隙間)まで比較しているので,
    // メンバは全部等しくても、memcmpが「等しくない」と返す恐れあり.
    if (0 == memcmp(strct_a, strct_b)) {
		; 
    }
}

適合コード

typedef {
    char member_1;
    int  member_2;
} STRCT;

void func_NG(void)
{
    STRCT strct_a;
    STRCT strct_b;

    strct_a.member_1 = strct_b.member_1 = 'z';
    strct_a.member_2 = strct_b.member_2 = 0xaa;
	
    // 個々のメンバに対して、比較を行う.
    if ((strc_a.member_1 == strct_b.member_1) && (strc_a.member_2 == strct_b.member_2)) {
		;
    }
}

論理演算子("|| ","&&")にはデータを変更する式・関数コール・およびvolatile変数の参照を行う式は用いない

論理演算子は、真偽が確定した時点で実行を中断します。例えば理和"||"の場合、左の式が真なら右の式は実行されず無視されます。論理演算子にはデータを変更する式・関数コール・volatile変数の参照を行う式は書かないでください。

違反コード

void func_NG(unsigned char ucHoge)
{
    int a = 10;
    int b =  5;

    if ((a == 10) || (b++ == 6)) // a == 10の時点で真ということが確定するので、b++ は実行されない.
        ; // b == 5 のまま.
}

適合コード

void func_NG(unsigned char ucHoge)
{
    int a = 10;
    int b =  5;

    b++;  // if文の外でインクリメントする.

    if ((a == 10) || (b == 6))
        ; // b == 6 となる.
}

ループカウンタに浮動小数点は使用しない

浮動小数点は誤差があるので、ループカウンタには使用しないでください。例えば.1は2進数では表現できない(循環小数)ので、誤差が含まれます。ちなみに、ループカウンタにはint型を推奨します。一般的にchar型、short型を使ってもファームサイズは小さくならず、むしろ大きくなる傾向になります。また、速度も遅くなる傾向になります。

違反コード

void func_NG(unsigned char ucHoge)
{
    float cnt;

    for (cnt = 0.1f; cnt <= 1.0f; cnt += 0.1f) {
        // NG, 0.1は2進では表現できず、期待通りの回数、ループしない可能性あり.
    }
}

適合コード

void func_OK(unsigned char ucHoge)
{
    int cnt;

    for (cnt = 1; cnt <= 10; cnt += 1) {
        // OK. 期待通り10回ループする.
    }
}

符号無し整数(unsigned)と0との大小の比較をしない

符号無し整数は必ず0以上なので、0との大小比較は常に真となるので意味がありません。符号無し整数を0と大小比較しないでください。

違反コード

void func_NG(unsigned char ucHoge)
{
    if (ucHoge >= 0) // 必ず真になる.
        return 1;
    else
        return 0; 
}

適合コード

void func_OK(signed char scHoge)
{
    if (scHoge >= 0) 
        return 1;
    else
        return 0; 
}

常に真、もしくは常に偽の条件文は使用しない

必ず真になるような条件分は無意味なので記述しないでください。コーディングミスなのか他人には判断しづらく、バグにつながります。また、コンパイラにそうのように解釈される記述もしないでください。

違反コード

int func_NG(void)
{
    int flag;

    flag = 1;

    if (flag == 1)  // 必ず真.コンパイラがelse節を抹消する可能性大.
        return 1;
    else
        return 0; 
}

適合コード

int func_OK(void)
{
    volatile int flag;

    flag = 1;

    if (flag == 1)  // volatileがあるので必ず真とは限らない.
        return 1;
    else
        return 0;
}

sizeof演算子には、データを変更する式・関数コール・およびvolatile変数の参照を行う式は用いない

sizeof演算子にわたされた式は、実行されません。sizeof演算子にデータを変更する式・関数コール・およびvolatile変数の参照を行う式は渡さないでください

違反コード

void func_NG(void)

{
    int i, j;
    i = 10;
    j = sizeof(i++);
    // sizeofではインクリメントは実行されないのでiは10のまま.
}

適合コード

void func_OK(void)
{
    int i, j;
    i = 10;
    i++;
    j = sizeof(i);
    // iは11になる.
}

条件分で真との比較をしない。偽と比較する

条件文(if文など)では
偽:0
真:非0 (1とは限らない)
として扱います。

このため、仮に真を1だと仮定して条件文で真と比較すると、期待通りに動作しない可能性があります。
条件文では常に偽と比較するようにしてください。
また、規格上、論理式では真なら必ず1を返しますが、統一性のため、論理式の場合も偽と比較してください。

違反コード

#include 

#define IS_TRUE   1

int main(void)
{
    int x, y, z;

    x = isdigit('5');

    if (x == IS_TRUE) {
          /* isdigit は真なら非0を返すが、1とは限らない。
           * なのでここにはこない可能性がある。
           */
    } else {
          ;
    }

    y = 2;
    z = 2;

   if ((y == z) == IS_TRUE) {
        /* (y == z) は論理式なので、真なら必ず1を返す。
         * なのでここに必ず来る。
         * だが、真との比較は間違えのもとなのでコーディング規約的にはNG.
         */
   } else {
         ;
   }

    return 0;
}

適合コード

#include 

#define IS_FALSE  0

int main(void)
{
    int x, y, z;

    x = isdigit('5');

    if (x != IS_FALSE) {
          /* 期待通りかならずここにくる
           */
    } else {
          ;
    }

    y = 2;
    z = 2;

   if ((y == z) != IS_FALSE) {
          /* 期待通り必ずここにくる
           */
   } else {
         ;
   }

    return 0;
}

無限ループにはwhile(1)でなくfor(;;)を使う

while(1)でもfor(;;)でも挙動は同じす。ですがwhile(1)の場合は「条件が定数なので常に真」とコンパイラや静的解析ツールが警告を出すことがあるので、無限ループにはfor(;;)を使ってください。

違反コード

void hoge_task(void)
{
    while(1) {
        // 略.
    }
}

適合コード

void hoge_task(void)
{
    for(;;) {
        // 略.
    }
}

除算をするときは0で割らない

数値で0で割ると、未定義の動作になります。ハングするかもしれませんし何も起こらないかもしれません、0が返ってくるかも知れませんし負の値が返ってくるかも知れません。
除算をするときには割る数が0か否かを確認するようにしてください。

違反コード

unsigned int my_div(unsigned int x, unsigned int y)
{
    return x / y;  // NG. yが0だった場合未定義の動作になる.
}

適合コード

unsigned int my_div(unsigned int x, unsigned int y)
{
    if (y == 0)
        return 0;
    else
        return x / y;  // OK. 直前でyの値をチェックしているのでyは必ず非0.
}

算術演算子の前後にはスペースを入れる

スペースは入れても入れなくても同じ、と考えられがちですが実際はそうでない場合もあります。コンパイラの字句解析は、最長一致の原則で行われます。つまり、複数の候補があった場合、できるだけ字句が長くなうように軸解析していきます。たとえば、 z=x+++y; という式は ‘z’ ‘=’ ‘x’ までは一意に解析できますが、その次は ‘+’ ‘と ‘++’ のどちらとも取りえます。この場合コンパイラは、より長い ++ と判断します。結果、z = x++ + y; と判断されます(z = x + ++y; でなく)。もしz = x++ + y; でなく z = x + ++y; と判断してもらいたい場合は、プログラマが明示的にスペースを入れてあげる必要があります。
コンパイラに字句の区切りを正しく伝えるために、適切にスペースを使う必要があります。とくに算術演算子は他の字句の一部として使用されることがあるため注意が必要です。簡単のため、算術演算子を使う場合は無条件で前後にスペースを入れるようにしてください。(K & R スタイルで書いておけば問題ないです)

違反コード

int func0(int x, int y)
{
    int z;

    /*
     * z = x++  +  ++y; のつもりだが、実際は
     * z = x++  ++  +y; と解釈されるのでコンパイルエラーとなる
     */
    z = x+++++y;
    return z;
}

int func1(int *px, int *py)
{
    int z;

    //
    // 除算の"/"とポインタ演算子の"*"の間にスペースがないので,
    // コメントの開始の "/*" と判断されるのでコンパイルエラーとなる.
    //
    z = *px/*py;
    return z;
} 

適合コード

int func(int x, int y)
{
    int z;

    /*
     * 明示的に "+" 演算子の前後にスペースを入れたので,
     * 期待通り z = x++  ++  +y; と解釈される
     */
    z = x++ + ++y;
    return z;
}

int func1(int *px, int *py)
{
    int z;

    // 除算の"/" の前後にスペースがあるので、
    // 期待通り *px と *py との除算と判断される
    z = *px  /  *py;
    return z;
}

以上、演算と式のコーディング規約でした!

他のコーディング規約は下記のページからたどれます。ぜひ見てみてくださいね

組込みソフトウェア向けC言語コーディング規約
どうも。ながやすです。 組込みソフト(ファームウェア)向けのc言語コーディング規約を書いています。これをみておけばcの文法的なミス減らせるかなと思います。また、C言語のベテランの方もひとつくらい...