从下面的代码段中可以看出,我已经声明了一个char
变量和一个int
变量。编译代码时,必须识别变量str
和i
的数据类型。
为什么我需要在扫描变量时再次告诉它是字符串变量还是整数变量,方法是指定%s
或%d
到scanf
?当我声明我的变量时,编译器是否足够成熟以确定它?
#include <stdio.h>
int main ()
{
char str [80];
int i;
printf ("Enter your family name: ");
scanf ("%s",str);
printf ("Enter your age: ");
scanf ("%d",&i);
return 0;
}
答案 0 :(得分:119)
因为scanf
和printf
之类的变量参数函数没有可移植方式来知道变量参数的类型,所以甚至没有传递多少个参数。
参见C FAQ:How can I discover how many arguments a function was actually called with?
这就是必须至少有一个固定参数来确定变量参数的数量和类型的原因。而这个参数(标准称之为parmN
,参见C11(ISO/IEC 9899:201x)§7.16变量参数)扮演这个特殊角色,并将传递给宏{{1 }}。换句话说,你不能在标准C中使用这样的原型函数:
va_start
答案 1 :(得分:29)
编译器无法提供必要信息的原因很简单,因为此处不涉及编译器。函数的原型没有指定类型,因为这些函数具有变量类型。因此,实际数据类型不是在编译时确定的,而是在运行时确定的。 然后该函数从堆栈中取一个参数,接着另一个参数。这些值没有与之关联的任何类型信息,因此唯一的方法是,函数知道如何解释数据,使用调用者提供的信息,即格式字符串。
函数本身不知道传入了哪些数据类型,也不知道传递的参数数量,因此printf
无法自行决定这一点。
在C ++中,您可以使用运算符重载,但这是一种完全不同的机制。因为这里编译器根据数据类型和可用的重载函数选择适当的函数。
为了说明这一点,printf
在编译时如下所示:
push value1
...
push valueN
push format_string
call _printf
printf
的原型是这样的:
int printf ( const char * format, ... );
因此除了格式字符串中提供的内容之外,没有传递任何类型信息。
答案 2 :(得分:13)
编译器可能很聪明,但函数printf
或scanf
是愚蠢的 - 他们不知道你为每次调用传递的参数类型是什么。这就是您每次都需要传递%s
或%d
的原因。
答案 3 :(得分:13)
printf
不是intrinsic function。它不是C语言本身的一部分。所有编译器都会生成调用printf
的代码,传递任何参数。现在,因为C不提供reflection作为在运行时计算类型信息的机制,程序员必须明确提供所需的信息。
答案 4 :(得分:10)
第一个参数是格式字符串。如果您要打印十进制数字,它可能如下所示:
"%d"
(十进制数)"%5d"
(十进制数填充宽度为5,带空格)"%05d"
(十进制数用零填充到宽度5)"%+d"
(十进制数字,始终带有符号)"Value: %d\n"
(数字之前/之后的一些内容)等,请参阅例如Format placeholders on Wikipedia以了解字符串可以包含的格式。
此处可以有多个参数:
"%s - %d"
(字符串,然后是一些内容,然后是数字)
答案 5 :(得分:8)
当我声明我的时候,编译器是否足够成熟以识别它 变量?
没有
您使用的是几十年前指定的语言。不要指望C语言的现代设计美学,因为它不是现代语言。现代语言倾向于在编译,解释或执行方面交换少量效率,以提高可用性或清晰度。 C来自于计算机处理时间昂贵且供应量非常有限的时期,其设计反映了这一点。
这也是为什么当你真正关心快速,高效或接近金属时,C和C ++仍然是首选语言的原因。
答案 6 :(得分:4)
scanf
原型int scanf ( const char * format, ... );
表示存储根据参数格式将数据提供给附加参数指向的位置。
它与编译器无关,它与scanf
定义的语法有关。需要参数格式才能让scanf
知道要为输入数据保留的大小。
答案 7 :(得分:4)
GCC(可能还有其他C编译器)会跟踪参数类型,至少在某些情况下如此。但语言不是那样设计的。
printf
函数是一个接受变量参数的普通函数。变量参数需要某种运行时类型识别方案,但在C语言中,值不携带任何运行时类型信息。 (当然,C程序员可以使用结构或位操作技巧创建运行时键入方案,但这些没有集成到语言中。)
当我们开发这样的函数时:
void foo(int a, int b, ...);
我们可以在第二个之后传递“任意”数量的附加参数,并且我们需要使用某种在函数传递机制之外的协议来确定它们有多少以及它们的类型是什么。 / p>
例如,如果我们这样调用这个函数:
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 *
类型而不是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);
答案 8 :(得分:2)
这是因为这是告诉函数(如printf
scanf
)传递哪种类型值的唯一方法。例如 -
int main()
{
int i=22;
printf("%c",i);
return 0;
}
此代码将打印字符而非整数22.因为您已告知printf函数将变量视为char。
答案 9 :(得分:0)
printf
和scanf
是以接收控制字符串和参数列表的方式设计和定义的I / O函数。
函数不知道传递给它的参数类型,编译器也无法将此信息传递给它。
答案 10 :(得分:0)
因为在printf中你没有指定数据类型,所以你要指定数据格式。这是任何语言中的一个重要区别,它在C语言中具有双重意义。
当您使用%s
扫描字符串时,您不是说“为我的字符串变量解析字符串输入”。你不能在C中说,因为C没有字符串类型。 C对字符串变量最接近的是固定大小的字符数组,它恰好包含表示字符串的字符,字符串的结尾由空字符表示。所以你真正说的是“这是一个用来保存字符串的数组,我保证它对于我希望你解析的字符串输入足够大。”
原始?当然。 C是40多年前发明的,当时一台典型的机器最多只有64K的RAM。在这样的环境中,节省RAM的优先级高于复杂的字符串操作。
尽管如此,%s
扫描程序仍然存在于更高级的编程环境中,其中存在字符串数据类型。因为它是关于扫描而不是打字。