printf和格式说明符中的&符号用法

3
#include <stdio.h>
#include <stdlib.h>

int main() {
    char  a;
    printf("What? \t");

    scanf("%s", &a);

    printf("U have to %s", a);
    return 0;
}

每当我构建和运行此代码并在%s中输入一个值时,就会出现错误,调试程序停止工作并关闭。但是当我像这样使用&符号:
#include <stdio.h>
#include <stdlib.h>

int main() {
    char  a;
    printf("What? \t");

    scanf("%s", &a);

    printf("U have to %s", &a);
    return 0;
}

printf 中,我们可以不用像在 scanf 中使用 & 符号来引用变量。为什么会这样?这与使用的格式说明符有关,比如在使用 %c 或者 %d 时,不需要在 printf 中使用 & 符号。这种情况是否与数据类型有关,哪些格式说明符会出现这种情况呢?请注意,以上内容保留了 HTML 标签,但已经被翻译成中文。

4
%s 需要一个指向字符串的指针,而不是单个字符。 - Barmar
3
未定义行为并不一定会失败,也不一定会成功。这种行为是未定义的,任何事情都可能发生。将指向单个字符的指针传递给scanf()函数,并使用%s作为对应的转换说明符,这将导致调用未定义行为。 - Jonathan Leffler
3个回答

14
这里有一个经典的例子,代码看起来能够正常工作,但原因是错误的。
让我们回顾一下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'为止。
所以%sprintfscanf中很特殊,因为字符串很特殊(因为数组很特殊)。对于%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调用与其他输入读取函数(如getcharfgets)混合使用--这会导致奇怪的结果。最后,scanf非常挑剔,并且(最终)缺乏真正有用的功能,因此根本不值得使用。但是,由于这个答案已经tl;dr了,所以这些都是另一天的话题。

这是一门关于 scanf() 的相当广泛的课程... 但您可能还想教用户有关返回值和测试它的必要性。 - chqrlie
@chqrlie 说得好。答案已经太长了,但这值得一提。 - Steve Summit
很棒的答案!我认为值得明确地说一些关于未定义行为的简短内容:就在这个答案说“这似乎起作用”的地方,我会急忙补充说,触发未定义行为的程序可能在一个上下文中(似乎)工作良好,然后在另一个上下文中不起作用,即使程序本身完全相同;因此,你甚至可以拥有一个在调试器和IDE中“工作”的程序,但在命令行上运行时表现截然不同;新手程序员需要学好这个,并尽早学习。 - landru27

3
%s 格式说明符需要一个指向字符串的指针。在使用 scanf 时,它必须是一个 char 数组,有足够的字符空间容纳你输入的单词和表示字符串结尾的尾随空字节。在 printf() 中,它必须是一个以 null 结尾的 char 数组。
使用指向 char 变量的指针不起作用,因为它没有空间容纳空字节。通过在变量外写入内容,你会导致未定义的行为。
char word[100];
scanf("%s", word);
printf("%s\n", word);

您可以使用%c来读取和写入单个字符而不是多个字符的字符串。

char letter;
scanf("%c", &letter);
printf("%c\n", letter);

0
在语句 char a; 中,a 是一个字符变量,要扫描一个字符变量,需要使用 %c 格式说明符。
scanf("%s",a);/* %s expects base address of char buffer, not single char */
scanf(" %c",&a);/* this is correct */

如果您想使用%s进行扫描,则输入应该是像char buf[10]这样的字符缓冲区。例如:

char a[10];
scanf("%s",a);

在使用 %c 或 %d 时,printf 中不需要放置 &(和号)标记吗?无需提供地址&printf(),因为 printf() 的工作是打印而不是扫描。例如:

char input;
scanf("%c",&input);/* here you need &, As scanf() will store input char into
                      address you provided i.e &input */
printf("%c",input);/*here no need &input, bcz input char already stored,
                     printf will just print the char*/

如果你想打印地址,可以使用%p

printf("%p",a);/*a is char buffer */

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接