次は、C言語のプログラムの一部です。整数1つ分のメモリーを確保して数値を代入し、表示しています。
int *p = malloc(sizeof(int));
*p = 123;
printf("%d\n", *p);
123
って表示されると思います。
malloc()
は……
malloc()
を使うときは、こんな感じでif
文が必要なはず……
int *p = malloc(sizeof(int));
if (p) {
*p = 123;
printf("%d\n", *p);
}
malloc()
とif
文のセットは、よく出てきますよね。いつもこんな書き方になってるなーって思って。
if
文が必要なのかは説明できるかしら?
malloc()
でちゃんとメモリーを確保できたかチェックするため、ですかね。
NULL
)を返すっていうのが、malloc()
の仕様ね。
NULL
)じゃないことを確認しないといけないんですね。
メモリーは、限られたリソースです。ほかのプログラムがたくさん動いているときなどは、必要なだけ確保できないことも考えられます。
このような、プログラムを実行しているときの状態によって起こるかもしれないエラーを「実行時エラー(ランタイムエラー)」といいます。実行時エラーは、必ずチェックしなければなりません。malloc()
も、チェックが必要な関数の一つだということです。
とはいえ、上記のプログラムが実際にメモリーの確保に失敗するケースはほとんどないでしょう。現代的なOSには、「仮想メモリー」という仕組みが搭載されているためです。物理的なメモリーにハードディスクなどを組み合わせてデータの記憶場所をやりくりすることで、自由に使える仮想的なメモリー空間を作り出しているのです。
malloc()
が実行時エラーを起こすことはけっこうあったのよ。
if
文を書き忘れるっていうのはシビアな問題だったの。
エラーの発生頻度が低いからといって、それがエラーチェックをしなくてもいい理由にはなりません。むしろ頻度が低いエラーほど、実際に起こったときには重大なトラブルを引き起こすとも考えられます。
メモリーの確保以外にも、ファイルへの入出力や通信などは、いつ実行時エラーが発生してもおかしくない操作です。何かエラーを返す可能性のある関数を呼び出したら、必ず結果をチェックするよう心がけましょう。
assertと実行時エラー
malloc()
のチェックにもassertは使えると思う?
結論から言うと、malloc()
が起こすような実行時エラーのチェックに、assertを使うことはできません。assertはプログラムに含まれる欠陥、つまりバグを検出するためのものだからです。assertが必要なのは開発中だけなので、リリース用のバージョンでは無視されます。
これに対して、実行時エラーはリリース後にもチェックする必要があります。実行時エラーは「メモリーが足りなくなった」のような、プログラムを実行中の状況によって発生するエラーだからです。何かassertとは別の方法で、エラーの有無をチェックしなければなりません。
int *p = malloc(sizeof(int));
assert(p);
……
if
文でチェックするのがいいんですね。
int *p = malloc(sizeof(int));
if (p) {
/* 通常の処理 */
……
} else {
/* エラー処理 */
……
}
if
文など別の方法を使いましょう。
if
文は行数が多くなっちゃいますね……
エラーチェックで手を抜くとどうなる?
次のプログラムは、「謎のテキスト」を処理して「結果のテキスト」を抜き出すというものです。実行すれば、おそらく問題なく動作するでしょう。でも、エラーチェックをしていないために、いつクラッシュしてもおかしくないところが2カ所もあります。
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <assert.h>
/* 謎のテキストから結果のテキストを抜き出して返す。
* - pSecretText: 謎のテキスト
* 結果のテキストは、呼び出し側がfree()で解放すること。
* 結果のテキストを抜き出せない場合は、0(NULL)を返す。
* pSecretTextが0(NULL)の場合の動作は未定義。
*/
char *ProcessSecretText(const char *pSecretText) {
assert(pSecretText);
char *pText = 0;
int textLength = 0;
/* 謎のテキストを1文字ずつ処理 */
for (const char *pLetter=pSecretText; *pLetter!='\0'; pLetter++) {
/* 数字は読み飛ばす */
if (isdigit(*pLetter)) {
continue;
}
/* 結果のテキストに1文字追加 */
pText = realloc(pText, textLength + 2);
pText[textLength] = *pLetter;
pText[textLength + 1] = '\0';
textLength += 1;
}
return pText;
}
int main(void) {
char *pText = ProcessSecretText("47H5e3ll2o9!18");
puts(pText);
free(pText);
pText = 0;
return EXIT_SUCCESS;
}
ProcessSecretText()
関数に「謎のテキスト」を渡しているのが分かるかしら?
"47H5e3ll2o9!18"
ですね。ちょっと実行してみていいですか?
Hello!
Hello!
って出ましたよ!
1カ所目のエラーチェック
まずは、関数のコメントに注目してみましょう。「結果のテキストを抜き出せない場合は、0(NULL)を返す。」と書かれています。これこそ、実行時エラーですね。
ところが、呼び出し側ではこのエラーをチェックしていません。
ProcessSecretText()
の実行結果をチェックしていない!
char *pText = ProcessSecretText("47H5e3ll2o9!18");
puts(pText);
free(pText);
pText = 0;
このままでは、puts()
関数に0(NULL
)を渡してしまう恐れがあります。ProcessSecretText()
関数の戻り値を、次のようにif
文でチェックする必要があるでしょう。
ProcessSecretText()
の実行結果をif
文でチェック!
char *pText = ProcessSecretText("47H5e3ll2o9!18");
if (pText) {
puts(pText);
free(pText);
pText = 0;
} else {
/* 結果のテキストを抜き出せなかった! */
puts("Error");
}
2カ所目のエラーチェック
次は、ProcessSecretText()
関数の中身に注目しましょう。「結果のテキストに1文字追加」とコメントがある部分です。
ここでは、realloc()
関数が使われていますね。これは、サイズを指定してメモリーを確保し直す標準関数です。malloc()
関数と同様に、失敗すると0(NULL
)を返すのがこの関数の仕様です。
ところが、この戻り値がチェックされていません。
realloc()
の実行結果をチェックしていない!
/* 結果のテキストに1文字追加 */
pText = realloc(pText, textLength + 2);
pText[textLength] = *pLetter;
pText[textLength + 1] = '\0';
textLength += 1;
このままでは、メモリーを確保できなかったのに文字を書き込んでしまう恐れがあります。ここでもif
文を使って、realloc()
関数の戻り値をチェックする必要があるでしょう。
realloc()
の実行結果をif
文でチェック!
/* 結果のテキストに1文字追加 */
{
char *p = realloc(pText, textLength + 2);
if (p) {
pText = p;
} else {
/* メモリーを確保できなかった! */
free(pText);
pText = 0;
break;
}
}
pText[textLength] = *pLetter;
pText[textLength + 1] = '\0';
textLength += 1;
エラーチェックは「今やる」が基本
さて、2カ所にエラーチェックを追加すると、プログラムは次のようになります。これで「良いプログラムになった」といえるでしょうか?
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <assert.h>
/* 謎のテキストから結果のテキストを抜き出して返す。
* - pSecretText: 謎のテキスト
* 結果のテキストは、呼び出し側がfree()で解放すること。
* 結果のテキストを抜き出せない場合は、0(NULL)を返す。
* pSecretTextが0(NULL)の場合の動作は未定義。
*/
char *ProcessSecretText(const char *pSecretText) {
assert(pSecretText);
char *pText = 0;
int textLength = 0;
/* 謎のテキストを1文字ずつ処理 */
for (const char *pLetter=pSecretText; *pLetter!='\0'; pLetter++) {
/* 数字は読み飛ばす */
if (isdigit(*pLetter)) {
continue;
}
/* 結果のテキストに1文字追加 */
{
char *p = realloc(pText, textLength + 2);
if (p) {
pText = p;
} else {
/* メモリーを確保できなかった! */
free(pText);
pText = 0;
break;
}
}
pText[textLength] = *pLetter;
pText[textLength + 1] = '\0';
textLength += 1;
}
return pText;
}
int main(void) {
char *pText = ProcessSecretText("47H5e3ll2o9!18");
if (pText) {
puts(pText);
free(pText);
pText = 0;
} else {
/* 結果のテキストを抜き出せなかった! */
puts("Error");
}
return EXIT_SUCCESS;
}
Hello!
実行時エラーのチェックを、あとからプログラムに追加するのは手間のかかる作業です。どうすれば「直った」といえるのか、よく考えなければならないためです。例えばメモリー不足のような、あまり発生しない状況をどうにかして作り出さなければなりません。
しかし、直すのが大変だからといって放置しておくと、いつか本当にエラーが発生してトラブルになってしまうかもしれません。リスクを避ける最善の方法は、常にエラーチェックを怠らないことだといえるでしょう。
エラーチェックは「今やる」が基本です。「ここはチェックが必要だぞ」と気付いたら、すぐにif
文を書きましょう。これは、いつか起こるトラブルを減らすためにも、大切な心がけです。