見出し画像

続C言語教室 - 第6回 エラー処理の基本

C言語にはエラー処理に関する特別な構文はありません。基本的には関数を呼び出した後に戻り値などでエラーの有無を確認して必要に応じた処理を行うということになっています。ですから戻り値のチェックを端折ることでエラーを無視してしまうことは日常茶飯事です。これがC言語で書かれたプログラムの信頼性に疑問符が付いたり脆弱性の原因になることも多くあります。

確かにプログラムの見通しの良さや、本当に例外的に発生するようなエラーを、どこまで丁寧に拾うかは悩むこともありますが、エラーに関する定義が明確に書かれているライブラリ関数を呼び出した場合は「必ず」エラーが発生したかを確認し、何らかの対処を記述するのは「プロの」プログラマの掟です。エラー処理を丁寧に書くと、コードを読む際の処理の流れが中断され、読みにくくなるのは確かなのですが、そこをうまく捌くのも技量のウチです。

少なくともファイルに関する処理に対しては、エラーが発生することは充分に想定され、間違いなく何らかの対処が必要なので、ツマラナイとは思ったとしてもちゃんと処理を書きましょう。ということで、ファイルを開いて処理を行い、最後にちゃんと後片付けをする際の例を考えてみましょう。

#include <stdio.h>
#include <stdlib.h>

int main() {
  int rval = 0;

  FILE* file = fopen("sample.txt", "w");
  if (file == NULL) {
    fprintf(stderr, "fopen:error(%d)\n", errno);
    return 1;
  }

  rval = fprintf(file, "Hello World\n");
  if (rval < 0) {
    fprintf(stderr, "fprintf:error(%d)\n", errno);
    return 1;
  }

  rval = fclose(file);
  if (rval != 0) {
    fprintf(stderr, "fclose:error(%d)\n", errno);
    return 1;
  }
  return 0;
}

ここではファイルを開いて文字列を書き出し、そしてファイルを閉じています。fopen、fprintf、fcloseはいずれもエラーが発生するおそれがあるので、戻り値を逐一チェックします。エラーが発生していれば理由を示すグローバル変数である errno の値を標準エラー出力に発生元の関数名とともに表示してmainを異常終了させます(正常に最後まで行ったら正常終了させる)。

それぞれのライブラリ関数が、どのような値を返した時にエラーの発生を意味するかは、それぞれの関数のドキュメントに明示されています。一般論ですがポインタを返す関数の場合はNULL、整数を返す関数の場合は負の値を返した時はエラーが発生しています。エラーが発生した場合は、グローバル変数 errno に理由を示すコードが入っています。

errno はグルーバル変数なので再びエラーが発生した場合に値が重ね書きされるので、すぐに使うか他の変数に退避して置かなければなりません。値の意味は error.h に定義されており、値を示すマクロ名が定義されていて個別のエラーの判定に使えます。またエラーメッセージを表示したい場合には、strerror または perror を使います。

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

strerrorは引き数にerrnoを渡すと、対応するメッセージ文字列へのポインタを返します。

#include <stdio.h>
void perror(const char *s);

perrorは、引き数がNULLで無ければ引き数で渡した文字列をエラーメッセージの前に追加したうえで、標準エラー出力にエラーメッセージを出力します。

例えば、fopen がエラーになる主な理由としては、

  • ファイルを作成できない

  • ファイルに書き込めない

などが考えられますが、いずれも処理を継続できないので、理由を示したうえで処理を中断せざる得ないでしょう。エラーが発生した場合の対処としては大きく分けると

  • 中断する

  • 再度実行する

  • 無視する

のいずれかが必要で中断することが多いとは思いますが、いきなりプログラムが終了してしまうのではなく該当する処理のみを中断することの方が多いでしょう。この時、処理を継続するために必要な処理または終了するために片付けなければならない処理をちゃんと済ませることを忘れてはいけません。

エラー処理は、そのエラーを実際に発生させて処理を確認することが難しく、コードに手を加えて処理が正しく動くか確かめる羽目になることも良くあります。エラーが発生した場合に直ちにプログラムが終了するのであれば問題を起こすことも少ないでしょうが、そのまま継続する場合はエラー処理でリソースを片付けたりメモリを解放するなどを忘れると続く処理の中で問題を引き起こすことも珍しくありません。そして書いたつもりがバグがあったりすると、それが原因であることを突き止めるのに苦労します。ですからエラー処理はあまり頑張らずに必要な処理を書くに留めるのが吉です。

エラー処理でどんなことをするべきかはデバッグ時とリリース時でも異なりますし、プログラムがコマンド的なものかアプリ的なものかでも異なります。またバックグラウンドで動作することが想定されている場合は画面に出力してもフォアグラウンドの表示に紛れてしまうだけなので適切ではありません。

エラー処理をどのように書くのかは、いろいろな流儀もあって難しいのですが、エラーチェックをサボって理由もわからずプログラムが終了してしまったり、エラーが発生した場合のみ発生する謎のバグを仕込んでしまわないように、関数を呼び出したらとにかくエラーが発生していないか確実に調べましょう。C言語ではエラーをチェックするしないもコードを書く人の自由なのでチェックをしなくてもエラーにならないですが、自分で関数を書くときもエラー状態を確実に呼び出し元に伝えて適切な処理を書いてもらうためにも、常にエラーを意識できるようになることが初心者脱出のポイントです。

でもエラー処理って、やはり難しいんですよね。テストが大変ですし、エラー処理の中でエラー処理をする羽目になるとやぶ蛇です。業務で書くプログラムだとメインの処理よりエラー処理のほうがコードが長くなることなんて珍しいことではありません。もっとも苦労して書いたエラー処理が実際には一度も呼ばれる事態はなかったなんて言うことも良くあるのですよね。

PS

AIが書いたコードは、エラー処理がスルーされていたり、無茶苦茶なコードを出してくることが良くあります。それはきっと学習元になるコードがそうだったりすることが多いのでしょうね。

ヘッダ画像は、以下のものを使わせていただきました。
https://www.irasutoya.com/2019/09/blog-post_109.html

#C言語 #プログラミング講座 #ファイル処理 #エラー処理 #エラーメッセージ #戻り値 #errno #strerror #perror #標準エラー出力

この記事が気に入ったらサポートをしてみませんか?