GCC (およびおそらく他の C コンパイラ) は、少なくともいくつかの状況では、引数の型を追跡します。しかし、言語はそのように設計されていません。
このprintf
関数は、可変引数を受け入れる通常の関数です。変数の引数には、ある種の実行時の型識別スキームが必要ですが、C 言語では、値は実行時の型情報を保持しません。(もちろん、C プログラマーは、構造体やビット操作のトリックを使用してランタイム型付けスキームを作成できますが、これらは言語に統合されていません。)
次のような関数を開発すると:
void foo(int a, int b, ...);
2 番目の引数の後に「任意の」数の追加の引数を渡すことができます。引数の数とその型を決定するのは、関数渡しメカニズムの外部にある何らかのプロトコルを使用することです。
たとえば、この関数を次のように呼び出すと:
foo(1, 2, 3.0);
foo(1, 2, "abc");
呼び出し先がケースを区別できる方法はありません。パラメータ受け渡し領域には数ビットしかなく、それらが文字データへのポインタを表しているのか、浮動小数点数を表しているのかわかりません。
この種の情報を伝達する可能性は数多くあります。たとえば、POSIX では、exec
関数のファミリはすべて同じ型を持つ可変引数を使用char *
し、null ポインターを使用してリストの終わりを示します。
#include <stdarg.h>
void my_exec(char *progname, ...)
{
va_list variable_args;
va_start (variable_args, progname);
for (;;) {
char *arg = va_arg(variable_args, char *);
if (arg == 0)
break;
/* process arg */
}
va_end(variable_args);
/*...*/
}
va_arg
呼び出し元がヌル ポインター ターミネーターを渡すのを忘れた場合、関数はすべての引数を消費した後も呼び出しを続けるため、動作は未定義になります。my_exec
関数は次のように呼び出す必要があります。
my_exec("foo", "bar", "xyzzy", (char *) 0);
0
null ポインター定数として解釈されるコンテキストがないため、 のキャストが必要です。コンパイラーは、その引数の意図した型がポインター型であることを認識していません。さらに、 ではなく type(void *) 0
として単純に渡されるため正しくありませんが、この 2 つはバイナリ レベルでほぼ確実に互換性があるため、実際には機能します。そのタイプの関数でよくある間違いは次のとおりです。void *
char *
exec
my_exec("foo", "bar", "xyzzy", NULL);
コンパイラはNULL
たまたまキャスト0
なしで定義されています。(void *)
別の考えられるスキームは、呼び出し元に、引数がいくつあるかを示す数値を渡すことを要求することです。もちろん、その数字は間違っている可能性があります。
の場合printf
、書式文字列は引数リストを記述します。関数はそれを解析し、それに応じて引数を抽出します。
冒頭で述べたように、一部のコンパイラ、特に GNU C コンパイラは、コンパイル時にフォーマット文字列を解析し、引数の数と型に対して静的な型チェックを実行できます。
ただし、フォーマット文字列はリテラル以外の場合があり、実行時に計算される場合があることに注意してください。これは、そのような型チェック スキームの影響を受けません。架空の例:
char *fmt_string = message_lookup(current_language, message_code);
/* no type checking from gcc in this case: fmt_string could have
four conversion specifiers, or ones not matching the types of
arg1, arg2, arg3, without generating any diagnostic. */
snprintf(buffer, sizeof buffer, fmt_string, arg1, arg2, arg3);