ありがちなC言語プログラムの間違い

作成:2005年11月24日

補遺:2005年11月24日

改訂:2005年12月3日

改訂:2005年12月13日

吉田誠一のホームページ   >   ソフトウェア工学   >   技術コラム   >   プログラミング

ここでは、C言語によるプログラミングでありがちな間違いを紹介します。

C言語によるプログラミングで犯しやすい間違いは、 C言語 FAQ 日本語訳C言語のよくある間違い に数多くの例が紹介されていますので、一度、目を通してみることをお勧めします。

ここでは、これらのページに見当たらなかった間違いの例を紹介します。

なお、C言語だけでなく、基本的にC++言語でも同じことが言えます。

目次

  1. is系関数に-1〜255以外の値を渡すと不定になる
  2. サイズの計算は0より小さくならない
  3. 実数の計算エラーは、整数化によって失われる
  4. 実数の計算では数学の定理は成り立たない
  5. enum型の値は重複もできてしまう
  6. bool型とBOOL型の一致判定では、「==」を使ってはいけない
  7. Win32 APIのBOOL型の戻り値は、FALSE以外は不定になる

is系関数に-1〜255以外の値を渡すと不定になる

入力された文字の中で、数字とアルファベットだけを抜き出そうと思って、次のように書きました。

void func ( const char *str ) {
        int i, len;
        len = strlen(str);
        for (i = 0 ; i < len ; i++) {
                if (isalnum(str[i])) {
                        putchar(str[i]);
                }
        }
}
        

どこに間違いがあるでしょうか?

C言語の標準ライブラリには、文字の種類を判別する一連の関数が用意されています。これらのis系関数は、ctype.hに定義されています。

isalnum isalpha isascii
isblank iscntrl iscsym
iscsymf isdigit isgraph
islower isprint ispunct
isspace isupper isxdigit

is系関数は、文字(0〜255)とEOF(-1)に対して、正しい結果を返すように規定されています。しかし、それ以外の値を渡した場合は、不定とされています。

ctype.hでは、is系関数の引数はint型と宣言されています(char型ではありません)。ここで、引数にchar型の値を渡そうとすると、問題が発生します。

現在の主なCコンパイラでは、char型は符号つき1バイト整数型ですので、取り得る値は、-128〜127となります。一方、int型は符号つき4バイト整数型です。

is系関数を呼び出す際に、渡されたchar型の値は、int型に変換されます。その結果、is系関数は、-128〜127の値を受け取ることになります。

正しい結果を得るためには、unsigned char型へのキャストを付けて、符号つき1バイト整数型→符号なし1バイト整数型→符号つき4バイト整数型、という具合に、2段階の変換を行います。

正しいソースコードは、下記の通りになります。

void func ( const char *str ) {
        int i, len;
        len = strlen(str);
        for (i = 0 ; i < len ; i++) {
                if (isalnum((unsigned char)str[i])) {
                        putchar(str[i]);
                }
        }
}
        

is系関数の名前で検索すると、多くのWWWページで紹介されているサンプルプログラムに、この間違いが見られます。

サイズの計算は0より小さくならない

10バイトのバッファに文字列が収まるかどうかを、下記の方法で調べてみました。

char *s = "0123456789abcdefg";

if (10 - strlen(s) < 0) {
        puts("長すぎて入らない!");
} else {
        puts("大丈夫、入る。");
}
        

また、10バイトのバッファにn個の整数が収まるかどうかを、下記の方法で調べてみました。

int n = 4;

if (10 - sizeof(int) * n < 0) {
        puts("多すぎて入らない!");
} else {
        puts("大丈夫、入る。");
}
        

どこに間違いがあるでしょうか?

strlen関数の戻り値や、sizeof演算子の値は、size_t型と定義されています。これは、符号なし整数です。

符号なし整数と符号つき整数とが混在する演算では、符号なし整数に統一されます。そのため、上記の例はいずれも、if文の中の演算結果は、符号なし整数となります。ですから、当然、0より小さくなることはありません。

正しいソースコードは、下記の通りになります。

char *s = "0123456789abcdefg";

if (10 < strlen(s)) {
        puts("長すぎて入らない!");
} else {
        puts("大丈夫、入る。");
}
        
int n = 4;

if (10 < sizeof(int) * n) {
        puts("多すぎて入らない!");
} else {
        puts("大丈夫、入る。");
}
        

strlen関数の戻り値や、sizeof演算子の値を、int型にキャストしても構いません。

実数の計算エラーは、整数化によって失われる

複雑な計算を行う、下記のようなプログラムを作りました。

double func ( ) {
                :
        /* 複雑な計算 */
                :
}

int main ( int argc, char **argv ) {
                :

        double result = func();
        printf("result = %d.\n", (int)result);

                :
}
        

どこに間違いがあるでしょうか?

実数を整数に不用意にキャストすると、計算エラーを見逃してしまいます。

一般的には、実数は、IEEE 754形式の浮動小数点数として表現されています。

浮動小数点数は、ふつうの値の他に、下記の3種類の特殊な値を表せます(値は倍精度の場合)。

意味 指数部 仮数部 符号部
NaN 非数 (Not a Number) 2047 0以外(不定) 不定
-Inf 負の無限大 2047 0 1
+Inf 正の無限大 2047 0 0

実数の計算では、不正な計算を行っても、プログラムは異常終了しません。例えば、0での除算を行っても、ただ、float型やdouble型の変数に、上記の特殊な値が格納されるだけです。実数の計算結果をそのまま表示すれば、計算エラーが起きたことが分かります。

しかし、int型のような整数は、不正な計算結果を表せません。そのため、実数の計算結果を整数に変換すると、正常な値と区別できなくなります。計算エラーが起きても、分からなくなってしまいます。

例えば、不正な計算結果持つdouble型の変数を、int型にキャストすると、下記のようになりました。

double型変数の値 int型に変換した後の値 (Visual C++) int型に変換した後の値 (gcc)
NaN 0 0または-2147483648
-Inf 0 0
+Inf 0 0

前述の例では、複雑な計算を行うfunc関数のアルゴリズムに、もしかしたら、間違いがあるかもしれません。もしもバグがあって、計算エラーが起きたら、func関数はNaNを返すことでしょう。

しかし、この例では、結果を表示する前に整数に変換してしまっています。すると、func関数がNaNを返しても、画面には0と表示される可能性が高いです。

int型にキャストすると、NaNやInfが、0という、ありきたりの値になってしまう点が厄介です。計算エラーが起きているのに、ユーザにはそれっぽい、でも嘘の結果を見せてしまうことになりがちです。テストを行っても、一見すると正しく計算されたように見えるので、バグを見逃しやすくなります。

実数を整数にキャストする時は、計算の結果が不正でなかったかどうかを判定する必要があります。

正しいソースコードは、下記の通りになります。

#include <float.h>

double func ( ) {
                :
        /* 複雑な計算 */
                :
}

int main ( int argc, char **argv ) {
                :

        double result = func();
        if (_finite(result))
                printf("result = %d.\n", (int)result);
        else
                printf("ERROR!\n");

                :
}
        

_finite関数は、処理系によっては定義されていないかもしれません。C99では、math.hにisfiniteというマクロ関数が定義されています。

実数の計算では数学の定理は成り立たない

平均値と標準偏差を計算するプログラムを、下記のように作りました。

void average_stddev ( double *data, int count ) {
        double a, a2, ave, var, std_dev;
        int i;

        a = a2 = 0.0;
        for (i = 0 ; i < count ; i++) {
                a += data[i];
                a2 += data[i] * data[i];
        }

        ave = var = std_dev = 0.0;
        if (count > 0) {
                ave = a / (double)count;
                var = a2 / (double)count - ave * ave;
                std_dev = sqrt(var);
        }
        printf("ave = %.16f  std_dev = %.16f\n", ave, std_dev);
}
        

どこに間違いがあるでしょうか?

実数の計算では、誤差がつきものです。

double型の変数を比較する際に、次のように書いてはいけない、という話は有名です。

if (a == b)
        

しかし、注意するのは、比較の時だけではありません。意外なところにも落とし穴があります。標準偏差の計算は、良く見かけるケースの1つです。

データの2乗和の平均から、平均値の2乗を引くと、分散が得られます。分散は、数学的には必ず0以上となります。しかし、実数の計算では、誤差によって、分散の値が負になる可能性があるのです。

分散の平方根を取ると、標準偏差が得られます。しかし、分散が負になってしまうと、平方根の計算が不正になってしまいます。

例えば、LinuxやCygwin上のgccや、Visual C++ (VC++) では、下記の値の場合に、sqrtの結果がNaN (Not a Number) となりました。

引数
count 3
data[0] 0.2
data[1] 0.2
data[2] 0.2

データがすべて同じ値の場合は、この例の他にも、sqrtの結果がNaNになるケースがたくさん見つかります。

正しいソースコードは、下記の通りになります。

void average_stddev ( double *data, int count ) {
        double a, a2, ave, var, std_dev;
        int i;

        a = a2 = 0.0;
        for (i = 0 ; i < count ; i++) {
                a += data[i];
                a2 += data[i] * data[i];
        }

        ave = var = std_dev = 0.0;
        if (count > 0) {
                ave = a / (double)count;
                var = a2 / (double)count - ave * ave;
                if (var < 0.0)
                        var = 0.0;
                std_dev = sqrt(var);
        }
        printf("ave = %.16f  std_dev = %.16f\n", ave, std_dev);
}
        

enum型の値は重複もできてしまう

enum型を次のように定義してみました。

typedef enum {
        FinalFantasy = 10,
        FinalFantasyII,
                :
        FinalFantasyXII,

        DragonQuest = 20,
        DragonQuestII,
                :
        DragonQuestVIII,

        MarioBrothers = 30,
                :
} Game;
        

どこに間違いがあるでしょうか?

enum型では、ふつうは0から始まって1ずつ番号が増えていきます。しかし、この例のように、番号を指定することもできます。この時、間違えて同じ番号を割り当ててしまっても、コンパイラはエラーとは判定しません。enum型は、複数の列挙子が同じ値になっても良いのです。

この例では、「FinalFantasyXI」という列挙子と、「DragonQuest」という列挙子が、同じ20という値になってしまっています。

ゲームの種類を表示しようと、次のように書いてみます。すると、「FinalFantasyXI」の場合は、「ファイナルファンタジー」と「ドラゴンクエスト」の両方が表示されてしまいます。

Game g;

if (FinalFantasy <= g  &&  g <= FinalFantasyXII) {
        puts("ファイナルファンタジー");
}
if (DragonQuest <= g  &&  g <= DragonQuestVIII) {
        puts("ドラゴンクエスト");
}
        

if〜else〜文では、enum型の値が重複していても、気づきません。

正しいソースコードは、下記の通りになります。

Game g;

switch (g) {
        case FinalFantasy:
        case FinalFantasyII:
                :
        case FinalFantasyXII:
                puts("ファイナルファンタジー");
                break;
        case DragonQuest:
        case DragonQuestII:
                :
        case DragonQuestVIII:
                puts("ドラゴンクエスト");
                break;
}
        

switch文で書いておけば、もしも値が重複してしまった時には、コンパイルエラーとなって、気づくことができます。

bool型とBOOL型の一致判定では、「==」を使ってはいけない

2つの関数を呼び出して、同じ結果になった時に処理を行おうと思って、次のように書きました。

BOOL b1 = func1();
bool b2 = func2();

if (b1 == b2) {
        // 処理
}
        

どこに間違いがあるでしょうか?

WindowsのC++言語プログラムでは、真偽を表すデータ型として、bool型とBOOL型の2種類が使えます。

標準的なC++言語の関数はbool型を、Win32 APIはBOOL型を使っていますので、Windowsのプログラムでは、どうしてもこの2つが混在せざるを得ません。意味は同じなので、うっかりしたり、コピー&ペーストしたりすると、2つのデータ型を書き間違えてしまうこともあります。

しかし、bool型とBOOL型は、実際にはまったく異なります。

データ型 サイズ
bool 1バイト unsigned char
BOOL 4バイト signed int

例えば、次のプログラムを実行すると、b1とb2はどちらも真ですが、結果は「違う!」と表示されます。

BOOL b1 = -1;
bool b2 = -1;

if (b1) {
        puts("b1は真");
}
if (b2) {
        puts("b2は真");
}

if (b1 == b2) {
        puts("同じ。");
} else {
        puts("違う!");
}
        

bool型とBOOL型が混在するプログラムでは、どちらも真である、という判定に「==」を使うのは、安全ではありません。安全な一致判定は、下記の通りになります。

BOOL b1 = func1();
bool b2 = func2();

if ( (b1 && b2) || ! (b1 || b2) ) {
        // 処理
}
        

「&」や「|」ではなく、「&&」や「||」を使わなくてはいけません。

Win32 APIのBOOL型の戻り値は、FALSE以外は不定になる

MFCのCStringクラスの文字列が、空かどうかを、下記のように調べてみました。

CString s;

if (s.IsEmpty() == TRUE) {
        puts("空っぽ!");
}
        

どこに間違いがあるでしょうか?

Windowsで用意されているAPI関数には、BOOL型を返すものがたくさんあります。これは、MFCにも引き継がれています。

BOOL型は、実際にはint型と同じです。BOOL型の値を表す定数として、TRUE(=1)とFALSE(=0)が定義されています。しかし、偽を表す値は0しかありませんが、真を表す値としては、0以外のいかなる値でも取ることができます。

これらのAPI関数の戻り値をMSDNで調べてみると、偽の場合はFALSE、真の場合は0以外を返す、と定義されています。真の場合にTRUEが返されるとは、定義されていません。

Win32 APIのBOOL型の戻り値を判断する場合は、TRUEではなく、FALSEと一致するかどうか、を調べなくてはいけません。

正しいソースコードは、下記の通りになります。

CString s;

if (s.IsEmpty() != FALSE) {
        puts("空っぽ!");
}
        

なお、服部健太氏から、次のようなご指摘を頂きました。

CStringクラスのIsXXX()系は

if (s.IsEmpty()) {
        // 真の時の処理
} else {
        // それ以外の処理
}
            

と使用するのが、より自然な気がします。この方が可読性もちょっとだけ増すと思います。

可読性に関して言えば、ご指摘の通りです。

補遺:2005年11月24日

服部健太氏から、次のようなご指摘を頂きまして、文章を修正しました。

最初のisxxxx()系の記述の中で、char型の表現範囲が-128〜127とありますが、ただのchar型が符号つきか符号なしかは、たしか処理系に依存したと思います。

Copyright(C) Seiichi Yoshida ( comet@aerith.net ). All rights reserved.