このページではC言語における errno
について解説します。
うーん、どうしても fopen
関数でエラーになる…
fopen("text.txt", "r");
C言語諦めてしまいそう…
そんな方法あるの?
プログラミングしていて避けて通れないのが「エラー」です。
作成したプログラムでエラーが発生して悪戦苦闘したことは、プログラマーの方であれば皆さん経験あるのではないでしょうか。
この「エラー」を解決できずにプログラミングを諦めたり挫折した人もいるでしょう…。
で、この「エラー」を解決するためにはそのエラーが発生した原因・理由を知るのが手っ取り早いです。
自身で作成した関数でエラーが発生しているのであれば、その関数の中に printf
などを仕込んで、どこでエラーが発生しているかを調べれば原因を推測することが可能ですよね。
で、厄介なのは標準関数(ライブラリ関数)などのソースコードが変更できないような関数内でのエラーです。この場合はソースコードに printf
を仕込んでエラーの原因を調べることができません(頑張ればできるのかも…)。
そこで活用していただきたいのが、今回紹介する errno
の仕組みです。
実は、errno
を利用すれば標準関数(システムコール)で発生した原因を調べることが可能です!
このページではこの errno
について解説していきたいと思います!
主に MacOS や Linux 向けの解説になります
Windows でも同様の仕組みはありますが、インクルードするファイルなどが異なる可能性があるので注意してください
Contents
errno
とは
errno
とは、システムコールやライブラリ関数(標準ライブラリの関数や socket
ライブラリの関数などなど)で発生した「エラーの原因を示す値」が設定される変数(やマクロ)になります。
より具体的には直前に発生した「エラーの原因を示す値」が設定されています(また、errno
はスレッド毎に設定されます)。
エラーの原因を示す値を「エラー番号」と呼びます。
エラー発生時にerrno
を参照することで、その番号を取得することができ、この値からエラーの原因を知ることができます。
errno
の使い方
この errno
を利用するためには errno.h
をインクルードしておく必要があります。
#include <errno.h>
この errno.h
のインクルードさえしてしまえば、あとは変数宣言などの前準備を行う必要もなく、そのまま errno
を変数のように使用することができます。
errno
はライブラリ関数やシステムコールでエラー発生時に自動的に値が設定されます。
ですので、ユーザーからすると「エラー発生時に errno
を参照する」のが基本的な使い方になります。
例えば fopen
関数でのエラーの原因を示す値を表示する場合は、下記のようにエラー直後に errno
を出力します。
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
で行われています。
#include <string.h>
char *strerror(int errnum);
引数で指定する errnum
はエラー番号です。なので、errno
自体もしくは errno
の値を格納した変数を指定して実行します。
例えば前述の 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
のエラーの原因が表示できません。
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);
でエラーが発生します。そうなると、errno
が fclose
でエラーが発生した原因を示す値に上書きされてしまいます。
ですので、本当は fopen
時のエラーの原因を表示したいのに、fclose
時のエラーの原因が表示されてしまうことになります。
このように errno
に設定されているエラー番号は、”直前” に発生したエラーの原因になります。ですので、errno
はエラーの直後に他の関数を実行してしまうと他のエラー番号に上書きされてしまう可能性があります。
上記は必ずエラーになる例なので分かりやすいですが、下記のように errno
表示前に printf
を行うだけでも、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
が上書きされたとしても問題ありません。
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 です。
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
実行直後に errno
を tmp_err
に一旦退避しています(strerror
関数でエラーが発生して上書きされる可能性がある)。
#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
関数でエラーを発生させるプログラムになります。
#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
関数でエラーを発生させてみます。
#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/