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;
}
va_end(variable_args);
}
如果调用者忘记传递空指针终止符,则该行为将是不确定的,因为该函数va_arg
在使用完所有参数后将继续调用。我们的my_exec
函数必须这样调用:
my_exec("foo", "bar", "xyzzy", (char *) 0);
对演员0
是必需的,因为没有上下文它被解释为一个空指针常量:编译器不知道,对于这样的说法预期的类型为指针类型。此外,这(void *) 0
是不正确的,因为它将简单地作为void *
类型而不是类型进行传递char *
,尽管几乎可以肯定两者在二进制级别上是兼容的,所以它将在实践中起作用。此类exec
函数的一个常见错误是:
my_exec("foo", "bar", "xyzzy", NULL);
编译器NULL
恰好被定义为0
没有任何(void *)
强制转换的地方。
另一种可能的方案是要求调用者传递一个数字,该数字指示有多少个参数。当然,该数字可能不正确。
对于printf
,格式字符串描述参数列表。该函数对其进行解析并相应地提取参数。
如开头所述,某些编译器(尤其是GNU C编译器)可以在编译时解析格式字符串,并根据参数的数量和类型执行静态类型检查。
但是,请注意,格式字符串可以不是文字字符串,并且可以在运行时计算,这对于此类类型检查方案是不可渗透的。虚构的例子:
char *fmt_string = message_lookup(current_language, message_code);
snprintf(buffer, sizeof buffer, fmt_string, arg1, arg2, arg3);
%x
,%d
,%s
都是格式说明符“告诉”printf()
如何显示数据; 即是将位流显示为十六进制数字还是十进制整数或ASCII表示。数据就是数据就是数据。:-)程序员(使用printf)可以随意解释它。