本文へスキップ
BecomeCoder

C言語コース · 第14章 複数ファイルと堅牢化 · レッスン52

エラー処理の作法と可変長引数 ― errno・perror・戻り値規約・stdarg

ブラウザで完結

導入

Cには例外(trycatch)がありません。関数が「うまくいかなかった」ことをどう呼び出し元に伝えるかは、すべて規約にゆだねられています。この規約を知らずに他人のCコードを読むと、なぜあちこちで戻り値を if でチェックしているのか分からず戸惑うことになります。

説明

戻り値による規約

Cの標準ライブラリでは、「int を返す関数は、0 を成功、0 以外を失敗として返す」「ポインタを返す関数は、失敗したら NULL を返す」という緩やかな規約がよく使われます。第8章で見た malloc が確保に失敗すると NULL を返すのも、この規約の一例です。呼び出す側は、戻り値を必ず確認してから結果を使う、という書き方が基本になります。

void *p = malloc(100);
if (p == NULL) {
    /* 確保に失敗した。ここで処理を中断するなどの対応をする */
} else {
    /* p を安全に使える */
}

errno ― 標準ライブラリが失敗の理由を教えてくれる仕組み

<errno.h> をインクルードすると使える errno は、標準ライブラリの関数が失敗したときに「なぜ失敗したか」を表す番号をセットするグローバル変数です。数値のままでは読みにくいので、<string.h>strerror(errno) や、<stdio.h>perror("説明") で人間向けのメッセージに変換します。数値を文字列に変換する strtol は、変換結果が大きすぎて long に収まらないとき、errnoERANGE(範囲外)をセットします。

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

int main(void) {
    errno = 0;
    char *end;
    long v = strtol("99999999999999999999", &end, 10);  /* 桁が大きすぎてオーバーフロー */
    if (errno == ERANGE) {
        printf("変換エラー: %s\n", strerror(errno));
    } else {
        printf("value=%ld\n", v);
    }
    return 0;
}

errno を確認する前に必ず errno = 0; でリセットしておくのが定石です(成功しても errno0 に戻してくれる関数ばかりではないため、前の失敗の値が残っていると誤判定してしまいます)。ファイルを開く fopen が失敗したときも同じ仕組みで、返り値が NULL になり errnoENOENT(ファイルが見つからない)などがセットされます。ただしファイル操作はこのサイトのブラウザ実行環境では期待どおりに動かないことがあるため、実際に手元の環境で試すときに覚えておいてください。

stdarg ― 個数の決まっていない引数を受け取る

第10章では <stdarg.h> を使って va_listva_startva_argva_end の基本を学びました。ここでは同じ道具を、「呼び出し側と受け取り側の約束を守らないと簡単に壊れる」というエラー処理的な視点で振り返ります。va_list という「引数を1つずつ取り出すためのカーソル」を用意し、va_start で開始位置を合わせ、va_arg で型を指定しながら1つずつ取り出し、最後に va_end で片付ける、という流れは前章のとおりです。

#include <stdio.h>
#include <stdarg.h>

int sum(int count, ...) {
    va_list args;
    va_start(args, count);   /* count の次から可変長引数が始まる */
    int total = 0;
    for (int i = 0; i < count; i++) {
        total += va_arg(args, int);   /* int型として1つ取り出す */
    }
    va_end(args);
    return total;
}

int main(void) {
    printf("%d\n", sum(3, 10, 20, 30));
    printf("%d\n", sum(5, 1, 2, 3, 4, 5));
    return 0;
}

sum は「最初の引数 count で個数を教えてもらい、その数だけ va_arg で取り出す」という、可変長引数を使う関数によくある形です。count の値と実際に渡した引数の個数がずれていたり、型を間違えて va_arg で取り出したりすると未定義動作になるので、呼び出し側との約束(個数・型)を守ることが重要です。

やってみよう

sum の呼び出しを増やしたり、渡す個数と値を変えたりして結果を確かめましょう。strtol の例(コピーしてエディタに貼り付けると実行できます)で、"123" のような普通の数値文字列を渡すと errnoERANGE にならず value=123 が表示されることも確認してみてください。

演習

va_list を使って、可変個の double 引数の平均を返す関数 average(int count, ...) を書いてください。average(4, 10.0, 20.0, 30.0, 40.0) の結果を printf("%.1f\n", ...) で表示してください(正しく書けると 25.0 になります)。

ヒント1を見る

double の可変長引数は va_arg(args, double) のように、int ではなく double を指定して取り出します。

ヒント2を見る

関数の戻り値の型も double にし、合計を countint)で割るときに整数同士の割り算にならないよう注意してください(totaldouble にしておけば自然と実数の割り算になります)。

実際に動かしてみよう

下のエディタにCを書いて「コンパイル & 実行」を押すと、ブラウザ内で本物のCコンパイラ(clang + dlmalloc)がその場でコンパイルして実行し、出力が表示されます。構造体・共用体・malloc/free・関数ポインタ・ファイル入出力などがそのまま動きます。本文の例を書き換えて試しながら進めましょう(初回だけコンパイラの読み込みに時間がかかります。以降はキャッシュされます。この回は標準入力(scanf)は使わず、値はコードに直接書きます)。

C/C++ — ブラウザ内で本物のコンパイラで実行

ブラウザ内で本物のC/C++コンパイラ(clang + libc++ / WebAssembly)を動かします。構造体・malloc/free・std::string・vector・クラス・テンプレート・STL などがそのまま実行できます(初回のみコンパイラの読み込みに時間がかかります。以降はブラウザにキャッシュされます)。
スクロールして表示された時点でも自動で読み込まれます。