C言語のプログラマーは誰でも同じ短いリストの間違いに出会います。たいていは最初の週に、そしてときには10年後にもう一度。これらをカタログ化する価値があるのは、その多くが間違いを説明してくれないエラーメッセージを出すか、もっと悪いことに何のメッセージも出さないからです。
以下の各項目は、症状、その背後にある原因、そして対処法という形になっています。
セミコロンの抜けと連鎖
症状: 大量のエラーが、どれも正しく見える行に対して報告される。
program.c:6:5: error: expected ';' before 'printf'
program.c:7:5: error: expected declaration specifiers before 'return'
原因: C言語は文を ; で終わらせます。1つ忘れるとコンパイラは次の行を現在の行に貼り付け、つながったテキストが意味をなさなくなった場所 - 通常は次の行 - で混乱を報告します。
int main(void) {
int x = 5 /* セミコロンが抜けている */
printf("%d\n", x);
return 0;
}
対処法: エラーがまとめて出たら、最初の1つだけを直して再コンパイルします。その後のものはすべて余波かもしれません。そして常に、コンパイラが指した行の1つ上を確認しましょう。
同じ連鎖は、閉じていない波かっこや終わっていない /* コメントからも起きます。この場合、エラーは数十行も離れた場所に現れることがあります。
もう1つ注意: if、for、while のヘッダーの後や、関数定義の閉じ波かっこの後にはセミコロンを付けません。if (x > 0); は問題なくコンパイルされ、何もしません - その ; が本体のすべてなのです。
比較のつもりの代入
症状: 常に真になる条件、あるいは不思議に値が変わる変数。
int x = 5;
if (x = 10) { /* 10 を代入し、その 10 を判定する -> 真 */
printf("x is ten\n"); /* 常に表示される。x は 10 になっている */
}
原因: = は代入、== は比較です。代入の値は代入された値なので、if (x = 10) は 10 を判定することになり、これは非ゼロ、つまり真です。人がそう書きたいこともあるので、コンパイラはこれを受け入れます。
対処法: あらゆる条件では == を使い、-Wall を有効にしてコンパイラに警告させましょう:
warning: suggest parentheses around assignment used as truth value
本当に条件の中で代入したい場合 - while ((c = getchar()) != EOF) がよくある例です - 余分なかっこがその意図を示し、警告も消えます。
関数の暗黙の宣言
症状:
warning: implicit declaration of function 'printf'
warning: implicit declaration of function 'malloc'
...そしてときには、その後に奇妙な実行時の結果やリンクエラーが続きます。
原因: コンパイラが、宣言を持たない関数の呼び出しに出会いました。C99 より前の方言では、その関数は int を返すと仮定して処理を続けました。64ビットシステムではこの仮定により、返されたポインタが32ビットに切り詰められます - これが、<stdlib.h> の書き忘れが malloc をクラッシュに変える仕組みです。
対処法: 正しいヘッダーをインクルードしましょう。
| 関数 | ヘッダー |
|---|---|
printf, scanf, fopen | <stdio.h> |
malloc, free, atoi, rand, exit | <stdlib.h> |
strlen, strcpy, strcmp | <string.h> |
sqrt, pow, fabs | <math.h> |
isdigit, toupper | <ctype.h> |
time | <time.h> |
自分で書いた関数の場合、同じ警告は定義より前に呼び出したことを意味します。main の上にプロトタイプを置きましょう:
& のない scanf
症状: 入力時にプログラムがクラッシュする、あるいは何も読まず変数が変わらないままになる。
int age;
scanf("%d", age); /* & が抜けている: アドレスではなく値を渡している */
原因: scanf はあなたの変数の中に書き込むので、変数のアドレスが必要です。age を渡すと、そこに入っていたゴミの値が渡され、scanf はそれを書き込み先として扱います。通常はセグメンテーション違反になります。
対処法: 変数の前に & を付けます - すでにアドレスである配列を除く、すべての型に対して:
同じ系統で隣り合う2つの罠: scanf で double には %lf を使うこと(そこでの %f は float を意味し、float 分のバイトを double に書き込むと値が壊れます)、そして %s には必ず幅を指定すること - 50バイトのバッファなら %49s - でなければ長い単語があふれます。
さらに良いのは、fgets で1行まるごと読んでから解析することです。これならあふれることがなく、バッファに余分な入力が残ることもありません。どちらも scanf で詳しく扱います。
== による文字列の比較
症状: 明らかに一致する2つの文字列が、異なるものとして比較される。
char a[] = "hello";
char b[] = "hello";
if (a == b) { /* 2つのアドレスを比較している: 偽 */
printf("same\n");
}
原因: C言語の文字列は値ではなく、先頭の文字へのポインタです。== はそのポインタを比較します。同じテキストを持つ2つの配列は別々のアドレスにあるので、この判定は偽になります。(ややこしいことに、同一のリテラル同士の比較は真になることがあります。コンパイラがコピーを1つだけ格納することがあるためで、これがこのバグを断続的なものにします。)
対処法: strcmp を使い、等しいときに 0 を返すことを覚えておきましょう:
逆向きの読み違いも人を引っかけます: if (strcmp(a, b)) は文字列が異なるときに真になります。非ゼロの結果は「等しくない」を意味するからです。常に == 0 を明示的に書きましょう。
整数除算
症状: 平均が 0 になる、パーセンテージが常に 0 か 100 になる、比率から小数部が消える。
int correct = 7, total = 10;
double score = correct / total; /* 0.7 ではなく 0.0 */
原因: 両方のオペランドが int なので、結果が double に代入される前にC言語は整数除算を行って切り捨てます。7 / 10 は 0 で、0 を double に変換すると 0.0 です。
対処法: 除算の前に、片方のオペランドを浮動小数点数にします:
片方のオペランドをキャストすれば、もう一方は自動的に昇格します。結果をキャストしても遅すぎます - 切り捨てはすでに起きています。型キャストを参照してください。
同じ罠は、中点を求める (a + b) / 2 や、x の値によらず常に 0 になる 1 / 2 * x のような式にも潜んでいます。
return の欠落や間違い
症状: 関数がもっともらしく見える誤った数値を返し、実行のたび、ビルドのたびに値が変わる。
int add(int a, int b) {
int sum = a + b;
/* return 文がない */
}
原因: void でない関数の末尾に、return せずに到達すると不定の値になります - 実際には戻り値レジスタにたまたま入っていたものです。呼び出し側がそれを使うと未定義動作になります。
より気づきにくい形は、一部の経路でだけ return するものです:
int classify(int n) {
if (n > 0) return 1;
if (n < 0) return -1;
/* n == 0 は末尾まで落ちてしまう */
}
対処法: すべての経路で return し、-Wall を付けてコンパイルしましょう - GCC の「control reaches end of non-void function」がどちらの形も捕まえてくれます。
未初期化の変数
症状: ゴミの出力、あるいは実行ごと・最適化レベルごとに変わる結果。
int total; /* スタックにあったものが何であれ入っている */
for (int i = 1; i <= 5; i++) {
total += i; /* ゴミに足している */
}
printf("%d\n", total); /* とんでもない数値 */
原因: ローカル変数はゼロクリアされません。グローバル変数や static 変数は自動的にゼロになりますが、ローカル変数はそのスタックアドレスにすでにあったバイトから始まります。
対処法: 宣言した場所で初期化しましょう。コストはゼロで、この種のバグをまるごと取り除けます:
-Wall -Wextra はこれらの多くを警告してくれ("may be used uninitialized")、-fsanitize=memory や valgrind が残りを捕まえます。これが特に厄介なのは、未初期化のポインタがまっすぐセグメンテーション違反につながるからです。
Off-by-one
症状: 最後の要素が処理されない、あるいは1つ多く触れてしまってプログラムが後でおかしくなる。
int arr[5];
for (int i = 0; i <= 5; i++) { /* 存在しない arr[5] に触れている */
arr[i] = i;
}
原因: n 要素の配列のインデックスは 0 から n - 1 までです。<= は1回余分に回ります。
対処法: i < n のパターンを使い、数値を2度書くのではなく配列から n を計算しましょう:
文字列版 - '\0' の場所を忘れること - は同じ間違いが別の帽子をかぶったもので、"hello" を入れる char word[5] はバッファオーバーランです。
知っておく価値のある小さな2つ
ループヘッダーの後のセミコロン。 for (int i = 0; i < 10; i++); に波かっこのブロックが続くと、ループは何もせず10回回り、それからブロックが1回実行されます。これは何の警告もなくコンパイルされます。
ポインタに対する sizeof。 関数の内側では配列の仮引数はポインタなので、sizeof(arr) は配列ではなくポインタのサイズ(8バイト)になります。長さは別の引数として渡しましょう:
これらの大半を防ぐ習慣
最初のプログラムから、警告を有効にしてコンパイルしましょう:
gcc -Wall -Wextra -g program.c -o program
-Wall -Wextra は、条件の中の代入、return の欠落、未初期化の読み取り、未使用の変数、引数と一致しない printf の指定子を捕まえます。開発中に -fsanitize=address,undefined を加えれば、残りのほとんどを起きたその瞬間に捕まえられます。
すべての警告を、まだ踏んでいないエラーとして扱いましょう。警告なしでコンパイルできるC言語のプログラムが正しいと保証されるわけではありません - しかし、クラッシュするC言語のプログラムのほとんどは、その前に何かを警告していたのです。
よくある質問
C言語の「implicit declaration of function」はどういう意味ですか?
コンパイラが、宣言を一度も見たことのない関数の呼び出しに出会ったということです。ほとんどの場合は #include の書き忘れで、printf には <stdio.h>、malloc には <stdlib.h>、strlen には <string.h> が必要です。自分で書いた関数を定義より前に呼んだ場合にも出ますが、これは main の上にプロトタイプを置けば解決します。
C言語で、問題なさそうな行にエラーが出るのはなぜですか?
たいていは本当の間違いがその前の行にあるからです。セミコロンの抜け、閉じていない波かっこ、終わっていないコメントがあると、コンパイラは2行を1行として読み進め、最終的に解析できなくなった場所で混乱を報告します。まずは報告された行の1つ上を確認しましょう。
C言語で文字列を == で比較できないのはなぜですか?
C言語の文字列は char *、つまりポインタだからです。== は指している文字ではなく2つのアドレスを比較します。同じ内容の文字列でも別々の場所にあれば結果は偽になります。<string.h> の strcmp(a, b) == 0 を使いましょう。内容が一致すると 0 を返します。
C言語で 5 / 2 が 2 になるのはなぜですか?
両方のオペランドが整数なので、C言語は整数除算を行い小数部を切り捨てるからです。2.5 を得るには片側を浮動小数点数にします: 5.0 / 2、変数なら (double)a / b です。結果をキャストしても遅すぎます - (double)(5 / 2) は 2.0 です。