第8章:標準ライブラリの仕様詳細

この章で学ぶこと

  • 標準ライブラリの保証と制約
  • errno の正しい使い方
  • シグナルハンドラの制限
  • restrict 修飾子の意味
  • 未定義動作を引き起こすライブラリ呼び出し
  • ---

    8.1 ライブラリ関数の制約

    制約違反

    ライブラリ関数に不正な引数を渡すと、通常は未定義動作です。

    memcpy(NULL, src, n);  /* 未定義動作 */
    strlen(NULL);          /* 未定義動作 */
    printf("%d", 3.14);    /* 未定義動作 */
    

    「shall」の意味

    規格でライブラリ関数の引数に「shall」が使われている場合、違反は未定義動作です。

    > The memcpy function copies n characters from the object pointed to by s2 into the object pointed to by s1. If copying takes place between objects that overlap, the behavior is undefined.

    ---

    8.2 errno

    正しい使い方

    #include <errno.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    errno = 0;  /* 呼び出し前にクリア */
    double d = strtod(str, &endptr);
    
    if (errno == ERANGE) {
        /* オーバーフローまたはアンダーフロー */
    }
    

    注意点

  • 呼び出し前にクリア: 関数は errno を 0 にしない
  • 成功時の値は不定: エラーがなくても errno が変更されることがある
  • マクロである: &errno は有効
  • /* NG: errno の値だけでエラー判定 */
    strtol(str, &endptr, 10);
    if (errno != 0) { /* 危険 */ }
    
    /* OK: 戻り値と errno の組み合わせ */
    errno = 0;
    long val = strtol(str, &endptr, 10);
    if (endptr == str || errno != 0) { /* エラー */ }
    

    ---

    8.3 シグナルハンドラの制限

    安全でない操作

    シグナルハンドラ内では、多くの操作が未定義動作です。

    /* NG: 非async-signal-safe関数 */
    void handler(int sig) {
        printf("Signal %d\n", sig);  /* 未定義動作 */
        malloc(100);                  /* 未定義動作 */
    }
    

    async-signal-safe 関数

    規格で明示的に許可された関数のみ使用可能:

  • abort()
  • _Exit()
  • signal() (限定的)
  • atomic_signal_fence()
  • volatile sig_atomic_t へのアクセス
  • 安全なパターン

    volatile sig_atomic_t flag = 0;
    
    void handler(int sig) {
        flag = 1;  /* OK */
    }
    
    int main(void) {
        signal(SIGINT, handler);
        while (!flag) {
            /* メインループ */
        }
    }
    

    ---

    8.4 restrict 修飾子

    標準ライブラリでの使用

    void *memcpy(void * restrict s1, const void * restrict s2, size_t n);
    void *memmove(void *s1, const void *s2, size_t n);  /* restrict なし */
    

    意味

    restrict は「このポインタを通じてのみアクセス」の約束。

    /* memcpy: オーバーラップ禁止 */
    memcpy(dst, src, n);  /* dst と src がオーバーラップすると未定義 */
    
    /* memmove: オーバーラップ許可 */
    memmove(dst, src, n);  /* 常に安全 */
    

    restrict 違反

    char arr[10] = "hello";
    memcpy(arr + 1, arr, 5);  /* 未定義動作 */
    memmove(arr + 1, arr, 5); /* OK */
    

    ---

    8.5 NULL ポインタと長さ 0

    多くの関数で未定義

    memcpy(dst, NULL, 0);  /* 未定義動作 */
    memcpy(NULL, src, 0);  /* 未定義動作 */
    strlen(NULL);          /* 未定義動作 */
    

    事前チェック

    if (n > 0 && src != NULL && dst != NULL) {
        memcpy(dst, src, n);
    }
    

    ---

    8.6 文字列関数

    終端の保証

    char buf[10];
    strncpy(buf, "hello world", sizeof(buf));
    /* buf は終端されない可能性! */
    
    buf[sizeof(buf) - 1] = '\0';  /* 明示的に終端 */
    

    snprintf の戻り値

    int ret = snprintf(buf, sizeof(buf), "value: %d", x);
    /* ret: 書き込まれるはずだった文字数(終端除く) */
    
    if (ret >= sizeof(buf)) {
        /* 切り捨てが発生 */
    }
    

    strlcpy / strlcat(非標準)

    /* BSD拡張、POSIX 2024で標準化予定 */
    size_t ret = strlcpy(dst, src, sizeof(dst));
    if (ret >= sizeof(dst)) {
        /* 切り捨て */
    }
    

    ---

    8.7 この章のまとめ

    標準ライブラリの注意点

  • errno: 呼び出し前にクリア、成功時は不定
  • シグナルハンドラ: async-signal-safe 関数のみ
  • restrict: オーバーラップ禁止の契約
  • NULL引数: 多くの関数で未定義動作

---

確認問題

問題1

memcpymemmove の違いは?

解答

memcpy はオーバーラップする領域では未定義動作。memmove はオーバーラップを正しく処理する。

問題2

シグナルハンドラ内で printf を呼ぶとどうなりますか?

解答

未定義動作。printf は async-signal-safe ではない。

---

次の章では、コンパイラ拡張と標準適合について学びます。