C言語のライブラリ関数は、戻り値だけでは「なぜ失敗したのか」を教えてくれないことがあります。
そこで使うのがerrnoです。
本記事では、「戻り値で失敗を検知する」「直後にerrnoを読む」「数値ではなく定数名で比較する」という基本を、初心者の方にもわかりやすく丁寧に解説します。
サンプルコードは実行結果付きで示し、よくある落とし穴やデバッグの勘所も整理します。
errnoとは?エラー原因を知る基本
errnoの意味と役割
errnoは、一部のライブラリ関数がエラーになったときに、その原因を表す番号を格納するための仕組みです。
型はintで、ヘッダerrno.hで宣言されています。
多くの実装ではスレッドごとに独立しています(スレッドを使わない初学者の段階では「直近のエラー理由が入る場所」と理解すれば十分です)。
大切な点は、errno単体では成功・失敗を判断できないことです。
まずは戻り値で失敗を検知し、その直後にerrnoを読みます。
ライブラリ関数とerrnoの関係
- 多くの関数は、失敗すると特別な戻り値(
NULL、-1、境界値など)を返し、errnoにエラー理由(例えばENOENTやEINVAL)を設定します。 - ただしすべての関数が
errnoを使うわけではありません。C標準/実装の仕様書を確認してください。- 例:
fopenは失敗時にNULLを返し、errnoに原因を設定するのが一般的です。 - 例:
strtolは範囲外のときERANGEを設定し、失敗理由の判定にendptrも併用します。 - 例: 数学関数(
log、acosなど)は領域外でEDOMやERANGEを設定することがあります(実装依存)。
- 例:
ヘッダの読み込み
errnoを使うには、必ず#include <errno.h>が必要です。
実際に呼ぶ関数のヘッダ(stdio.h、stdlib.h、math.hなど)も忘れないでください。
// errno_header.c
#include <stdio.h> // printf
#include <errno.h> // errno
int main(void) {
// 何もしていないのでerrnoの値は未定義のまま(意味を持たない)
// 成功・失敗の判定にerrnoを単独で使ってはいけません。
printf("この時点のerrnoは意味を持ちません(値=%d)\n", errno);
return 0;
}
例示のための出力(環境により値は異なります):
この時点のerrnoは意味を持ちません(値=0)
errnoが更新されるタイミング
関数が失敗し、その関数がerrnoを使う設計のときに更新されます。
成功時に0へクリアされる保証はありません。
戻り値がエラーを示した直後に読むのが鉄則です。
errnoの使い方
基本手順
- 関数を呼び、まず戻り値で成功・失敗を判定します。
- 失敗だった場合に限り、直後に
errnoを読むか、必要ならローカル変数へ保存します。 - 比較は定数名(例:
ENOENT)を使い、数値の生値で比較しません。
戻り値のチェック方法
- ポインタ返却関数(例:
fopen)は、NULLなら失敗です。 - 整数返却関数は、規約で決められた失敗値(
-1など)で判断します。 - 変換系(例:
strtol)はendptrや境界値とerrnoの両方で判定します。
errnoの比較は数値ではなく定数名
環境により番号は異なるため、errno == 2のような比較は避け、errno == ENOENTのように定数名で書きます。
成功時はerrnoを見ない
成功時にerrnoが0である保証はありません。
以前の失敗の値が残っているかもしれないため、成功した呼び出しではerrnoを参照しないでください。
直後に読む/必要なら値を保存する
errnoは別の関数呼び出しで上書きされる可能性があるため、必要ならすぐローカル変数に退避します。
使用例のイメージ
以下は、ファイルオープンの失敗でerrnoを正しく読む最小例です。
定数名を表示するために簡単なヘルパ関数を用意しています(実装により定義のないコードもあるため#ifdefで守っています)。
// errno_fopen_example.c
#include <stdio.h>
#include <errno.h> // errno, ENOENT など
#include <stdlib.h>
// errno の数値から代表的な定数名を返すヘルパ(学習用)
// 実装により未定義の定数がある可能性があるので #ifdef で保護
static const char* errno_name(int e) {
switch (e) {
#ifdef ENOENT
case ENOENT: return "ENOENT";
#endif
#ifdef EINVAL
case EINVAL: return "EINVAL";
#endif
#ifdef ERANGE
case ERANGE: return "ERANGE";
#endif
#ifdef EDOM
case EDOM: return "EDOM";
#endif
#ifdef EAGAIN
case EAGAIN: return "EAGAIN";
#endif
#ifdef ENOMEM
case ENOMEM: return "ENOMEM";
#endif
default: return "UNKNOWN_ERRNO";
}
}
int main(void) {
// 存在しないファイルをあえて開く
const char* path = "no_such_file.txt";
FILE* fp = fopen(path, "r");
if (fp == NULL) {
// 失敗を戻り値で検知 → 直後に errno を読む
int saved = errno; // 退避が安全
printf("fopen に失敗しました: path=%s\n", path);
printf("errno=%d (%s)\n", saved, errno_name(saved));
// エラーの種類で分岐(数値ではなく定数名で比較)
if (saved == ENOENT) {
printf("原因: ファイルが存在しません(ENOENT)\n");
}
return EXIT_FAILURE;
}
// ここは成功時の処理(この例では到達しません)
fclose(fp);
return EXIT_SUCCESS;
}
想定される実行結果(環境により番号は異なります):
fopen に失敗しました: path=no_such_file.txt
errno=2 (ENOENT)
原因: ファイルが存在しません(ENOENT)
次は、strtolでの範囲エラー(ERANGE)と、数値でない入力を区別する例です。
// errno_strtol_example.c
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <limits.h>
int main(void) {
const char* ok = "42";
const char* huge = "9999999999999999999999999999";
const char* bad = "abc";
// 成功例: 成功時は errno を参照しない
errno = 0; // クリアしてもよいが、成功時は使わないのが鉄則
char* end = NULL;
long v1 = strtol(ok, &end, 10);
if (end != ok) {
printf("OK: %s -> %ld\n", ok, v1);
}
// 範囲外(オーバーフロー)の例: errno==ERANGE を確認する
errno = 0; // 直前でクリアしておくと検知しやすい
end = NULL;
long v2 = strtol(huge, &end, 10);
if (errno == ERANGE) {
printf("ERANGE: 範囲外の数値です。入力=%s, 返却=%ld(例: LONG_MAX)\n", huge, v2);
}
// 数値でない入力の例: endptr で判定する(このケースは errno を使わない)
errno = 0; // ここでの errno は未使用
end = NULL;
long v3 = strtol(bad, &end, 10);
(void)v3; // 未使用抑止
if (end == bad) {
printf("INVALID: 数値が読み取れませんでした。入力=%s\n", bad);
}
return 0;
}
実行結果の一例:
OK: 42 -> 42
ERANGE: 範囲外の数値です。入力=9999999999999999999999999999, 返却=9223372036854775807(例: LONG_MAX)
INVALID: 数値が読み取れませんでした。入力=abc
errnoの注意点とよくあるミス
errnoは自動で0に戻らない
一度エラーで設定されたerrnoは、自動では0に戻りません。
そのため成功時にerrnoを見て「0だから成功だ」と判断してはいけません。
// errno_persists_example.c
#include <stdio.h>
#include <errno.h>
int main(void) {
errno = 0;
// 失敗して errno が設定される想定
FILE* fp = fopen("no_such_file.txt", "r");
if (!fp) {
printf("失敗直後 errno=%d\n", errno);
}
// ここで成功する関数を呼んでも errno は自動で0に戻らない
puts("これは成功する出力です");
printf("成功後でも errno はそのまま=%d\n", errno);
return 0;
}
実行結果の一例:
失敗直後 errno=2
これは成功する出力です
成功後でも errno はそのまま=2
別の関数で上書きされる
直前のエラー原因を保持したいときは、errnoをすぐ変数に退避してください。
次の呼び出しで上書きされる可能性があるからです。
// errno_overwrite_example.c
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <limits.h>
int main(void) {
// 1) fopen の失敗で ENOENT になる想定
FILE* fp = fopen("no_such_file.txt", "r");
if (!fp) {
int saved = errno; // 直後に退避
printf("fopen 失敗: errno=%d\n", saved);
// 2) 別の処理の途中で errno が別の値に変わる例(strtol の ERANGE)
errno = 0;
char* end = NULL;
(void)strtol("999999999999999999999999", &end, 10); // 範囲外を意図
printf("別処理後の現在の errno=%d\n", errno);
// 退避済みの値を使って本来の原因を報告できる
if (saved == ENOENT) {
printf("本来の原因(退避した値): ENOENT(ファイルがない)\n");
}
}
return 0;
}
実行結果の一例:
fopen 失敗: errno=2
別処理後の現在の errno=34
本来の原因(退避した値): ENOENT(ファイルがない)
※ 数字は環境依存です。
ここでは34がERANGEに対応している例です。
未定義のerrnoを信じない
戻り値が成功を示しているのにerrnoを読むのは誤りです。
古い値が残っているだけかもしれません。
逆に、戻り値が失敗を示していないのにerrnoが非0でも、それは今回の呼び出しの失敗を意味しません。
番号の値は環境依存
同じerrno名でも番号はOSや実装で異なります。
数値で比較・記録せず、定数名(ENOENTなど)で扱ってください。
ログに出すときは番号と定数名の両方を出すと読みやすいです。
デバッグ時の確認ポイント
- 戻り値で失敗を検知しているか、失敗直後に
errnoを読んでいるかを確認します。 - 途中で
errnoが上書きされていないか。必要ならint saved = errno;で退避します。 - 比較は
ENOENT等の定数名で行っているか。 strtolなどは戻り値・endptr・errnoの組み合わせで判定しているか。- 成功時に
errnoを見て判断していないか。
よく使うerrnoの例と意味
下の表は代表的なerrnoの概要です。
番号は実装依存なので示しません。
| 定数名 | 意味(代表) | よく起こる場面(例) |
|---|---|---|
| ENOENT | エンティティが存在しない | fopenで存在しないファイル |
| EINVAL | 無効な引数 | 不正なパラメータ値、範囲外の引数 |
| ERANGE | 範囲外 | strtolや数学関数のオーバーフロー/アンダーフロー |
| EDOM | 定義域エラー | acos(2.0)やlog(-1.0)など |
| EAGAIN | 一時的に利用不可 | ノンブロッキングI/Oで後でもう一度(主にPOSIX) |
| ENOMEM | メモリ不足 | malloc等(設定は実装依存) |
ENOENT
「対象が存在しない」エラーです。
fopen("no_such_file.txt", "r")の例のように、ファイルやディレクトリが見つからないときに発生します。
EINVAL
「無効な引数」です。
関数に不正な値やサポート外の指定を渡した場合に起こります。
どの関数がEINVALを使うかは関数ごとに異なるため、公式ドキュメントを確認してください。
ERANGE
「範囲外」です。
strtolでオーバーフローしたときや、数学関数の範囲エラーで設定されます。
上のstrtolサンプルのように、呼び出し前にerrno = 0でクリアしておくと検知しやすいです。
EDOM
「定義域エラー」です。
数学関数において、入力がその関数の定義域外のときに発生します。
実装によりerrnoを使わない設定の場合もあるため注意してください。
参考例(実装により挙動が異なる可能性あり。数学関数は-lmでのリンクが必要な環境があります):
// errno_math_domain_example.c
#include <stdio.h>
#include <math.h>
#include <errno.h>
int main(void) {
errno = 0;
double x = acos(2.0); // 定義域 [-1,1] の外
if (errno == EDOM) {
printf("EDOM: 定義域エラーを検出しました。acos(2.0)\n");
}
// isnan による検査も併用できる
if (isnan(x)) {
printf("結果は NaN です\n");
}
return 0;
}
想定される実行結果(一例):
EDOM: 定義域エラーを検出しました。acos(2.0)
結果は NaN です
EAGAIN
「リソースが一時的に不足/利用不可で、もう一度試せば成功する可能性がある」ことを示します。
非ブロッキングI/Oやプロセス/スレッド生成など、主にPOSIX系APIで見られます。
C標準ライブラリだけを使う小規模プログラムでは遭遇頻度は低めです。
ENOMEM
「メモリ不足」です。
ただし、mallocが必ずerrnoをENOMEMに設定するとはC標準では保証されません。
多くの実装やPOSIX環境では設定されますが、戻り値NULLの判定を第一にしてください。
まとめ
errnoは「なぜ失敗したか」を知るための補助情報であり、成功・失敗の判定は必ず戻り値で行うのが大原則です。
失敗を検知したらerrnoを直後に読み、必要なら退避します。
比較は数値ではなく定数名を使い、成功時にerrnoを参照する誤りを避けましょう。
番号は環境依存であること、strtolや数学関数のようにerrno以外の情報(endptrや返り値の境界値)も併用する関数があることも忘れないでください。
これらの基本を守れば、エラーの原因特定が一気に安定します。
なお、errnoの内容を人間にわかりやすいメッセージにするperrorやstrerror、およびassertによる不整合検出は別の記事で詳しく扱います。
