C言語のerrnoの使い方: 初期化、文字列化、参照、ハンドリング方法の具体例
- 作成日: 2021-08-02
- 更新日: 2023-12-26
- カテゴリ: C言語
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;
}
↑の場合、errno
を0
で初期化した後にstrerror()
に渡すと、「No error
」という文字列が返ってきます。
これはerrno
の0
という値がエラーの状態を持っていないことを表しています。
つまりerrno
を0
で初期化するのは「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()
関数は引数arg
が0
より下の値だった場合にerrno
にEINVAL
を保存します。
EINVAL
は引数が不正だった場合のエラー定数です。
check()
の呼び出し前にerrno
を初期化します。これはcheck()
の呼び出し前のエラー状態をクリアするためです。
check()
を呼び出した後にerrno
が0
かどうかチェックします。errno
の0
はエラーを持っていない状態なので、errno
が0
以外であればerrno
はエラーを持っているということになります。
↓のコードを見てみましょう。
errno = 0; // errnoの初期化
check(1);
// エラーは発生しない
if (errno != 0) {
printf("FAIL.\n");
}
このcheck()
の呼び出しではcheck()
に渡している引数は1
です。
check()
は引数が0
より下の場合にerrno
にエラー定数を保存する関数なので、この場合はerrno
は0
のままです。
よって、check()
の呼び出し後にerrno
をチェックしてますが、errno
は0
のままなので、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()
は内部でerrno
にEINVAL
を保存します。
if (errno != 0)
という条件式が真になるので、if
文の中が実行されます。
errno
を参照するわけですが、errno
の参照で気をつけたいのがerrno
の値の退避です。
↓のようにcheck()
に近い所でerrno
を別の変数に保存しておきます。
int save_errno = errno; // errnoを保存する
これはなぜかというと、errno
はグローバル変数なので、標準ライブラリを呼び出すごとにその値がころころ変わる可能性があるのです。
そのためif
文の中でprintf()
などを使った場合、参照したいerrno
の値が変わってしまう可能性があります。
そこでsave_errno
にerrno
の値を保存しておきます。こうすれば参照したいエラー定数を維持することが出来ます。
これは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
について使い方を見てみました。
標準ライブラリなどで使われているエラーハンドリングの機構ですが、あまり表には出てこない不思議なやつです。
この記事を読めば標準ライブラリのエラーハンドリングはばっちりですね。
🦝 < エラー定数ぜんぶ覚えた(大嘘
🐭 < おそろしい子・・・