【问题标题】:In C, why is %s working without giving it a value?在 C 中,为什么 %s 不给它一个值就可以工作?
【发布时间】:2019-06-22 17:17:07
【问题描述】:

根据我的知识和一些线程like this,如果你想在 C 中打印字符串,你必须这样做:

printf("%s some text", value);

并且将显示该值而不是%s

我写了这段代码:

char password[] = "default";
printf("Enter name: \n");
scanf("%s", password);
printf("%s is your password", password); // All good - the print is as expected

但我注意到我可以在没有值部分的情况下做完全相同的事情,它仍然可以工作:

printf("%s is your password");

所以我的问题是为什么%s 占位符在我不给它的情况下得到一个值,它怎么知道给它什么值?

【问题讨论】:

  • 在我看来只是一个编译器问题,似乎指向垃圾
  • 如果格式说明符说格式字符串后面有一个参数,而没有这样的参数,则行为未定义。一种可能的结果可能是以您认为合理的方式行事。
  • 运气好。由于您没有提供所需的参数,因此它会拾取碰巧存在的任何杂散值。在这种情况下,它可能会从上一次调用中获取password 参数。但你不能依赖这个。

标签: c


【解决方案1】:

这是未定义的行为,任何事情都可能发生,包括看起来正确的事情。但是它是不正确的。 如果您使用正确的选项,您的编译器可能会告诉您问题。

标准说(强调的是我的):

7.21.6.1 fprintf 函数

  1. fprintf 函数将输出写入流指向的流, 受格式指向的字符串的控制,该格式指定如何 随后的参数被转换为输出。 如果有 格式的参数不足,则行为未定义。 如果 格式已用尽,而参数仍然存在,多余的参数 被评估(一如既往),但在其他方面被忽略。 fprintf 当遇到格式字符串的结尾时,函数返回。

【讨论】:

  • 以及为什么这可能恰好显示password 的正确值:函数scanfprintf 都采用一个常量字符串和password 作为参数。因此,如果来自scanfpassword 仍在printf 期望其第二个参数的空间中,printf 将碰巧找到要为%s 打印的“正确”值。
【解决方案2】:

printf() 函数使用 C 语言功能,可让您将可变数量的参数传递给函数。 (技术上称为“可变参数函数”-https://en.cppreference.com/w/c/variadic-我将简称为“可变参数”。)

当在 C 中调用函数时,函数的参数被压入堆栈 (*) - 但 varargs 功能的设计无法让被调用函数知道传入了多少参数。

printf() 函数执行时,它扫描格式字符串,%s 告诉它在变量参数列表的下一个位置查找字符串。由于列表中没有 更多参数,因此代码“离开数组的末尾”并抓取它在内存中看到的下一个内容。我怀疑正在发生的事情是内存中的下一个位置仍然具有您之前调用scanf 的地址password,并且由于该地址指向一个字符串,并且您告诉printf 打印一个字符串,您得到了幸运的是,它奏效了。

尝试在调用scanf("%s", password);printf("%s is your password"); 之间调用另一个函数(例如:printf("%s %s %s\n","X","Y","Z"),您几乎肯定会看到不同的行为。

免费建议:C 语言有很多尖角和未定义位,但是好的编译器(以及静态分析或“lint”工具)可以警告您很多常见错误。如果您打算使用 C 语言工作,请学习如何将编译器警告调到最大,了解所有错误和警告的含义(当它们发生时,而不是一次全部发生!)并强迫自己编写无需任何编译器即可编译的 C 代码警告。它会为您省去很多不必要的麻烦。

(*) 为简单起见在这里概括 - 有时参数可以在寄存器中传递,有时是内联的,等等等等。

【讨论】:

  • 这里的问题是,因为它确实工作,它很可能使用堆栈中的东西,而是使用基于寄存器的调用约定!
【解决方案3】:

所以,有很多帖子告诉你不应该这样做printf("%s is your password");,而你只是幸运。我从你的问题中猜想你有点知道这一点。但很少有人告诉你为什么你很幸运的可能原因。

要了解可能发生的情况,我们必须了解函数参数是如何传递的。函数的调用者必须将参数放在约定的位置,以便函数找到参数。所以对于参数 1...N,我们称这些地方为r1 ...rN。 (这种协议是我们称之为“函数调用约定”的一部分)

也就是说这段代码:

scanf("%s", password);
printf("%s is your password",password);

可能会被编译器转成这个伪代码

r1="%s";
r2=password;
call scanf;

r1="%s is your password";
r2=password;
call printf;

如果您现在从 printf 调用中删除第二个参数,您的伪代码将如下所示:

r1="%s";
r2=password;
call scanf;

r1="%s is your password";
call printf;

请注意,call scanf; 之后,r2 可能未修改,仍设置为 password,因此 call printf;“有效”

您可能认为您发现了一种优化代码的新方法,即消除了一个r2=password; 分配。这可能适用于旧的“愚蠢”编译器,但不适用于现代编译器。

现代编译器在安全的情况下已经这样做了。而且它并不总是安全的。不安全的原因可能是scanfprintf 有不同的调用约定,r2 可能在你背后被修改了等等。

为了更好地了解编译器在做什么,我建议查看编译器在不同优化级别的汇编器输出。

请始终使用-Wall 进行编译。编译器通常善于在你做蠢事时告诉你。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-05-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-12-28
    • 1970-01-01
    • 2014-01-17
    相关资源
    最近更新 更多