从下面的代码片段中可以看到,我声明了一个char变量和一个int变量。 编译代码时,它必须标识变量str和i的数据类型。
为什么在扫描变量期间需要通过将%s或%d指定为scanf来再次告诉它是字符串还是整数变量? 声明变量时,编译器还不够成熟,无法识别吗?
#include
int main ()
{
char str [80];
int i;
printf ("Enter your family name:");
scanf ("%s",str);
printf ("Enter your age:");
scanf ("%d",&i);
return 0;
}
%x,%d,%s都是"说明" printf()如何显示数据的格式说明符;即是将位流显示为十六进制数字还是十进制整数或ASCII表示形式。数据就是数据就是数据。 :-)程序员(使用printf)可以随意解释它。
请参见Yu Haos的答案...问题是printf和scanf是varargs函数,这意味着它们的参数没有静态类型。 C是弱类型的……函数没有可以检查的运行时类型信息。格式用于此目的。
我对格式说明符的回答如何?并且您认为@xvp先生建议使用scanf()不一定要使用stdio.h吗?
谁说格式始终是编译器可以分析的像"%s"这样的字符串文字?它可以是具有计算值的字符串。编译器无法在编译时分析它们。
编译器的成熟与否是否与语言功能有关,任何人都难以回答关于您的编译器的问题,而您还没有说是哪个。
尽管C语言的局限性以及您可以在运行时创建格式字符串的事实意味着对于printf,scanf和朋友,您将始终需要再次指定类型,如果格式字符串是经过硬编码的,则有些编译器至少会为您验证类型是否匹配:gcc.gnu.org/onlinedocs/gcc/
@TheCodeArtist如果这足以关闭程序员的C语言,我们还是不希望他们。
尽管这一点是正确的,但没有真正的原因使编译器看不到"%?"。 (其中%?是编译器扩展),然后使用typeof(next_arg_in_list)来猜测是什么?应该被替换为,并带有使用" 0x"之类的上下文来指定十六进制等的奖励。
在许多方面,这一评论之上的第二条评论可能是我见过的最糟糕的评论...
@technosaurus首先,该建议是一种提供运行时类型信息的方法,与OP完全不同,后者将完全消除format参数。其次,有一个真正的原因:C语言标准不支持任何此类东西。也许有一天会。
因为像scanf和printf这样的变量参数函数没有可移植的方式来知道变量参数的类型,甚至没有传递多少个参数。
请参见C常见问题解答:如何发现函数实际使用了多少个参数?
这就是为什么必须至少有一个固定的自变量来确定变量自变量的数量,也许是类型的原因。这个参数(标准称为parmN,请参见C11(ISO / IEC 9899:201x)§7.16变量参数)起着特殊的作用,并将被传递给宏va_start。换句话说,您无法在标准C语言中使用具有以下原型的函数:
void foo(...);
+1是这里唯一能识别出问题所在的答案...缺少可变参数的静态类型。
另外,如上面的注释中所述,在var类型和printf格式"类型"之间没有一一对应的关系。
@MarkHu是的,但这更像是结果,而不是原因。无论如何,请考虑一下,为什么printf的格式说明符使用%f输出double,float呢?
在我上面的评论之后添加的第二段是非常不幸的,完全是错误的。仅在没有添加其他语言功能的情况下,没有其他方法可以在基于堆栈的实现中指定参数的起始地址,才需要第一个参数。确定参数类型的其他方法有很多,包括全局变量,并且没有特殊的原因必须将类型信息放在第一个参数中……例如,在开始未知类型的参数之前,可以有5个已知类型的参数,并且您仍然必须对所有这些都使用va_arg。
@JimBalter我没有在任何地方说第一个论点,对吗?根据定义,parmN是...之前最右边的参数,它不一定是第一个参数。在标准C中是必需的。
@YuHao是的,我讨厌第一个参数,但是其余的都是正确的……需要parmN才能将地址??放入堆栈;它与如何确定类型无关,这取决于函数……而类型肯定不必从parmN派生。当然,它是标准C的"必需",因为它是标准的一部分...我在X3J11上,并且确切地知道为什么...这是现有实现中现有varargs机制的标准化,并由C编译器供应商认可谁主导了委员会。
@JimBalter我的意思是,即使您使用其他方法确定类型信息,标准C变量自变量函数也必须在语法上具有parmN自变量。
您写道:"这是原因……确定数量,也许是类型……"。那根本不是原因。您无法通过parmN判断参数的数量。例如,对于某些函数,最后一个参数为NULL,否则为结束指示符。
最后,第二段与问题无关……第一段对此进行了说明。
编译器无法提供必要信息的原因很简单,因为此处未涉及编译器。函数的原型未指定类型,因为这些函数具有变量类型。因此,实际的数据类型不是在编译时确定的,而是在运行时确定的。
然后,该函数从堆栈中获取一个参数,在另一个参数之后。这些值没有任何关联的类型信息,因此,函数唯一的方法就是通过使用调用方提供的信息(格式字符串)来知道如何解释数据。
函数本身不知道传入的数据类型,也不知道传递的参数数量,因此printf不能自行决定。
在C ++中,可以使用运算符重载,但这是一种完全不同的机制。因为在这里编译器会根据数据类型和可用的重载函数选择适当的函数。
为了说明这一点,printf在编译时如下所示:
push value1
...
push valueN
push format_string
call _printf
printf的原型是这样的:
int printf ( const char * format, ... );
因此,除了格式字符串中提供的内容之外,没有任何类型信息会遗留。
通常对机制进行很好的解释,然后再讲技术性的语言。
使用字符串作为格式的一个很好的用途是,该格式实际上可以来自各种来源:其未在源代码中进行硬编码。您甚至可以要求用户提供自己的格式字符串,或者将其与gettext一起使用以更改模式顺序。
@ Max-P,是的,那绝对是优势,但同时也是危险的填空游戏。 :)当然,无论如何,您都必须编写一些包装器,因为如果可以在命令行上提供格式字符串(可以这么说),则必须确保还提供了适当的参数。
push ... push call序列是实现printf调用的一种可能方式。实际的调用约定不是由C标准指定的(通常由ABI为平台指定)。例如,某些参数可以在寄存器中传递,并且可以以任何顺序传递。
@KeithThompson,我知道。这取决于实施和优化。但是,无论如何完成,都没有关联的类型信息,只有调用者指定的原始二进制值。
printf不是内部函数。它本身不是C语言的一部分。编译器所做的全部工作就是生成代码以调用printf,并传递任何参数。现在,由于C不提供反射作为在运行时找出类型信息的机制,因此程序员必须显式提供所需的信息。
@Kevin Panko感谢您的出色编辑!就编辑而言,我仍然是菜鸟。
对两位投票者:有帮助的解释...
+1表示printf不是语言功能或任何特殊功能,因此编译器没有义务对其进行优化
@Tarik如果printf不是C语言的一部分,那么它实际上在哪里实现?通过操作系统?
编译器可能很聪明,但是函数printf或scanf却很愚蠢-它们不知道您为每次调用传递的参数的类型是什么。这就是为什么您需要每次通过%s或%d的原因。
您如何才能使这些功能更智能? C不支持反射。
在C++中,您可以:cin >> counter之类的东西;或cout << str;按预期工作。但是在C中您不能这样做,这就是为什么发明了像%d这样的字符串格式的原因。
在C ++中,cin / cout的示例不合适。之所以起作用,是因为重载,因此编译器可以在编译时决定它将为调用提供哪个函数。这是与varargs完全不同的机制。对于用户来说,它看起来很相似,但从技术上讲并不是这样。
那是因为C ++支持运算符重载,这最终意味着您有一个用于<
我认为理论上编译器可以插入正确的%x。
从理论上讲,是的,但是字符串的格式是未知的。毕竟,您可以传入任意字符串。从理论上讲,在编译时可以完成的工作基本上是像printf一样解析格式字符串,并检查参数是否正确。但是,那么您将必须为所有标准功能提供此功能。但是话又说回来,您可以打印一个字符串作为指针或字符串。
即使这样也行不通:没有什么可以阻止某人尝试传递比int,double或char*更复杂的内容:例如,如果变量是某个怪异结构的指针。
@Timo从理论上讲,可以做任何事情,但这将改变C的底层性质以及它所具有的灵活性。不再是C ...但是为什么要打扰呢? C ++已经存在。
第一个参数是格式字符串。如果您要打印一个十进制数字,它可能看起来像:
"%d"(十进制数)
"%5d"(十进制数字用空格填充到宽度5)
"%05d"(十进制数字用零填充到宽度5)
"%+d"(十进制数,始终带有符号)
"Value: %d
"(数字前后的一些内容)
等等,请参阅例如Wikipedia上的Format占位符,以了解可以包含哪些格式字符串。
这里也可以有多个参数:
"%s - %d"(字符串,然后是一些内容,然后是数字)
Isn't the compiler matured enough to identify that when I declared my
variable?
没有。
您使用的是数十年前指定的语言。不要指望C提供现代设计美学,因为它不是现代语言。现代语言将倾向于在编译,解释或执行中牺牲少量效率,以提高可用性或清晰度。从计算机处理时间昂贵且供应极为有限的时代开始,它的设计便体现了这一点。
这也是为什么当您真正在乎快速,高效或接近金属时,C和C ++仍然是首选语言的原因。
那真是奇怪的评论。我从来没有主张过。
是的,很抱歉,我将其写到了错误的打开标签中。抱歉给你带来不便
GCC(可能还有其他C编译器)至少在某些情况下会跟踪参数类型。但是语言不是那样设计的。
printf函数是一个接受变量参数的普通函数。可变参数需要某种运行时类型识别方案,但是在C语言中,值不携带任何运行时类型信息。 (当然,C程序员可以使用结构或位操作技巧创建运行时键入方案,但这些未集成到语言中。)
当我们开发这样的函数时:
void foo(int a, int b, ...);
我们可以在第二个参数之后传递"任意"数量的附加参数,这取决于我们使用函数传递机制之外的某种协议来确定有多少个参数以及它们的类型。
例如,如果我们这样调用此函数:
foo(1, 2, 3.0);
foo(1, 2,"abc");
被呼叫者无法区分案件。参数传递区域中只有一些位,我们不知道它们是表示字符数据的指针还是浮点数。
交流此类信息的可能性很多。例如,在POSIX中,exec系列函数使用的变量参数具有相同的类型char *,并且使用空指针来指示列表的结尾:
#include
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);
scanf如原型int scanf ( const char * format, ... );所述,根据参数格式将给定数据存储到附加参数所指向的位置。
它与编译器无关,都与为scanf定义的语法有关。需要参数格式以使scanf知道要为输入的数据保留的大小。
这是因为这是告诉函数(例如printf scanf)要传递哪种类型的值的唯一方法。例如-
int main()
{
int i=22;
printf("%c",i);
return 0;
}
此代码将打印字符而不是整数22,因为您已告诉printf函数将变量视为char。
因为在printf中您没有指定数据类型,所以您在指定数据格式。这在任何语言中都是重要的区别,在C语言中则是双重重要的。
使用%s扫描字符串时,并不是说"为我的字符串变量解析字符串输入"。您不能在C中这么说,因为C没有字符串类型。 C与字符串变量最接近的是固定大小的字符数组,该数组恰巧包含表示字符串的字符,字符串的结尾由空字符表示。因此,您真正要说的是"这里有一个用于容纳字符串的数组,我保证它足够大,足以容纳要解析的字符串输入"。
原始?当然。 C是40多年前发明的,当时一台典型的机器最多具有64K RAM。在这样的环境中,保存RAM比复杂的字符串操作具有更高的优先级。
尽管如此,%s扫描程序仍然存在于更高级的编程环境中,在该环境中存在字符串数据类型。因为是关于扫描,而不是打字。
不,每个格式说明符都指定所需的参数类型。 %s需要类型为char*的参数(必须是指向字符串的指针)。 %d需要类型为int的参数。 %x和%o require和类型为unsigned int的参数。依此类推。 %s有点不同,它处理自变量指向的数据。哦,NULL不能指示字符串的结尾;多数民众赞成在一个空的指针常量。它由空字符\0表示。
首先,您误用了" requires"一词。这意味着如果提供错误的数据类型,则会出现错误。您会收到警告,仅此而已。 %s期望char *,但是如果您提供其他内容,您仍然可以编译。 //虽然您对NULL的看法是正确的,但是我会改变它。
我并不是说"需要"一词暗示需要进行诊断。实际上,该行为是不确定的,并且提供错误的类型是错误的。 (它不需要诊断,因为通常这是不可能的;格式字符串不必是字符串文字。)通过提供"%s"或"%d"格式,程序员(您)将指定您提供或int分别作为对应的参数。
"这意味着如果提供错误的数据类型,您将得到一个错误" –不,不是。这是错误的,因此,在大多数情况下,这是您的答案。
@JimBalter告诉我林错但不告诉我原因的评论不是很有用。
阅读C标准以了解有关程序和实现的要求。仅需要针对约束错误提供诊断程序,但是还有许多其他要求,例如,正如Keith正确指出的那样,%s需要类型为char*的参数。
当前的问题是"为什么C用这种方式做事?"这与标准无关-语言在存在标准之前就已经存在。如果您想了解一种语言的设计,则需要阅读其基础文档。在这种情况下,Kernighan&Richie是一个不错的起点。
printf和scanf是I / O函数,其设计和定义为接收控制字符串和参数列表的方式。
函数不知道传递给它的参数的类型,并且Compiler也无法将此信息传递给它。
-1据我所知,这并不能回答问题。