这里有一个经典的例子,代码看起来能够正常工作,但原因是错误的。
让我们回顾一下printf和scanf。格式说明符%d用于int类型的值。你可以这样读取整数:
int i;
scanf("%d", &i);
你可以像这样打印出来:
printf("%d\n", i);
为什么有的情况下要用 &
,而有的情况下不需要呢?这是因为C语言采用的是“按值传递”的方式。如果我们写成
scanf("%d", i); /* WRONG */
我们要把
i
的值传递给
scanf
函数,但我们不想传递(旧的)
i
的值给
scanf
,我们希望
scanf
读取一个新值,并将其存储到
i
中。换句话说,我们希望
scanf
实际上将
i
的新值传回给我们。为了实现这一点,我们需要将指向变量
i
的指针传递给
scanf
,以便它将刚刚读取的整数存储到
i
中。这就是
&
的作用——它生成指向
i
的指针。
另一方面,在调用 printf
时,常规的参数传递方式可以正常工作。我们确实希望将 i
的值传递给 printf
,以便它可以将其打印出来。如果我们调用:
printf("%d\n", &i); /* WRONG */
这是不可行的,因为printf
需要一个int
,而我们错误地给了它一个指向int
的指针。
所以现在我们知道,对于带有%d
的整数,printf
想要一个int
,而scanf
想要一个指向int
的指针。
让我们谈谈字符。格式%c
用于表示字符。我们可以使用scanf
读取一个字符:
char c;
scanf("%c", &c);
我们可以使用printf
来打印它:
printf("%c\n", c);
再次强调,模式完全相同。scanf需要一个指针,以便它可以填充值,因此我们传递&c。但是printf只需要值,所以我们传递普通的c。
现在我们来到字符串。C中的字符串是字符数组。此外,C中的字符串总是以特殊的空字符'\0'结尾,标志着字符串的结束。因此,如果我们想声明一个变量,可以包含最多9个字符的字符串,我们可以写
char s[10];
那么我们有9个字符的空间,再加上终止符
'\0'
。
但在C语言中,数组是特殊的:每当你将一个数组传递给函数,或者需要数组的“值”时,你得到的是指向数组第一个元素的指针(编译器自动生成的)。
这意味着使用
scanf
和
%s
读取字符串时,我们只需调用:
scanf("%s", s);
"但是{{&}}在哪里呢?" 你问道。 "我以为调用{{scanf}}时总是需要一个{{&}}!"
嗯,不完全正确。 调用{{scanf}}时您始终需要一个{{指针}}。 实际上,当您调用{{scanf(“%s”,s)}}时,就像您写的一样。
scanf("%s", &s[0]);
当你在使用
scanf
时,如果使用
%s
,它会期望一个指向多个字符的第一个指针,也就是一个指向字符数组开头的指针,它会从那里开始写入读取到的字符串。(那么它如何知道这个数组的大小呢?如果用户输入的字符串太长而无法适应该数组怎么办?我们马上就会讲到这些问题。)
当然,你也可以使用
%s
打印字符串,像这样:
printf("%s\n", s);
这就像你自己写的一样{{,再次强调}}。
printf("%s\n", &s[0]);
当你使用
printf
时,
%s
期望一个指向第一个要打印的字符的指针,直到找到终止符
'\0'
为止。
所以
%s
在
printf
和
scanf
中很特殊,因为字符串很特殊(因为数组很特殊)。对于
%d
、
%c
和几乎所有其他格式说明符,通常在调用
scanf
时需要
&
,而在调用
printf
时通常不需要
&
。但是对于
%s
,通常无论是在
printf
还是在
scanf
中都不需要
&
。
(如果我们仔细思考一下,异常并不是因为scanf和%s不需要&。记住,规则实际上是,scanf总是需要指针。scanf和%s不需要&的唯一原因是,当你传递一个数组时,你会自动得到一个指向数组第一个元素的指针。所以,异常实际上是针对printf和%s的:printf和%s确实需要一个指针,而printf和%s设计为需要一个指针的原因是没有办法不给它一个指针:对于字符串,你总是最终会给它一个指针。)
所以,%s的规则是,scanf期望一个指向几个字符中的第一个字符的指针,printf也期望一个指向几个字符中的第一个字符的指针。
现在,有了所有这些背景知识,我们可以看看你的代码。你基本上写了
char c;
scanf("%s", &c);
起初这似乎有点正确。
scanf
和
%s
需要一个字符指针,而你给了它
&c
,这是一个字符指针。 但是,
%s
实际上想要几个字符的指针,即第一个字符的指针。 但是你只给了它一个单独字符的指针。 因此,当用户输入字符串时,键入的第一个字符将存储在
c
中,但其余字符和终止符
'\0'
将被写入未分配的内存中,在变量
c
右侧某个地方。 它们将覆盖(“损坏”)可能用于其他用途的内存。 这是一个严重的问题,但可能不会立即显现出来。
最后,你尝试使用
printf
再次打印出内容。 你首先尝试了
printf("%s\n", c); /* WRONG */
但这完全行不通。原因是使用
printf
的
%s
需要一个指向
char
的指针,而你提供的只是一个普通的
char
。假设
c
包含字母
'A'
。这将导致
printf
去到地址65并开始打印字符,直到找到终止符
'\0'
。为什么是地址65?因为65是
A
的ASCII码。但在内存中,可能没有一个合适的以空字符结尾的字符串从地址65开始;实际上,你的程序很有可能根本没有读取地址65的权限。
然后你尝试了
printf("%s\n", &c); /* ALSO WRONG */
而且这样似乎可以工作。它之所以“可以工作”,是因为如果scanf成功将一个完整的字符串存储到c中并顺便存储在其右侧的未分配内存中,并且破坏该内存不会导致(太多)其他问题,那么当您将指针&c传递给printf时,printf可以找到这些字符,组成一个字符串,并将其打印出来。
所以它“可以工作”,但正如我所说的,原因是错误的:在此过程中,它践踏了它不“拥有”的内存,迟早会导致其他问题无法解决。
那么应该如何扫描和打印字符串?一种方法就像我们之前看到的那样:
char s[10];
scanf("%s", s);
printf("%s\n", s);
现在,当
scanf
获得指向数组
s
第一个元素的指针时,它有10个字符可以使用。
我们确实需要担心用户可能会输入超过9个字符的可能性。但是有一种解决方法:我们可以告诉
scanf
允许读取多长的字符串,它可以写入我们传递给它的数组的字符数。
scanf("%9s", s);
那个
9
告诉
scanf
,它不允许从用户读取超过9个字符。而由于9小于10,因此还有空间容纳终止符
'\0'
。
关于
scanf
还有很多需要注意的地方。正如chqrlie在评论中指出的那样,检查其返回值非常重要,以确保它成功转换了您想要的值。 它对空格有一些奇怪的规则。除非您知道自己在做什么,否则不能将
scanf
调用与其他输入读取函数(如
getchar
或
fgets
)混合使用--这会导致奇怪的结果。最后,
scanf
非常挑剔,并且(最终)缺乏真正有用的功能,因此根本不值得使用。但是,由于这个答案已经
tl;dr了,所以这些都是另一天的话题。
scanf()
函数,并使用%s
作为对应的转换说明符,这将导致调用未定义行为。 - Jonathan Leffler