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