【C言語】errnoを利用してエラーの原因を特定する

このページにはプロモーションが含まれています

このページではC言語における errno について解説します。

うーん、どうしても fopen 関数でエラーになる…

fopen("text.txt", "r");

C言語諦めてしまいそう…

なんでエラーになってるの?

それが分からないから困ってるんだよ…
オーケー、そのエラーの原因を調べるための方法を今回は解説していくよ!

そんな方法あるの?

プログラミングしていて避けて通れないのが「エラー」です。

作成したプログラムでエラーが発生して悪戦苦闘したことは、プログラマーの方であれば皆さん経験あるのではないでしょうか。

この「エラー」を解決できずにプログラミングを諦めたり挫折した人もいるでしょう…。

で、この「エラー」を解決するためにはそのエラーが発生した原因・理由を知るのが手っ取り早いです。

自身で作成した関数でエラーが発生しているのであれば、その関数の中に printf などを仕込んで、どこでエラーが発生しているかを調べれば原因を推測することが可能ですよね。

で、厄介なのは標準関数(ライブラリ関数)などのソースコードが変更できないような関数内でのエラーです。この場合はソースコードに printf を仕込んでエラーの原因を調べることができません(頑張ればできるのかも…)。

そこで活用していただきたいのが、今回紹介する errno の仕組みです。

実は、errno を利用すれば標準関数(システムコール)で発生した原因を調べることが可能です!

このページではこの errno について解説していきたいと思います!

MEMO

主に MacOS や Linux 向けの解説になります

Windows でも同様の仕組みはありますが、インクルードするファイルなどが異なる可能性があるので注意してください

errno とは

errno とは、システムコールやライブラリ関数(標準ライブラリの関数や socket ライブラリの関数などなど)で発生した「エラーの原因を示す値」が設定される変数(やマクロ)になります。

より具体的には直前に発生した「エラーの原因を示す値」が設定されています(また、errno はスレッド毎に設定されます)。

エラーの原因を示す値を「エラー番号」と呼びます。

エラー発生時にerrno を参照することで、その番号を取得することができ、この値からエラーの原因を知ることができます。

errno の使い方

この errno を利用するためには errno.hをインクルードしておく必要があります。

errno.hのインクルード
#include <errno.h>

この errno.h のインクルードさえしてしまえば、あとは変数宣言などの前準備を行う必要もなく、そのまま errno を変数のように使用することができます。

errno はライブラリ関数やシステムコールでエラー発生時に自動的に値が設定されます。

関数がerrnoを設定する様子

ですので、ユーザーからすると「エラー発生時に errno を参照する」のが基本的な使い方になります。

ユーザープログラムでerrnoを参照する様子

例えば fopen 関数でのエラーの原因を示す値を表示する場合は、下記のようにエラー直後に errno を出力します。

fopenでのエラー番号取得
FILE *f;
f = fopen("text.txt", "r");
if (f == NULL) { /* fopenが失敗だった */

    /* errnoでエラーの原因を表示 */
    printf("fopen error(%d)\n", errno);
    return -1;
}

上記により、fopen に失敗した時は printf 関数で errno に設定されたエラー番号を表示することができます。

例えば私の PC 環境で、"text.txt" が存在しない状態で上記を実行すると、下記のようにエラー番号が表示されました。

fopen error (2)

僕のプログラムでも表示してみたら 2 が表示されたよ!

でも…、2 ってなんだ…?

そうだね!このエラー番号の意味が分からないと、結局原因がわからないよね

次はこのエラー番号が何を示すかを知るための方法を解説していくよ!

errno に設定されているのはエラーの番号なので、この番号の意味が分からないとエラーの原因の特定は難しいです。

なので、次はこのエラー番号の意味を表示したり調べたりする方法について解説していきます。

スポンサーリンク

strerror を使って文字列で表示

手っ取り早いのが strerror 関数を使うことです。

strerror 関数は、引数で指定したエラー番号の意味を文字列で取得する関数になります。もっと正確に言うと、エラー番号の意味を表す文字列へのアドレスを取得する関数です。

strerror 関数の定義は下記のようになっており、関数の宣言は string.h で行われています。

strerror
#include <string.h>
char *strerror(int errnum);

引数で指定する errnum はエラー番号です。なので、errno 自体もしくは errno の値を格納した変数を指定して実行します。

例えば前述の fopen のエラー番号を表示する際は下記のようにソースコードを記述します。

fopenのエラー原因取得
FILE *f;
f = fopen("text.txt", "r");
if (f == NULL) { /* fopenが失敗だった */

    /* errnoでエラーの原因を表示 */
    printf("fopen error(%s)\n", strerror(errno));
    return -1;
}

今度はエラー時に下記のように “文字列として” エラーの原因を表示することができます。

fopen error(No such file or directory)

 

お!

今度は分かりやすい!

つまり存在しないファイルを読み込みモードで fopen してるのがダメってことだね!

そういうこと!

これでエラーの原因が特定できるので、エラーの解決もしやすいよね

今回の場合は fopen に指定するパスを見直せば良いわけだ!

こんな感じで strerror を使って errno を文字列化してやることで、エラー発生時の原因を簡単に特定できるようになります。

エラー番号の定義を調べる

また、エラー番号は errno を利用するためにインクルードした errno.h 自体、もしくは、errno.h からさらにインクルードされているヘッダーで定義されています。

なので、これらのヘッダーファイルを見れば、どのエラー番号がどのような意味を持つのかを確認することができます。

VSCode などの統合開発環境を利用していれば、マウス操作でインクルードされているヘッダーファイルを辿ることができるので、結構簡単に調べることができます。

例えば VSCode であれば、#include <errno.h> と記述した行の「errno.h」の部分にマウスカーソルを合わせて右クリックし、「定義へ移動」を続けてクリックすれば errno.h のファイルを開くことができます。

ヘッダーファイル表示する様子

私の PC の環境だと、errno.h の中でさらに sys/errno.h がインクルードされており、この sys/errno.h にエラー番号が定義されていました。

例えばエラー番号 2 は下記のように定義されています。

エラー番号の定義例
#define ENOENT          2               /* No such file or directory */

環境によって定義名が存在しなかったり、定義値が異なったりする可能性もあるので注意してください。

ちょっと見てみたけど定義名からはエラーの原因がさっぱり分からないものも多いね…

確かに!

まあでもエラーの原因を特定するための情報にはなる

例えば関数名とエラー番号の定義名を一緒に検索するようにすれば、解決方法を解説してるページもより見つけやすくなるよ!

errno 利用時の注意点

errno は(そのスレッドで)直前に発生したエラーの原因を示す値を格納する変数(マクロの場合もある)になります。

“直前に” ってところがポイントです。

例えばある関数でエラーが発生して errno が設定されたとしても、その後に他の関数でエラーが発生すると、その errno の値が後から実行された関数のエラー原因に上書きされてしまいます。

分かりやすい例を書くと、下記のようなソースコードでは本来表示したい fopen のエラーの原因が表示できません。

fcloseでのerrnoの上書き
FILE *f1, *f2;

f1 = fopen("text1.txt", "r");
if (f1 == NULL) {
    return -1;
}

f2 = fopen("", "r"); /* ファイル名がおかしいのでエラー */
if (f2 == NULL) {
    fclose(NULL); /* NULLをクローズしようとしてエラー */
    printf("fopen error(%s)\n", strerror(errno));
}

上記を実行すると、fclose(NULL); でエラーが発生します。そうなると、errnofclose でエラーが発生した原因を示す値に上書きされてしまいます。

ですので、本当は fopen 時のエラーの原因を表示したいのに、fclose 時のエラーの原因が表示されてしまうことになります。

このように errno に設定されているエラー番号は、”直前” に発生したエラーの原因になります。ですので、errno はエラーの直後に他の関数を実行してしまうと他のエラー番号に上書きされてしまう可能性があります。

上記は必ずエラーになる例なので分かりやすいですが、下記のように errno 表示前に printf を行うだけでも、printf 関数でエラーが発生して errno が上書きされる可能性があるので本当は良くありません。

printfによるerrnoの上書きの可能性
FILE *f;

f = fopen("", "r");
if (f == NULL) {
    printf("fopen error\n"); /* ここで上書きされる可能性あり */
    printf("errno : %d\n", errno);
    return -1;
}

このような errno の上書きを防ぐためには、関数のエラー直後に errno の値を退避しておく、もしくは関数のエラー直後に表示するようにすれば良いです。

例えば、下記のように関数のエラー直後(printf 実行前)に errno の値を他の変数(tmp_err)に格納すれば、たとえもし printf 関数実行時に errno が上書きされたとしても問題ありません。

errnoの退避
FILE *f;

f = fopen("", "r");
if (f == NULL) {
    int tmp_err = errno;
    printf("fopen error\n");
    printf("errno : %d\n", tmp_err);
    return -1;
}

また、下記のようにエラー発生直後に errno を表示しても OK です。

エラー直後のerrno表示
FILE *f;

f = fopen("", "r");
if (f == NULL) {
    printf("errno : %d\n", errno);
    printf("fopen error\n");
    return -1;
}

スポンサーリンク

errno を利用したプログラム例

最後にいくつかの関数をわざとエラーを発生させて、errno でエラー原因を表示する例を紹介しておきたいと思います。

fopen 関数のエラー

下記は今までも紹介してきた fopen 関数でエラーを発生させるプログラムです。

errno 自体は errno.h で宣言されているため、このソースコード上では変数宣言していないことが確認できると思います。

また、errno 利用時の注意点で解説したことを踏まえて fopen 実行直後に errnotmp_err に一旦退避しています(strerror 関数でエラーが発生して上書きされる可能性がある)。

fopen関数のエラー
#include <stdio.h>
#include <errno.h>
#include <string.h>

int main(void) {

    FILE *f;
    int tmp_err;

    f = fopen("text.txt", "r");
    if (f == NULL) {
        tmp_err = errno;
        printf("fopen error(%s:%d)\n", strerror(tmp_err), tmp_err);
        return -1;
    }
    fclose(f);

    return 0;
}

text.txt というファイルが存在しない状態で実行すると fopen 関数実行時にエラーが発生します。私の環境では下記のようにエラーの原因が表示されました。

fopen error(No such file or directory:2)

エラー番号 2 の定義名は ENOENT であり、これらの情報から、オープンしようとしているファイルが存在しないために fopen 関数に失敗していることが分かります。

malloc 関数のエラー

下記は malloc 関数でエラーを発生させるプログラムになります。

malloc関数のエラー
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main(void) {

    char *p;
    int size = -1;

    p = (char*)malloc(size);
    if (p == NULL) {
        int tmp_err = errno;
        printf("malloc error(%s:%d)\n", strerror(tmp_err), tmp_err);
        return -1;
    }
    free(p);

    return 0;
}

このプログラムを実行すると、私の PC では下記が表示されました。

malloc error(Cannot allocate memory:12)

12 の定義名は ENOMEM であり、これらの情報から、メモリが足りなくて malloc 関数に失敗している(引数で指定する値が大きすぎる)ことが分かります(malloc 関数の引数に指定するのは “符号なし” の値なのに -1 を指定してしまっている)。

スポンサーリンク

connect 関数のエラー

最後の例はソケット通信における connect 関数でエラーを発生させてみます。

connect関数のエラー
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#define SERVER_ADDR "127.0.0.1"
#define SERVER_PORT 8080

int main(void) {
    int sock;
    struct sockaddr_in addr;

    /* ソケットを作成 */
    sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (sock == -1) {
        printf("socket error\n");
        return -1;
    }

    /* 構造体を全て0にセット */
    memset(&addr, 0, sizeof(struct sockaddr_in));

    /* サーバーのIPアドレスとポートの情報を設定 */
    addr.sin_family = AF_INET;
    addr.sin_port = htons((unsigned short)SERVER_PORT);
    addr.sin_addr.s_addr = inet_addr(SERVER_ADDR);

    /* サーバーに接続要求送信 */
    if (connect(sock, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) == -1) {
        int tmp_err = errno;
        printf("connect error(%s:%d)\n", strerror(tmp_err), tmp_err);
        close(sock);
        return -1;
    }

    /* ソケット通信をクローズ */
    close(sock);

    return 0;
}

実は、これはエラー終了することなく正しく動作するプログラムになります。

ただし、connect 関数で接続する先の PC(で実行するサーバープログラム)のソケットが接続待ちになっている時のみです。

サーバープログラムを用意せずに実行すると、connect 関数でエラーが発生し、私の環境では下記のようなエラーの原因が表示されました。

connect error(Connection refused:61)

61 の定義名は ECONNREFUSED であり、これらの情報から接続先の PC から接続が拒まれていることが原因と推測できます。

要は、接続先の PC は接続待ちをしていないので接続が拒まれているということです。

また、このプログラムを実行する PC がネットワークに接続していない場合も connect 関数でエラーが発生し、私の環境では下記のようなエラーの原因が表示されました。

connect error(Network is unreachable:51)

51 の定義名は ENETUNREACH であり、これらの情報から接続先の PC に到達できないことが原因と推測できます。

これは接続元の PC と接続先の PC との間にネットワーク経路が存在しない場合に発生するエラーで、接続元の PC がネットワークに繋がっていないとこのようなエラーが発生します。

こんな感じで、関数でエラーが発生する原因はさまざまです。特にソケット通信関連の関数では、プログラム自体の誤りだけでなく、サーバーの状態やネットワークの状態などに応じてエラーが発生する原因が異なります。

単なるエラーと捉えるとエラーを解消するのは難しいですが、原因も踏まえて考えるとエラーの解消方法が考えやすくなると思います。

また、自身でエラーの原因の意味やその原因の解消方法が分からない場合でも、「関数名 エラー番号の定義名(or エラー番号)」でググったりすると、その解消方法を紹介しているページを見つけて解消することができたりします。

例えば上記の例でいうと「connect ECONNREFUSED」でググると解説ページはたくさん見つかります!

まとめ

このページではC言語における errno について解説しました!

関数実行時にエラーが発生した時に、そのエラーを解消するためにはエラーの原因を知るのが手っ取り早いです。

そして、そのエラーの原因は errno により特定することができます。

ただし、全ての関数で errno が設定されるとは限らない点には注意してください。例えば自身で作成した関数で発生したエラーでは、errno が設定されるとは限りません(関数内での標準関数実行時にエラーが発生した時などは errno が設定される可能性がある)。

プログラミングをしているとエラーが発生してその解消方法に悩まされることは多々あります。

エラーの原因が分からなくて途方に暮れてしまう前に、是非この errno を利用してエラーの原因を調べてみてください!

オススメの参考書(PR)

C言語学習中だけど分からないことが多くて挫折しそう...という方には、下記の「スッキリわかるC言語入門」がオススメです!

まず学習を進める上で、参考書は2冊持っておくことをオススメします。この理由は下記の2つです。

  • 参考書によって、解説の仕方は異なる
  • 読み手によって、理解しやすい解説の仕方は異なる

ある人の説明聞いても理解できなかったけど、他の人からちょっと違った観点での説明を聞いて「あー、そういうことね!」って簡単に理解できた経験をお持ちの方も多いのではないでしょうか?

それと同じで、1冊の参考書を読んで理解できない事も、他の参考書とは異なる内容の解説を読むことで理解できる可能性があります。

なので、参考書は2冊持っておいた方が学習時に挫折しにくいというのが私の考えです。

特に上記の「スッキリわかるC言語入門」は、他の参考書とは違った切り口での解説が豊富で、他の参考書で理解できなかった内容に対して違った観点での解説を読むことができ、オススメです。題名の通り「なぜそうなるのか?」がスッキリ理解できるような解説内容にもなっており、C言語入門書としてもかなり分かりやすい参考書だと思います。

もちろんネット等でも色んな観点からの解説を読むことが出来ますので、分からない点は別の人・別の参考書の解説を読んで解決していきましょう!もちろん私のサイトも参考にしていただけると嬉しいです!

入門用のオススメ参考書は下記ページでも紹介していますので、こちらも是非参考にしていただければと思います。

https://daeudaeu.com/c_reference_book/

同じカテゴリのページ一覧を表示