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 { // それ以外の処理 }と使用するのが、より自然な気がします。この方が可読性もちょっとだけ増すと思います。
可読性に関して言えば、ご指摘の通りです。