C言語のerrnoの使い方: 初期化、文字列化、参照、ハンドリング方法の具体例

298, 2021-08-02

目次

C言語のerrnoの使い方

C言語で困っちゃうのがエラーハンドリングです。
C言語のエラーハンドリング方法って初学者の内に教えてくれるところをあまり見かけませんよね?
C言語の標準ライブラリや外部ライブラリなどは一般的にerrnoを使ったエラーハンドリングをしています。
この記事ではerrnoの使い方をまとめています。

具体的には↓を見ていきます。

  • errnoの概要

  • errno.hのインクルード方法

  • errnoの初期化方法

  • errnoの文字列化方法

  • errnoを自作関数で使ってみる

  • errnoを使ったエラーハンドリング方法

errnoの概要

errnoは何なのかというと、実体はただの整数型のグローバル変数です。
この変数にエラー定数を保存してエラーハンドリングを行います。
標準ライブラリは全般的にこのエラーハンドリングを採用していて、何か標準ライブラリでエラーが起きたときはerrnoを参照すればエラー内容がわかるという寸法です。

たとえばprintf()などもエラーが起こることがあります。
その場合、printf()は内部でerrnoにエラー定数をセットします。
printf()を使っている側はこのerrnoの値を参照してエラーハンドリングを行います。

これがerrnoの概要ですが、初学者のチュートリアルではあまり解説されていません。
標準ライブラリのエラーハンドリングは質のいいアプリを作るのに必要になってくるので、押さえておくと損が無い感じです。

errnoをハンドリングしよう

errno.hのインクルード方法

グローバル変数のerrnoを使うにはerrno.hをインクルードしておく必要があります。

#include <errno.h>

errnoの初期化方法

errnoはただの整数の変数です。
そのため↓のように0クリアしておくと初期化することができます。

    errno = 0;  // errnoの初期化

errnoはグローバル変数なので、標準ライブラリを呼び出すごとにその値が変わる可能性があります。
そのためerrnoの初期化は非常に重要になってきます。
初期化しておかないと前の関数呼び出しのエラー結果がerrnoに残っていた場合に、ちゃんとエラーハンドリングが出来なくなってしまいます。

errnoを使う場合は「errnoは初期化するもの」という認識を持っておくといいでしょう。

errnoの文字列化方法

errnoに保存されたエラー定数はじっさいにはただの整数ですので、この整数を見ても何が何だかわかりません。
そのためerrnoのエラー定数を文字列に変換する関数が用意されています。
strerror()を使うとerrnoを文字列のメッセージに変換できます。

/**
 * C言語のエラーハンドリング
 * errnoを使ったハンドリング方法
 */
#include <stdio.h>
#include <string.h>
#include <errno.h>

int main(void) {
    // errnoの初期化とメッセージの取得方法
    errno = 0;  // errnoの初期化
    printf("%s.\n", strerror(errno));
    // No error

    return 0;
}

↑の場合、errno0で初期化した後にstrerror()に渡すと、「No error」という文字列が返ってきます。
これはerrno0という値がエラーの状態を持っていないことを表しています。
つまりerrno0で初期化するのは「errnoをエラーの状態を持っていない状態にする」という意味があります。

errnoを自作関数で使ってみる

では実際にerrnoにエラー定数を代入してみましょう。

check()という単純な関数を作ります。

/**
 * C言語のエラーハンドリング
 * errnoを使ったハンドリング方法
 */
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <errno.h>

/**
 * argが0より低ければerrnoにEINVALをセットする
 */
void check(int arg) {
    if (arg < 0) {
        errno = EINVAL;  // Invalid argument
    }
}

int main(void) {
    errno = 0;  // errnoの初期化
    check(1);
    // エラーは発生しない
    if (errno != 0) {
        printf("FAIL.\n");
    }

    errno = 0;  // errnoの初期化
    check(-1);
    if (errno != 0) {
        // エラーが発生!
        int save_errno = errno;  // errnoを保存する

        // strerror()でerrnoを参照すると、エラー内容がわかる
        printf("FAIL. %s.\n", strerror(save_errno));
    }

    return 0;
}

↑のコードをコンパイルして実行すると↓のような結果になります。

FAIL. Invalid argument.

check()関数は引数arg0より下の値だった場合にerrnoEINVALを保存します。
EINVALは引数が不正だった場合のエラー定数です。
check()の呼び出し前にerrnoを初期化します。これはcheck()の呼び出し前のエラー状態をクリアするためです。
check()を呼び出した後にerrno0かどうかチェックします。errno0はエラーを持っていない状態なので、errno0以外であればerrnoはエラーを持っているということになります。

↓のコードを見てみましょう。

    errno = 0;  // errnoの初期化
    check(1);
    // エラーは発生しない
    if (errno != 0) {
        printf("FAIL.\n");
    }

このcheck()の呼び出しではcheck()に渡している引数は1です。
check()は引数が0より下の場合にerrnoにエラー定数を保存する関数なので、この場合はerrno0のままです。
よって、check()の呼び出し後にerrnoをチェックしてますが、errno0のままなので、printf()などは実行されません。

次に↓のコードです。

    errno = 0;  // errnoの初期化
    check(-1);
    if (errno != 0) {
        // エラーが発生!
        int save_errno = errno;  // errnoを保存する

        // strerror()でerrnoを参照すると、エラー内容がわかる
        printf("FAIL. %s.\n", strerror(save_errno));
    }

↑の場合、check()-1を渡しているのでcheck()は内部でerrnoEINVALを保存します。
if (errno != 0)という条件式が真になるので、if文の中が実行されます。
errnoを参照するわけですが、errnoの参照で気をつけたいのがerrnoの値の退避です。
↓のようにcheck()に近い所でerrnoを別の変数に保存しておきます。

        int save_errno = errno;  // errnoを保存する

これはなぜかというと、errnoはグローバル変数なので、標準ライブラリを呼び出すごとにその値がころころ変わる可能性があるのです。
そのためif文の中でprintf()などを使った場合、参照したいerrnoの値が変わってしまう可能性があります。
そこでsave_errnoerrnoの値を保存しておきます。こうすれば参照したいエラー定数を維持することが出来ます。
これはLinuxのマニュアルにも記載されている、いわば公式の推奨する方法です。

errnoを使ったエラーハンドリング方法

errnoの値を実際にハンドリングする場合も見てみます。

/**
 * C言語のエラーハンドリング
 * errnoを使ったハンドリング方法
 */
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <errno.h>

/**
 * argが0より低ければerrnoにEINVALをセットする
 */
bool check(int arg) {
    if (arg < 0) {
        errno = EINVAL;  // Invalid argument
    }
}

int main(void) {
    int val = -1;

    errno = 0;  // errnoの初期化
    check(val);
    if (errno != 0) {
        int save_errno = errno;  // errnoを保存する

        // check()が失敗したので↓でエラー処理をする
        // エラー内容を表示する
        printf("failed to check(). %s.\n", strerror(save_errno));

        // errnoの値によってエラー処理を分岐する
        switch (save_errno) {
        default:
            // サポートしていないerrno
            printf("unknown error.\n");
            break;
        case EINVAL:
            // ここで復帰処理を入れても良い
            printf("val (%d) is invalid argument.\n", val);
            break;
        }
    }

    return 0;
}

↑の場合strerror()errnoを参照している所は今までの解説にもありましたが、switch文でエラー参照をしている所を詳しく見てみたいと思います。

        // errnoの値によってエラー処理を分岐する
        switch (save_errno) {
        default:
            // サポートしていないerrno
            printf("unknown error.\n");
            break;
        case EINVAL:
            // ここで復帰処理を入れても良い
            printf("val (%d) is invalid argument.\n", val);
            break;
        }

↑のようにcheck()の呼び出し後にsave_errnoの値をチェックし、そのエラー定数ごとに分岐処理を書くことで、check()のエラーをハンドリングすることが可能です。
↑の場合はsave_errnoの値がEINVALだった場合は追加のエラーメッセージを出力しています。
check()のエラーをカバーしたい場合はここでcheck()の復帰処理などを入れてもいいと思います。

復帰処理は、たとえばファイルを開こうとしてエラーが発生したら、ファイル名を再構築して再びファイルを開くことを試行するなど、いろいろ処理は考えられます。
あるいはメモリが足りないというエラーが出たら、アプリで使っている余計なメモリを解放するとかも考えられます。

焼け石に水の可能性もある

エラー復帰はむずかしいね

おわりに

今回はC言語のerrnoについて使い方を見てみました。
標準ライブラリなどで使われているエラーハンドリングの機構ですが、あまり表には出てこない不思議なやつです。
この記事を読めば標準ライブラリのエラーハンドリングはばっちりですね。

エラー定数ぜんぶ覚えた(大嘘

おそろしい子・・・