閉じる

【C言語】errnoとエラー処理の基本|エラー原因の特定とメッセージ取得

C言語でシステムコールや標準ライブラリ関数を使うとき、成功・失敗の判定だけでなく、なぜ失敗したのかという原因を知ることが重要です。

そのときに活躍するのがerrnoです。

本記事では、errnoの基本的な考え方から、正しい使い方、エラーメッセージの取得方法、注意点まで、実用的な観点から詳しく解説します。

errnoとは何か

errnoの役割と位置づけ

errnoは、ライブラリ関数やシステムコールのエラー原因を表す整数の「変数」です

多くの標準ライブラリ関数は、エラー発生時にこのerrnoにエラー内容を示す番号を設定します。

具体的には次のような特徴があります。

  • C標準ではerrnoint型の変数として定義されています。
  • C言語のヘッダ<errno.h>で宣言されます。
  • 関数が失敗したあとにerrnoを見ることで、失敗の具体的な理由を判断できます

errnoが更新されるタイミング

多くの関数はエラー発生時にerrnoを書き換えますが、次の点に注意が必要です。

  • 成功した関数はerrnoを0に戻すとは限りません
  • 関数呼び出しに失敗した後、次の別の関数呼び出しを行うと、errnoが上書きされる可能性があります。
  • そのためエラー原因を知りたいときは、エラーを返した直後のタイミングでerrnoを参照することが重要です。

このようにerrnoは、戻り値だけでは分からない「なぜ失敗したか」を教えてくれる補助情報として機能します。

errnoを使うためのヘッダファイル

errnoを使うには、プログラムの先頭で次のヘッダをインクルードします。

C言語
#include <errno.h>   /* errno本体とエラー番号の定義 */
#include <string.h>  /* strerror関数(エラーメッセージ取得) */
#include <stdio.h>   /* printfなど */

<errno.h>には、errno変数の宣言と、エラー番号のマクロ定義(例: ENOENT, EACCESなど)が含まれます

errnoの基本的な使い方

典型的な利用パターン

errnoを用いたエラー処理の典型的な流れは次のようになります。

  1. errnoを0に初期化する(必要な場合)。
  2. 標準ライブラリ関数・システムコールを呼び出す。
  3. 戻り値で成功・失敗を判定する。
  4. 失敗していたらerrnoを確認し、エラー番号に応じて処理分岐やメッセージ出力を行う。

fopenとerrnoの基本例

ファイルを開くfopenとerrnoを組み合わせた、最も基本的なサンプルを示します。

C言語
#include <stdio.h>
#include <errno.h>   /* errnoとエラー番号 */
#include <string.h>  /* strerror関数 */

int main(void) {
    /* 存在しないファイル名をあえて指定する */
    const char *filename = "not_exists.txt";

    /* 念のためerrnoを0に初期化しておく */
    errno = 0;

    FILE *fp = fopen(filename, "r");
    if (fp == NULL) {
        /* fopenが失敗したのでerrnoを確認する */
        printf("ファイルを開けませんでした: %s\n", filename);

        /* errnoの数値を表示 */
        printf("errno = %d\n", errno);

        /* strerrorで人間向けメッセージに変換 */
        printf("エラーメッセージ: %s\n", strerror(errno));

        return 1;
    }

    /* ここに通常の処理を書く(今回はすぐ閉じるだけ) */
    fclose(fp);

    return 0;
}
実行結果
ファイルを開けませんでした: not_exists.txt
errno = 2
エラーメッセージ: No such file or directory

この例では、ファイルが存在しないためfopenが失敗し、errnoにENOENT(2)が設定されます。

errnoは数値そのものではなく、ENOENTのようなマクロ名で扱うのが基本です。

errnoのエラー番号マクロ

errnoに格納される値は、多くの場合、定義済みマクロ(定数)で表現されます

たとえばファイル関連でよく登場するものには次があります。

マクロ名代表的な意味
ENOENTファイルやディレクトリが存在しない
EACCESアクセス権限がない
EEXISTすでに同名のファイルが存在する
ENOMEMメモリ不足
EINVAL無効な引数(値)が渡された

C標準で必須なのはごく一部ですが、UNIX系システムではさらに多くのエラー番号が定義されています。

strerrorとperrorでメッセージを取得する

strerrorを使って文字列に変換する

errnoは単なる数値なので、そのままでは意味がわかりにくいです。

そこでerrnoを人間向けのエラーメッセージ文字列に変換する関数がstrerrorです。

C言語
#include <string.h>

char *strerror(int errnum);
  • 引数errnumにerrnoの値を渡します。
  • 戻り値としてエラー内容を示す文字列へのポインタを返します。
  • この文字列は静的領域にあり、プログラム側でfreeなどしてはいけません。

もう少し実践的な例を見てみます。

C言語
#include <stdio.h>
#include <errno.h>
#include <string.h>

void print_error_message(const char *func_name) {
    /* errnoの値に対応するエラーメッセージを取得 */
    const char *msg = strerror(errno);

    /* 関数名とあわせて表示する */
    printf("%sでエラーが発生しました: %s (errno=%d)\n",
           func_name, msg, errno);
}

int main(void) {
    errno = 0;
    FILE *fp = fopen("no_such_file.txt", "r");
    if (fp == NULL) {
        print_error_message("fopen");
        return 1;
    }

    fclose(fp);
    return 0;
}
実行結果
fopenでエラーが発生しました: No such file or directory (errno=2)

perrorを使って簡単に出力する

もっと簡単に、標準エラー出力にメッセージを表示したい場合はperrorを使います

C言語
#include <stdio.h>

void perror(const char *s);
  • 引数sに、エラーの前に付けたい文字列(プレフィックス)を渡します。
  • 出力形式は"s: メッセージ\n"のようになります。
  • errnoの値を内部で参照し、対応するメッセージを出力します。
C言語
#include <stdio.h>
#include <errno.h>

int main(void) {
    FILE *fp = fopen("no_such_file.txt", "r");
    if (fp == NULL) {
        /* "fopen"というプレフィックスを付けてエラーを表示 */
        perror("fopen");
        return 1;
    }

    fclose(fp);
    return 0;
}
実行結果
fopen: No such file or directory

strerrorはメッセージを「取得する」関数、perrorはメッセージを「表示する」関数と覚えると整理しやすいです。

errnoを使うときの注意点

errnoをチェックする前に戻り値を確認する

最も重要な注意点は、「戻り値がエラーであることを確認してからerrnoを見る」ことです

多くのライブラリ関数は、成功時にはerrnoを変更しないことがあります。

そのため、「処理は成功したのに、過去のエラー値がerrnoに残っている」という状況がありえます。

C言語
#include <stdio.h>
#include <errno.h>

int main(void) {
    errno = 0;

    /* 成功するかもしれない処理 */
    FILE *fp = fopen("existing.txt", "r");

    /* いきなりerrnoだけを見るのは危険 */
    printf("errno = %d\n", errno);

    if (fp == NULL) {
        perror("fopen");
        return 1;
    }

    fclose(fp);
    return 0;
}

このような書き方だと、fopenが成功しているのに、過去に別の処理で発生したエラー値が残っていて、誤解を生む可能性があります。

必ず戻り値で成功・失敗を判定してからerrnoを用いるようにします。

errnoはスレッドローカル(実装依存)

POSIX環境や多くの実装では、errnoはスレッドごとに独立した値(スレッドローカル)として扱われます

そのため、別スレッドのエラーが自分のスレッドのerrnoに影響を与えることはありません。

ただし、以下の点に注意してください。

  • マルチスレッド環境ではstrerrorの代わりにstrerror_rなどのスレッドセーフな関数を使うことが推奨されるケースがあります。
  • C標準だけではスレッドとの関係は定義されておらず、POSIXなどの仕様に依存します。

この記事の範囲では、「マルチスレッドでも基本的にはerrnoはスレッドごとに独立している」と理解しておけば十分です。

errnoを勝手に上書きしない

ふつう、アプリケーション側がerrnoに任意の値を入れる必要はほとんどありません

errnoはライブラリ関数やシステムコールが設定するためのものだからです。

ただし、次の目的で一時的に0を代入することはあります。

  • 「この関数呼び出しで本当にエラーが起きたか」をはっきりさせたい場合に、事前にerrno = 0;としておき、その関数の後でerrno != 0かどうかを確認する。

それ以外の用途でerrnoに任意の整数を代入するのは避けるべきです。

errnoは「結果」としてセットされるべきもので、「指定」する対象ではないと考えるとよいです。

errnoを使った実用的なエラー処理例

ファイル読み込み関数でerrnoを返すパターン

現実的なコードでは、「一つの関数の中でファイルを開く→読む→閉じる」という処理をまとめ、失敗した場合にerrnoの値を呼び出し元に返す、といったパターンがよく使われます。

C言語
#include <stdio.h>
#include <errno.h>
#include <string.h>

int read_file(const char *filename) {
    FILE *fp = fopen(filename, "r");
    if (fp == NULL) {
        /* fopenが設定したerrnoをそのまま返す */
        return errno;
    }

    /* ここでファイル読み込み処理を行う(サンプルのため省略) */

    if (fclose(fp) != 0) {
        /* fcloseもerrnoを設定する場合がある */
        return errno;
    }

    /* 正常終了 */
    return 0;
}

int main(void) {
    const char *filename = "sample.txt";
    int err = read_file(filename);

    if (err != 0) {
        /* ここでerrnoではなく、関数から返ってきたerrを使うのがポイント */
        printf("ファイル処理中にエラーが発生しました: %s\n", filename);
        printf("エラーコード: %d\n", err);
        printf("内容: %s\n", strerror(err));
        return 1;
    }

    printf("ファイル処理は正常に完了しました。\n");
    return 0;
}
実行結果
ファイル処理中にエラーが発生しました: sample.txt
エラーコード: 2
内容: No such file or directory

このように、内部で発生したerrnoを「intの戻り値」として外側に伝えると、ライブラリ的な関数が作りやすくなります。

errnoに応じて処理を分岐する

errnoの値に応じて、処理の分岐を実装することもよくあります。

C言語
#include <stdio.h>
#include <errno.h>
#include <string.h>

int main(void) {
    const char *filename = "test.txt";

    FILE *fp = fopen(filename, "r");
    if (fp == NULL) {
        /* errnoに応じて分岐 */
        switch (errno) {
        case ENOENT:
            printf("ファイルが存在しません: %s\n", filename);
            break;
        case EACCES:
            printf("ファイルへのアクセス権がありません: %s\n", filename);
            break;
        default:
            printf("ファイルを開けませんでした(%d): %s\n",
                   errno, strerror(errno));
            break;
        }
        return 1;
    }

    printf("ファイルを開くことに成功しました。\n");
    fclose(fp);
    return 0;
}
実行結果
ファイルが存在しません: test.txt

このような分岐を行うと、エラー内容に応じてユーザーへの案内やリトライ戦略を変えることができます

まとめ

errnoは、C言語でのエラー処理において「なぜ失敗したか」を知るための中心的な仕組みです。

関数の戻り値で成功・失敗を判定し、失敗時にはerrnoを参照してstrerrorperrorでメッセージ化する、という流れを押さえておけば、多くのエラー状況に対応できます。

注意点としては、戻り値を確認せずにerrnoだけを見ることを避けること、エラーが発生した直後にerrnoを使うこと、そして必要に応じてerrnoの値を呼び出し元に伝える設計を行うことが挙げられます。

これらを意識することで、原因が追いやすく、ユーザーにとっても分かりやすいエラー処理をC言語で実現できます。

診断・エラーハンドリング

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!