GCC(可能还有其他 C 编译器)至少在某些情况下会跟踪参数类型。但是语言不是这样设计的。
该printf
函数是一个接受可变参数的普通函数。变量参数需要某种运行时类型标识方案,但在 C 语言中,值不携带任何运行时类型信息。(当然,C 程序员可以使用结构或位操作技巧创建运行时键入方案,但这些并没有集成到语言中。)
当我们开发这样的功能时:
void foo(int a, int b, ...);
我们可以在第二个参数之后传递“任意”数量的附加参数,我们可以使用函数传递机制之外的某种协议来确定有多少以及它们的类型。
例如,如果我们这样调用这个函数:
foo(1, 2, 3.0);
foo(1, 2, "abc");
被调用者无法区分这些情况。参数传递区域中只有一些位,我们不知道它们是表示指向字符数据的指针还是浮点数。
传达此类信息的可能性很多。例如,在 POSIX 中,exec
函数族使用具有相同类型的变量参数char *
,并且使用空指针来指示列表的结尾:
#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
转换是必需的,因为没有上下文可以将其解释为空指针常量:编译器不知道该参数的预期类型是指针类型。此外(void *) 0
是不正确的,因为它只是作为void *
type 而不是传递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);