0Pricing
C Academy · レッスン

型安全性に関する注意点

可変長引数のリスクです。

「型安全性に関する注意点」はCoddyKit上の無料C Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはC Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 C Academyコースには全4レッスンが含まれています。

可変個引数では型チェックが行われない

通常の C 関数では、引数がパラメーターの型と一致するかチェックされます。可変個引数では、このチェックが完全に省略されます。

コンパイラーは ... の後に何を渡したか検証できないため、間違いが起きやすく危険です。

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

int add(int n, ...) {
    va_list a; va_start(a, n);
    int s = 0;
    for (int i = 0; i < n; i++) s += va_arg(a, int);
    va_end(a);
    return s;
}

int main(void) {
    printf("%d\n", add(2, 3, 4));
    return 0;
}

va_arg で間違った型を使う

間違った型で引数を読み取ると、結果は未定義動作になります。

int を渡して double として読み取ると、無関係なバイト列として再解釈され、意味のない値になったりクラッシュしたりします。

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

int read_int(int n, ...) {
    va_list a; va_start(a, n);
    int v = va_arg(a, int);
    va_end(a);
    return v;
}

int main(void) {
    printf("%d\n", read_int(1, 100));
    return 0;
}

フォーマット文字列の不一致

printf スタイルの関数では、間違った指定子が典型的なバグになります。printf("%d", 3.14) は double を int として読み取ります。

最新のコンパイラーはリテラルの不一致について警告できますが、実行時に組み立てられるフォーマット文字列までは検出できません。

#include <stdio.h>

int main(void) {
    double pi = 3.14;
    printf("%f\n", pi);
    return 0;
}

整数昇格の意外な挙動

可変個引数部分では、小さい型は int に昇格します。そのため、char を渡した場合でも、読み取るときは int にする必要があります。

va_arg(a, char) のように char として読み取ると未定義動作になります。

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

void show(int n, ...) {
    va_list a; va_start(a, n);
    int c = va_arg(a, int);
    printf("char is %c\n", c);
    va_end(a);
}

int main(void) {
    show(1, 'A');
    return 0;
}

float は double になる

可変個引数では、float 引数は必ず double に昇格します。double として読み取る必要があり、float として読み取ってはいけません。

これは可変個引数で最もよくあるエラーの1つです。

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

void show(int n, ...) {
    va_list a; va_start(a, n);
    double d = va_arg(a, double);
    printf("%.2f\n", d);
    va_end(a);
}

int main(void) {
    float f = 1.5f;
    show(1, f);
    return 0;
}

引数を読み取りすぎる

呼び出し側が渡した引数の個数を超えて va_arg を呼び出すと、スタックから不正なデータを読み取ることになります。

正確な個数または番兵が不可欠なのはこのためです。

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

int sum(int n, ...) {
    va_list a; va_start(a, n);
    int s = 0;
    for (int i = 0; i < n; i++) s += va_arg(a, int);
    va_end(a);
    return s;
}

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

NULL ポインターの番兵には注意が必要

番兵として NULL を使うのは危険です。一部のプラットフォームでは NULL が int としての 0 であり、ポインターとはサイズが異なるためです。

(char *)NULL のように明示的にキャストして、正しい型が渡されるようにしてください。

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

void print_all(const char *first, ...) {
    va_list a; va_start(a, first);
    const char *s = first;
    while (s) { printf("%s\n", s); s = va_arg(a, const char *); }
    va_end(a);
}

int main(void) {
    print_all("a", "b", (char *)NULL);
    return 0;
}

コンパイラーのフォーマット属性

GCC と Clang には、関数が printf スタイルのフォーマットを使うことをコンパイラーに伝える __attribute__((format(printf, 1, 2))) が用意されています。

これにより、コンパイラーはコンパイル時にフォーマット文字列をチェックし、多くのバグを検出できます。

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

void logf(const char *fmt, ...)
    __attribute__((format(printf, 1, 2)));

void logf(const char *fmt, ...) {
    va_list a; va_start(a, fmt);
    vprintf(fmt, a);
    va_end(a);
}

int main(void) {
    logf("%d items\n", 3);
    return 0;
}

より安全な代替手段を優先する

可変個引数はエラーが起きやすいため、可能な場合は型安全な設計を優先してください。明示的な配列、構造体、または固定個数のパラメーターを渡す方法があります。

可変個引数は、本当に printf のような機能が必要な場合に限って使いましょう。

#include <stdio.h>

int sum_array(const int *a, int n) {
    int s = 0;
    for (int i = 0; i < n; i++) s += a[i];
    return s;
}

int main(void) {
    int nums[] = {1, 2, 3, 4};
    printf("%d\n", sum_array(nums, 4));
    return 0;
}

契約をドキュメント化する

コンパイラーは可変個引数の型を強制できないため、明確なドキュメントが安全策になります。

呼び出し側が渡すべき型と引数の個数、そしてリストの終端方法を正確に記載してください。

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

/* sum_ints(n, ...): pass exactly n int values */
int sum_ints(int n, ...) {
    va_list a; va_start(a, n);
    int s = 0;
    for (int i = 0; i < n; i++) s += va_arg(a, int);
    va_end(a);
    return s;
}

int main(void) {
    printf("%d\n", sum_ints(2, 5, 5));
    return 0;
}

十分にテストする

バグは気付かないうちに潜むため、可変個引数関数をさまざまな引数の個数と型でテストしてください。

引数がゼロ個の場合や最大個数の場合などの境界値を試すと、間違いが見つかることがよくあります。

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

int sum(int n, ...) {
    va_list a; va_start(a, n);
    int s = 0;
    for (int i = 0; i < n; i++) s += va_arg(a, int);
    va_end(a);
    return s;
}

int main(void) {
    printf("%d\n", sum(0));
    printf("%d\n", sum(1, 9));
    return 0;
}

理解度チェック

可変個引数の型に関するリスクについての理解度を確認しましょう。

まとめ

可変個引数関数の危険性について学びました。

  • コンパイラーは可変個引数を型チェックしません。
  • 間違った型、個数、番兵は未定義動作を引き起こします。
  • 昇格を忘れないでください。小さい整数は int に、float は double になります。
  • コンパイラーのフォーマット属性を使い、契約をドキュメント化し、可能な場合は型安全な代替手段を選んでください。

よくある質問

「型安全性に関する注意点」レッスンは無料ですか?

はい。「型安全性に関する注意点」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、C Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 C Academyコースには全4レッスンが含まれています。

「型安全性に関する注意点」で何を学びますか?

可変長引数のリスクです。 ブラウザで直接実行するハンズオンコードでC Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

C Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのC Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「型安全性に関する注意点」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このC Academyレッスンでコードを書いて実行できますか?

はい。すべてのC Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. stdarg マクロ
  2. printf 風関数の記述
  3. 型安全性に関する注意点
  4. 実践例
← C Academyに戻る