プログラミング中に、作ったはずのプログラムがウンともスンとも言わなくなったり、謎のエラーメッセージが出てきて頭を抱えちゃったり…なんて経験、ありますよね?
特にC言語を学び始めたばかりだと、エラーとの付き合い方が分からなくて、心が折れそうになるかもしれません。
この記事では、C言語で発生するエラーと上手に付き合っていくための、基本的な考え方から実践的なテクニックまで、分かりやすさを最優先にして解説していきます。
この記事を読むと、こんなことができるようになります!
- エラー処理がなぜ必要なのかが分かる
- C言語でよく見るエラーの種類を知る
- エラーの原因を特定する道具(`errno`)の使い方が分かる
- エラーメッセージを表示する関数(`perror`, `strerror`)を使いこなせる
- ファイル操作やメモリ確保での実践的なエラー処理コードを書ける
- エラー処理で気を付けるべきポイントを理解できる
C言語のエラー処理とは?なぜ重要なのか?
まず、エラー処理って一体何でしょう?
簡単に言うと、プログラムが実行中に何か予期しない問題にぶつかったとき、プログラムが完全に止まってしまわないように、または問題が起きたことをちゃんと知らせるように、あらかじめ備えておくことです。
もし、エラー処理を全くしていなかったらどうなるでしょう?
例えば、ファイルを開こうとしたけど、そのファイルが存在しなかった場合。エラー処理がないと、プログラムは何も言わずに異常終了してしまうかもしれません。ユーザーから見たら、何が起きたのかさっぱり分かりませんよね。
また、計算に必要なメモリを確保しようとしたけど、空きメモリが足りなかった場合。無理やり処理を続けようとして、もっと深刻な問題を引き起こす可能性だってあります。
ちゃんとしたエラー処理をしておくことで、
- プログラムが突然止まるのを防ぎ、安定して動作させられる
- 問題が発生したときに、ユーザーに何が起きたか分かりやすく伝えられる
- 開発者自身も、どこで問題が起きているのか突き止めやすくなる(デバッグが楽になる!)
といったメリットがあります。面倒に感じるかもしれませんが、しっかりしたプログラムを作るためには、エラー処理は避けて通れない道なんです。
むしろ、積極的に取り組むべき、プログラムを育てる上での愛情表現みたいなもの、と考えてみませんか?
C言語におけるエラーの種類と基本的な考え方
C言語のプログラムを書いていると、色々な種類のエラーに遭遇します。全部を覚える必要はありませんが、よくあるパターンを知っておくと対処しやすくなりますよ。
例えば、
- ファイルを開こうとしたけど、ファイルが存在しない(ファイルI/Oエラー)
- プログラム実行に必要なメモリが足りない(メモリ確保エラー)
- ユーザーに入力してもらった値が、想定外のものだった(不正な入力)
- 計算結果が、扱える範囲を超えてしまった(オーバーフロー/アンダーフロー)
などがあります。
では、C言語では、こういったエラーが起きたことをどうやって知るのでしょうか?
基本的な考え方は、「関数の戻り値(返り値)を確認する」です。
C言語の標準ライブラリに含まれる関数の多くは、処理が成功したか失敗したかを示す特別な値を返すように作られています。
例えば、
- ファイルを開く`fopen`関数は、失敗すると`NULL`という特別なポインタを返します。
- メモリを確保する`malloc`関数も、失敗すると`NULL`を返します。
- 文字を入力する`getchar`関数は、ファイルの終端やエラー時に`EOF`という特別な値を返します。
このように、関数を使った後は、その戻り値が「失敗」を示していないかチェックする。これがC言語におけるエラーハンドリングの第一歩です。
戻り値を見ないのは、テストの結果を見ずに「たぶん大丈夫だろう」と言っているようなもの。しっかり確認するクセをつけましょうね!
C言語のエラー処理で必須!`errno` 変数とは
関数の戻り値で「失敗した!」ということは分かっても、じゃあ「なぜ失敗したのか?」その詳しい理由を知りたい時がありますよね。
例えば、ファイルが開けなかった(`fopen`が`NULL`を返した)としても、
- ファイルが存在しなかったから?
- ファイルは存在するけど、読み込む権限がなかったから?
- そもそも指定したパスの形式がおかしかったから?
など、色々な原因が考えられます。
そこで登場するのが、グローバル変数 `errno` (エラーナンバー) です。
`errno` は、多くの標準ライブラリ関数が、処理に失敗したときに、そのエラーの原因を示す数値(エラーコード)を格納するための特別な変数です。
`errno` を使うには、まず `#include
#include <errno.h>
注意点として、`errno` の値は、エラーが発生した直後に確認する必要があるということです。エラーが発生していない関数呼び出しや、関係ない処理を間にはさんでしまうと、`errno` の値が変わってしまったり、意味のない値が入っていたりすることがあります。
イメージとしては、関数が失敗した時に、こっそり `errno` という名のメモ帳にエラー理由の番号を書いてくれる感じ。ただし、そのメモはすぐに別の内容で上書きされちゃうかもしれないので、失敗を確認したら、すぐにメモ(`errno`)をチェックする、という流れが肝心です。
`errno` に入っている数値だけを見ても、それがどんなエラーかは分かりにくいですよね。その数値を分かりやすいメッセージに変換する方法は、次で説明します!
エラーメッセージを表示する関数の使い方 (エラー処理 方法)
`errno` に格納されたエラーコード(数値)を、人間が読んで理解できるエラーメッセージに変換してくれる便利な関数が用意されています。
ここでは、その代表的な2つの関数、`perror` と `strerror` の使い方を見ていきましょう。
`perror` 関数の使い方と具体例
`perror` (ピーエラー) 関数は、指定した文字列に続けて、現在の `errno` に対応する標準的なエラーメッセージを、標準エラー出力(通常はコンソール画面)に出力してくれる関数です。
使い方はとてもシンプル。
#include <stdio.h> // perror を使うのに必要 #include <errno.h> // errno を使うのに必要 // perror関数の呼び出し方 perror("ここに自分で好きなメッセージを入れる");
引数には、エラーが発生した状況を示すような、自分で考えたメッセージ(文字列)を指定します。`perror` は、その文字列の後ろにコロン(:)とスペースを付け、さらに `errno` に対応するシステムのエラーメッセージを付け加えて出力します。
例えば、存在しないファイルを開こうとして失敗した場合のコードを見てみましょう。
#include <stdio.h> #include <errno.h> int main(void) { FILE *fp; // 存在しないであろうファイルを開こうとしてみる fp = fopen("存在しないファイル.txt", "r"); if (fp == NULL) { // ファイルオープン失敗! // errnoにエラーの原因を示す番号がセットされているはず perror("ファイルを開けませんでした"); // perrorでエラーメッセージを表示 return 1; // エラーがあったことを示して終了 } // (もし開けたらここに来る) printf("ファイルを開けました!\n"); fclose(fp); return 0; }
もし「存在しないファイル.txt」が本当に存在しなければ、実行すると、コンソールにはこんな感じのメッセージが表示されるはずです(メッセージの細かい表現はOSによって少し異なります)。
ファイルを開けませんでした: No such file or directory
自分で書いた「ファイルを開けませんでした」に続いて、システムが教えてくれたエラー理由「No such file or directory」(そんなファイルやディレクトリはありません)が表示されていますね。`perror` を使うだけで、エラーの原因がぐっと分かりやすくなるのが実感できると思います。
`strerror` 関数の使い方と具体例
もう一つの関数 `strerror` (エスティーアールエラー) は、引数に与えたエラーコード(通常は `errno` の値)に対応するエラーメッセージの文字列を返してくれる関数です。
`perror` と違って、`strerror` はメッセージを直接出力するのではなく、文字列データそのものを返します。そのため、`printf` などと組み合わせて、より自由な形式でエラーメッセージを表示したい場合に便利です。
使うには `#include
#include <string.h> // strerror を使うのに必要 #include <errno.h> // errno を使うのに必要 #include <stdio.h> // printf などを使うのに必要 // strerror関数の使い方(例: printfと組み合わせる) printf("エラー発生! 原因: %s (エラーコード: %d)\n", strerror(errno), errno);
`strerror` に現在の `errno` の値を渡すと、対応するエラーメッセージ文字列へのポインタ(文字列がメモリ上のどこにあるかを示す住所のようなもの)が返ってきます。
`printf` の `%s` を使って、その文字列を表示させることができます。ついでに `%d` で `errno` の数値そのものを表示するのも良いでしょう。
先ほどのファイルオープン失敗の例を `strerror` で書き換えてみましょう。
#include <stdio.h> #include <errno.h> #include <string.h> // strerror を使うために追加 int main(void) { FILE *fp; int error_code; // errnoの値を一時的に保存する変数 fp = fopen("存在しないファイル.txt", "r"); if (fp == NULL) { error_code = errno; // ★エラー発生直後にerrnoの値を保存! fprintf(stderr, "エラー: ファイルを開けませんでした。\n"); // stderrに出力するのがお作法 fprintf(stderr, "原因: %s (コード: %d)\n", strerror(error_code), error_code); return 1; } printf("ファイルを開けました!\n"); fclose(fp); return 0; }
実行結果は、`perror` の時と似たような感じになりますが、`printf` (ここではエラーメッセージなので `fprintf(stderr, ...)` を使っています) で書式を自分で決められるのがポイントです。
エラー: ファイルを開けませんでした。 原因: No such file or directory (コード: 2)
一つ注意点!上のコード例で `error_code = errno;` としている部分がありますね。`fprintf` のような別の関数を呼び出す前に、`errno` の値を別の変数にコピーしています。
これは、`fprintf` 自体が(まれにですが)`errno` の値を変更してしまう可能性があるため、エラー発生直後の `errno` の値を確実に保持しておくためのテクニックです。`strerror` を使う場合は、このように一時変数に `errno` をコピーしておくと、より安全ですね。
`perror` は手軽さが魅力、`strerror` は柔軟性が魅力。状況に応じて使い分けられるようになると、エラー処理の幅が広がりますよ!
サンプルコードで学ぶC言語エラー処理の方法
基本的な道具(戻り値チェック、`errno`、`perror`/`strerror`)の使い方が分かったところで、もう少し実際のプログラムに近い場面でのエラー処理を見ていきましょう。
ファイル操作とメモリ確保は、エラーが発生しやすい代表的な処理です。
ファイル処理におけるエラー処理の方法
ファイルを開いて何かを読み書きする、というのはプログラムでよくやる操作ですが、ここにはエラーの落とし穴がいっぱいあります。
- ファイルが存在しない
- ファイルへのアクセス権限がない
- ディスクの空き容量がない(書き込み時)
- そもそもパス名がおかしい
などなど。これらに備える基本的なエラー処理は、やはり `fopen` の戻り値チェックです。
#include <stdio.h> #include <stdlib.h> // exit関数を使うために必要 #include <errno.h> int main(void) { FILE *fp = NULL; // 初期化しておくのがお作法 char *filename = "my_data.txt"; // 存在するかどうか分からないファイル名 // "r"モード(読み込みモード)でファイルを開いてみる fp = fopen(filename, "r"); // ★fopenの戻り値をチェック! if (fp == NULL) { // 開けなかった場合 fprintf(stderr, "エラー: ファイル '%s' を開けませんでした。\n", filename); perror("原因"); // errnoに基づいたエラー理由を表示 exit(EXIT_FAILURE); // プログラムを異常終了させる (stdlib.hが必要) // EXIT_FAILUREは通常1などの0以外の値 } // --- ファイルが開けた場合の処理 --- printf("ファイル '%s' を正常に開けました。\n", filename); // ここでファイルの内容を読み込んだりする処理を書く // ... (処理は省略) ... // --- ファイルを閉じる --- if (fclose(fp) != 0) { // fcloseも実は失敗することがある(ディスクフルなど) perror("ファイルクローズエラー"); // クローズ失敗時の処理は状況によるが、ここではメッセージ表示のみ } printf("処理が完了しました。\n"); return EXIT_SUCCESS; // 正常終了 (通常は0) }
このコードのポイントは、
- `fopen` の後、すぐに `if (fp == NULL)` で戻り値をチェックしていること。
- エラーだったら、`fprintf(stderr, ...)` で状況を説明し、`perror` でシステムからのエラー理由を表示していること。
- エラー発生後、処理を続けられないので `exit(EXIT_FAILURE)` でプログラムを終了させていること(`EXIT_FAILURE` は `stdlib.h` で定義されている、失敗を示す定数です)。
- ファイル処理が終わったら `fclose` でファイルを閉じること。(そして、`fclose` の戻り値も念のためチェックしていること)。
ファイル操作の前には必ず `fopen` の戻り値を確認し、失敗したら `perror` や `strerror` で原因を調べて適切に対処する。この流れをしっかり身につけましょう。
メモリ確保 (`malloc`) におけるエラー処理の方法
プログラムの実行中に、データなどを置いておくためのメモリ領域が必要になることがあります。そんな時に使うのが `malloc` (エムアロック、メモリアロケーションの略) 関数です。
`malloc` は、指定したサイズのメモリ領域を確保して、その先頭アドレス(メモリ上の住所)を返してくれます。しかし、システムのメモリが不足している場合など、メモリを確保できないことがあります。その場合、`malloc` は `NULL` を返します。
もし、`malloc` が `NULL` を返したのに、それに気づかず、確保できなかったメモリ領域を使おうとするとどうなるでしょう?多くの場合、プログラムは「セグメンテーション違反(Segmentation fault)」といったエラーで強制終了してしまいます。これは非常にありがちなバグの原因です。
`malloc` を使った後も、必ず戻り値が `NULL` でないかチェックする必要があります。
#include <stdio.h> #include <stdlib.h> // malloc, exit を使うために必要 #include <errno.h> // errno, strerror を使うために必要(必須ではないが念のため) #include <string.h> // strerror を使うために必要 int main(void) { int *numbers = NULL; // 整数型へのポインタを初期化 size_t count = 1000000000; // わざと巨大な数を指定してみる size_t i; printf("%zu 個の整数を格納するためのメモリを確保しようとします...\n", count); // 指定した個数 * int型のサイズ 分のメモリを確保 numbers = (int *)malloc(count * sizeof(int)); // ★mallocの戻り値をチェック! if (numbers == NULL) { // メモリ確保失敗! fprintf(stderr, "エラー: メモリを確保できませんでした。\n"); // malloc失敗時のerrnoは必ずしも設定されるとは限らないが、 // ENOMEM (メモリ不足) が設定されることが多い if (errno == ENOMEM) { fprintf(stderr, "原因: %s (システムメモリが不足しています)\n", strerror(errno)); } else { // 他の理由(非常に稀)かもしれない fprintf(stderr, "原因: 不明なエラー (errno: %d, %s)\n", errno, strerror(errno)); } exit(EXIT_FAILURE); // プログラム終了 } // --- メモリが確保できた場合の処理 --- printf("メモリ確保成功! アドレス: %p\n", (void*)numbers); // 確保したメモリを使う処理 (例: 0で初期化) printf("確保したメモリを初期化します...\n"); // (巨大なサイズなので、このループ自体が時間がかかる or 危険な場合もある) // for (i = 0; i < count; i++) { // numbers[i] = 0; // } printf("初期化完了(のつもり)。\n"); // --- 使い終わったメモリを解放 --- free(numbers); // mallocで確保したメモリは必ずfreeで解放する numbers = NULL; // 解放後はNULLを入れておくと安全 (二重解放防止) printf("メモリを解放し、処理を完了しました。\n"); return EXIT_SUCCESS; }
(注意:上のコードの `count` は非常に大きな値にしているので、実際に動かすとメモリ確保に失敗する可能性が高いです。また、確保できたとしてもその後のループ処理に時間がかかったり、環境によっては別の問題が起きるかもしれません。あくまで `malloc` のエラーチェックの例として見てください。)
ここでのポイントは、`malloc` の呼び出し直後に `if (numbers == NULL)` で成否を確認していることです。失敗していたらエラーメッセージを表示して `exit` しています。ファイル処理と同様、メモリ確保も失敗する可能性があることを常に念頭に置き、必ずチェックする習慣をつけましょう。
また、`malloc` で確保したメモリは、使い終わったら必ず `free` 関数で解放するのを忘れないでくださいね!解放し忘れると「メモリリーク」という問題につながります。(これはエラー処理とは少し違いますが、セットで覚えておきましょう。)
C言語のエラー処理における注意点
エラー処理の方法をいくつか見てきましたが、ここで、初心者の人が特に気をつけたい点、間違いやすい点をまとめておきましょう。
`errno` を確認するタイミング
何度も言いますが、`errno` はとても移り気です!
`errno` の値が意味を持つのは、エラーが発生した可能性のある関数(例:`fopen`, `malloc` など)を呼び出した「直後」だけ、と肝に銘じてください。
例えば、下のようなコードはダメな例です。
// ダメな例! fp = fopen("myfile.txt", "r"); printf("ファイルを開こうとしました。\n"); // ← 関係ない処理が間に入っている! if (fp == NULL) { perror("fopenエラー"); // この時点のerrnoはfopenがセットしたものとは限らない! }
`fopen` が失敗して `errno` にエラーコードがセットされたとしても、その後の `printf` 関数呼び出しが(内部で何かシステムコールを呼んで)`errno` の値を上書きしてしまうかもしれません。
正しくは、
// 良い例 fp = fopen("myfile.txt", "r"); if (fp == NULL) { // エラー発生直後にerrnoを使う! error_code = errno; // 必要なら変数に保存 perror("fopenエラー"); // または fprintf(stderr, "エラー原因: %s\n", strerror(error_code)); } else { // 成功した場合の処理 }
このように、関数の戻り値をチェックして「失敗した」と分かったら、他の処理を挟まずに、すぐに `errno` を確認(または変数に保存)する。この鉄則を守りましょう。
関数の戻り値を必ず確認する
これも口を酸っぱくして言いますが、C言語の関数の戻り値は、基本的に「必ず」確認する習慣をつけましょう!
特に、
- ファイル操作系 (`fopen`, `fclose`, `fread`, `fwrite`, `fprintf`, `fscanf` など)
- メモリ操作系 (`malloc`, `calloc`, `realloc`)
- 入力系 (`scanf`, `getchar`)
- システムに関わるような関数全般
これらの関数は、処理が失敗する可能性が常にあります。戻り値を見ずに「きっと成功しただろう」と楽観的にプログラムを書き進めるのは、時限爆弾を抱えているようなものです。いつか予期せぬところで問題が起きて、原因究明に苦労することになります。
「毎回チェックするのは面倒…」と感じるかもしれません。でも、その一手間が、後々のデバッグ地獄から救ってくれるのです。戻り値のチェックは、未来の自分への投資だと思って、しっかり実装しましょう!
関数の説明書(マニュアルやリファレンス)には、通常、どのような場合にどのような値を返すかが書かれています。関数を使うときは、その説明を読んで、エラー時にどのような値が返ってくるのかを確認する癖をつけると、より的確なエラー処理が書けるようになりますよ。
【まとめ】エラーに強いC言語プログラムを目指して
C言語のエラー処理について、基本的な考え方から具体的な関数の使い方、実践例、そして注意点まで見てきました。
もう一度、ポイントを振り返ってみましょう。
- エラー処理は、プログラムを安定させ、問題を分かりやすくするために欠かせないもの。
- C言語のエラー処理の基本は「関数の戻り値」をチェックすること。
- エラーの詳しい原因を知るにはグローバル変数 `errno` が役立つ。
- `errno` の数値を分かりやすいメッセージにするには`perror` や `strerror` 関数を使う。
- `fopen` や `malloc` など、失敗する可能性のある関数の後は必ずチェックを入れる!
- `errno` はエラー発生直後に確認する!
最初は少し難しく感じるかもしれませんが、今回学んだことを意識して、一つ一つの関数呼び出しに対して「これは失敗する可能性があるかな?」「失敗したらどう対処しよう?」と考える習慣をつけることが、上達への近道です。
エラーは、プログラムが「ここ、ちょっと困ってるんだけど!」と教えてくれるサインです。そのサインを無視せず、きちんと耳を傾けて対処してあげることで、あなたの書くC言語プログラムは、もっともっと安定して、信頼できる、かっこいいプログラムへと成長していくはずです!
エラー処理は、決して難しいだけの厄介者ではありません。むしろ、プログラムの品質を高めるための基本的なスキルであり、自信を持ってプログラムを書くための土台となります。
【関連記事】
0 件のコメント:
コメントを投稿
注: コメントを投稿できるのは、このブログのメンバーだけです。