第1章
演算子/区切り子/構文の落とし穴/式/関数/引き数/配列/ポインタ/文字列/構造体/マクロ/移植性/浮動小数点
Cで間違いやすいコーディング例

 Cはコーディングの自由度が大きく,特殊な演算子も多いため,他の言語とくらべて間違ったコーディングをしてしまうことが少なくありません.本章ではCの初心者A君にN先輩が手ほどきする形でCで間違いやすいコーディング例のいくつかを紹介していきます. (筆者)


演算子編
―― =と==,&&と&,>>と>,演算子の優先順位

=と== ――等価と代入

A君 :先輩,おはようございます.

N先輩 :おはよう.どう,Cプログラミングは少しは慣れたかい?

A君 :はい.少しは慣れましたが,ミスが多くて….どこが間違っているのか,わけがわからなくなることも多くあります.

N先輩 :そうか.じゃあ間違いやすいコーディング例をいくつか示してあげよう.今後のプログラミングの際に役立ててよ.

A君 :ぜひ,お願いします.

コンパイラからの警告はないが…

N先輩 :まずはリスト1のプログラムを見てごらん.

〔リスト1〕=と==
#include <stddef.h>

char *pointer;
int  flag;

void func(void)
{
    if (pointer = NULL) /* ←もしpointerがNULLであれば */
        flag = 1;       /* ←flagを1にする */
}

・ 等価のつもりが代入になっていた…

N先輩 :VC++ 5.0(Visual C++ 5.0)で

   cl /c /Ot /W3 test.c

 のようにコンパイルしても“warning”も“error”も出ないプログラムなんだけど,たぶん間違ってコーディングしているんだ.

A君 :これなら僕もやったことがありますよ.if文の中の=が==の間違いなんですよね.

N先輩 :そうだね.僕もやったことがあるけど,=と==のコーディングミスはたいていの人が経験していると思うよ.実はこのミスを予防するコーディング方式があるんだけど,わかるかい.

・ =と==のミスを避ける方法?

A君 :うーむ…ちょっとわからないです.

N先輩 :実はね,この=と==のミスを避けるために「定数との比較では定数を左側に書く」といった対策をとっている人も少なくないんだよ.たとえばね,

   if (NULL = pointer)

      /* ... */;

 のように間違えると代入文の左に定数がくることはないから,コンパイラが指摘してくれるんだ.

A君 :そう言われてみればそうですね.うまい方法ですけど,コーディングを見ると,ちょっと,変な感じがしますね.

N先輩 :確かに,このようなスタイルはプログラムの読みやすさを若干損ねるけど,プロは確実に動くことのほうを重視するものなんだよ.といっても,僕はこのようなコーディングをしないんだけどね(プロと呼べないかもしれませんね).

A君 :プロではないからですか?

・ いまのコンパイラでは“warning”が出るが…

N先輩 :えっ,そんな言い方をするかね? まぁ.いいや.一つ目の理由は,A君が感じたようにちょっと変な感じがするっていう点なんだ.プログラムを読み返すときに違和感があって,そっちのほうに神経が向く可能性があって,肝心のところを見逃すってこともなきにしもあらずだからね.二つ目の理由は,両方変数のときには適用できないってことかな.それで,三つ目のもっとも正当な理由は,最近のコンパイラでは“warning”を出してくれるからだよ.たとえば,C++ Builder 6だったら,

   警告 W8060 test.c 8: おそらく不正な代入

(関数 func)

 という“warning”が出る.VC++ 5.0でも“warning”レベルを/W4に上げると,

   test.c(8) : warning C4706:

   条件式の比較値は,代入の結果になっています.

 という“warning”が出るんだ.だから,“warning”を無視しない習慣を付けておけば予防できるミスなんだよ.

A君 :でも,

   if (*p = *q)

    /* ... */;

 というようなコーディングをしても“warning”が出てめんどうだと思ったことがありますけど.

N先輩 :確かに,“warning”はよけいなお節介と感じる場合も少なくないけど,この“warning”を消すためには,

   if ((*p = *q) != '\0')

    /* ... */;

 といったコーディングをすればいいんだ.“warning”も数が多いと“warning”に対する感覚が麻痺してくるから,できるだけ潰しておいたほうがあとあとケアレスミスで悩むことも少なくなると思うよ.

A君 :はい.そうするようにします.

● &&と& ――論理演算子とビット演算子

N先輩 :A君は&&と&の違いはわかってるかい?

A君 :&&は論理演算子で真(“0”以外),偽(“0”)を対象にした論理的なANDで,&はビットごとのANDを取る演算子ですよね.だから,

   0x02 && 0x08

 は両方の値とも論理値としたら“0”以外だから,結果は真つまり“1”となり,

   0x02 & 0x08

 はビットごとのANDだから“0”となるんですよね.

N先輩 :そういうことだね.それじゃ,リスト2の(1)と(2)はどんな違いがあるか説明できるかい.

A君 :(1)はfunc1()とfunc2()の論理的なANDを取って,flag1は結果として“0”または“1”の値を取りますけど,(2)はfunc1()とfunc2()のビットごとのANDを取りますよね.

N先輩 :だいたい合ってるけど.うーむ,それじゃね,func1()とfunc2()がともに“0”か“1”の値しか返さないときにどういう違いがあるかわかるかな.

A君 :func1(),func2()がともに“0”か“1”の値しか返さないんだったら,両方が“1”を返したときだけflag1もflag2も“1”がセットされるから,とくに違いはないんじゃないですか.

N先輩 :それがね,一つだけ大きな違いがあるんだ,

A君 :それは何ですか.

・ &演算子ではつねに後述の関数を評価する

〔リスト2〕&&と&
int func1(void);
int func2(void);

int flag1, flag2;

void func(void)
{
    flag1 = func1() && func2(); /* (1) 論理AND   */
    flag2 = func1() &  func2(); /* (2) ビットAND */
}

N先輩 :リスト2の例でいえば

   func1() && func2()

 はfunc1()が偽(“0”)を返したときはfunc2()を実行しないけど,

   func1() & func2()

 はfunc1()とfunc2()がかならず実行されるという点が大きく違うんだ.

A君 :へえー,そうなんですか.

N先輩 :だから,両方の関数を実行させたいときにはわざと(2)のようなコーディングをすることもあるんだけど,ちょっとわかりにくくなるから,できれば

   ftmp1 = func1();

   ftmp2 = func2();

   flag2 = ftmp1 && ftmp2;

 といったコーディングをしたほうが良いと思うけれどね.これだと二つの関数が実行されることは,明確にわかるよね.

A君 :なるほどね,わかりました.この関係は,||(論理和結合演算子)と|(ビット和演算子)の場合でも同じことですよね.

N先輩 :そうだよ.

   func1() || func2()

 の場合はfunc1()が真(非“0”)を返したときにはfunc2()を実行しないけど,

   func1() | func2()

 はfunc1()とfunc2()がかならず実行されるんだよ.

● >>と> ――右シフトと不等号

〔リスト3〕>>と>
unsigned udat1, udat2;

void func(void)
{
    udat1 = udat2 > 1;
}

N先輩 :それじゃ,リスト3も簡単なコーディングミスの例だけどわかるかな.

A君 :プログラムを見るかぎりでは,右不等号の>ではなくて,右シフトの>>にすべきではないかと思います.

N先輩 :そうだね.>>と>のコーディングミスは1ビット右シフトの,

   udat >> 1

 を,

   udat > 1

 と書いてもコンパイルエラーにならないし,けっこう気づきにくいミスなんだ.これはコンパイルスイッチの“warning”レベルを最高にしても“warning”が出ないから,自分で気をつけるしかないと思うよ.

A君 :はい,そうします.

● 演算子の優先順位

〔リスト4〕演算子の優先順位(間違い)
/*
    BCD 2 バイト → 10 進文字列 (右詰め & '\0' ターミネート)
*/
void bcdtona(unsigned val,		/* 符号無整数          			 */
             char *str, 		/* 大 → 小 10進文字列格納アドレスサイズ >= n+1	 */
             int  n)            	/* 桁数           				  */
{
    str += n;                 	/* ストリング最後のアドレス+1 セット 	*/
    *str-- = '\0';
    do {
        *str-- = (char)(val & 0x0f + '0'); /* 最下位 4ビット Ascii 変換 */
        val >>= 4;
    } while (--n > 0 && val > 0);
    while (--n >= 0)
        *str-- = ' ';       	/* スペース セット            		  */
}

N先輩 :リスト4はBCD 2バイトを10進文字列に変換するプログラムだけど,間違いがあるんだ.どこが間違ってるかわかるかい.

A君 :ふーむ…,最下位4ビットASCII変換のところの,

   val & 0x0f + '0'

 があやしそうですね.

N先輩 :演算子の優先順位によるトラブルは中級ユーザーでも経験するものだけど.まあ,演算子の優先順位が100%頭の中にはいっている人は少ないだろうからね.僕が経験したものではリスト4の,

   *str = (char)(val & 0x0f + '0');

 や,

   val1 = val2 << 4 + val3;

 などがあるけど.+,−の優先順位は意外に高くて,これらは,

   *str = (char)(val & (0x0f + '0'));

   val1 = val2 << (4 + val3);

 と解釈されるんだ.だから,リスト4では,

   val & 0x0f + '0'

 は,

   (val & 0x0f) + '0'

 のように()でくくらないといけないんだ(表1).

〔表1〕演算子の優先順位

A君 :そうですね.

N先輩 :演算子の優先順位はポインタが絡んでくると一層ややこしくなって,

   *ptr[n]

 だと[]の優先順位が高いため,

   *(ptr[n]) または *(*(ptr + n))

 となるんだ.ポインタそのものが読みにくい性格をもっているから,二重三重のポインタではリストを見直しただけでは気づかないこともあるけどね.優先順位があやふやな場合はマニュアルやオンラインヘルプで確かめることも必要だけど,()でくくるっていう習慣も付けておいたほうが良いだろう.もっとも,()が多くなりすぎると式が見にくくなるから,一時変数を使用して式を分けるというのも対策の一つかもしれないね.最近はコンパイラの最適化の性能が上がってきてるから,式を分けても生成コードが悪くなることは少なくなってきたからね.

A君 :はい,わかりました.

区切り子(セパレータ)編
――カンマ(,)の打ち忘れ,余分なセミコロン(;),足りないセミコロン(;)

● カンマ(,)のうち忘れ

N先輩 :リスト5は完全なバグなんだけどどこが悪いかわかるかい.

〔リスト5〕カンマ(,)のうち忘れ
#include <stdio.h>

void func(void)
{
    static char *msg[] = {
        "abc",
        "def"
        ""
    };
    char **pmsg = msg;

    while (**pmsg)
        printf(*pmsg++);
}

以降の内容は本誌を参照ください

インデックス
プロローグ
◆第1章

今月号特集トップページへ戻る


Copyright 2004 中島 信行