“printf("%n %d", &a, a)”是否定义良好?

3
#include <stdio.h>

int main(void) {
    int i = 0;
    printf("abc %n %d", &i, i);
    printf("\n%d\n", i);
}

当我执行了这个命令后,得到了以下结果。
abc  0
4

我认为这个结果是期望的。

但是当我执行下一个命令时,得到了不同的结果。

int main(void) {
    int i; // not initialize
    printf("abc %n %d", &i, i);
    printf("\n%d\n", i);
}

产生的结果:

abc  1
4

我不知道为什么第一个printf()i的结果是1

更奇怪的是,我发现了更多异常行为:

int main(void) {
    int sdf;
    printf("abc %n %d", &sdf, sdf);
    printf("\n%d\n", sdf);
    int i;
    printf("abc %n %d", &i, i);
    printf("\n%d\n", i);
}

使用此输出:

abc  1
4
abc  Random_Value
4

第一个总是显示1,但其他则显示随机值(我认为这是垃圾值)。
我认为意图就是产生垃圾值,但我不明白为什么第一个输出结果不同。

2
你的编译器提示了哪些警告? - Andrew Henle
1
当打印一个未初始化的本地整数变量的值时,得到一个随机值正是我所期望的。 - Roberto Caboni
@alk 谢谢你。但我还不知道为什么。 - Kyung Kyu Lee
@RobertoCaboni 我也是这么想的。但第一个始终显示“1”,而不像上面那样是垃圾值。 - Kyung Kyu Lee
1
@KyungKyuLee 这是垃圾 1 - 0___________
5个回答

3
结果是很明确的。地址ii被传递给printf,然后printf将值分配给i。但由于在它之前传递了i,所以printf在调用之前将打印i的值。
后面的片段使用未初始化的变量,这个未确定的值将首先被打印出来。其行为后面将与片段1相同。
未初始化的变量将具有未确定的值。如果变量的类型具有陷阱表示(这里不是这种情况),则可能会产生UB。但由于执行环境相同,很可能会看到相同的值。如果在其他操作系统上运行它、在其他计算机上运行它或使用其他编译器,它们可能会有所不同。

1
在这种情况下,它实际上并不是自动UB,因为i的地址被取出了。只有在实现支持陷阱表示并且初始值恰好为一个时,才会出现UB。 - dbush
我找到了为什么。 我只是运行了这个。 #include <stdio.h> int main(void) { int i; printf("%d", i); } 结果是'1',而不是垃圾值。 这只是取决于编译器的行为吗? - Kyung Kyu Lee
@dbush 发现得真好。 - 0___________
@KyungKyuLee 了解有关未定义行为的更多信息。 - RobertS supports Monica Cellio
@KyungKyuLee,我已经解释了为什么你可能会看到相同的值。它们是“垃圾”。我无法更简单地解释了。 - 0___________
1
@P__J__ 你提到了"UB"这个缩写,但大多数初学者可能并不知道它的含义和作用。也许你应该更好地解释一下。 - RobertS supports Monica Cellio

3

存储在堆栈区域、具有auto存储类别的未初始化本地整数变量的值是未定义的。因此,打印它会得到随机值。

这就是为什么你的输出结果会出现随机值。

abc  1
4
abc  Random_Value
4

值为1同样也是垃圾值。

实际上,它是堆栈中的第一个位置,其值可能因系统和/或编译器而异。我的猜测是,它的值为1,因为它表示一个“幽灵argc”,即使没有参数定义函数,该函数也存在于主函数中,并且其值为1。

由于argc表示从命令行调用程序时使用的参数数量(至少为1:可执行文件名),因此有一种验证这个假设的方法:以这种方式调用你的程序。

executableName foo

这将使argc变为2,因此第一个printf显示的值也应该变为 2

出于好奇,我在我的机器上编译了您的第二个示例(使用W10 64位操作系统下的DevC++和gcc编译器)。我证实了两个陈述:

  1. 当发生未定义行为时,更改环境会导致输出的更改
  2. argc存在于堆栈中,并影响未初始化局部变量的初始值

执行uninitVars.exe后的输出:

abc
0
abc
1

在执行uninitVars.exe dog之后的输出结果。
abc
0
abc
2

执行 uninitVars.exe dog cat 后的输出结果为:
abc
0
abc
3

看来我的栈的前四个字节总是被设置为0(它们是返回值的位置吗?),而第二个字节实际上就是argc,即使在main()的原型中没有明确定义它。


相反,第二个打印的值显示不同,因为它在两个printf调用之后定义,并且它们的执行会在栈中写入多个字节(其中一些是地址,这解释了为什么该值总是不同,因为进程的地址是虚拟的并且总是不同的)。


我认为这就是问题发生的原因。但是主函数的第一个不带参数的堆栈始终为1,而不是超过2。我尝试了“executableName foo”,但它只打印出'1'。 - Kyung Kyu Lee
@KyungKyuLee 这否定了我的“聪明猜测”。但在将执行权交给主函数之前,它肯定是由操作系统写入的某些内容。我无法确定,抱歉。 - Roberto Caboni
@KyungKyuLee 看看我的修改。我在我的电脑上进行了测试,即使我没有复制你的行为,我还是发现了一些有趣的东西。如果你仍然想要“玩弄”堆栈,这将会很有用。(但在你的真实程序中,最好的答案仍然是:在读取变量值之前初始化变量!;) )。 - Roberto Caboni

2

如果所讨论的变量未初始化,则其行为在最好的情况下是不确定的,在最坏的情况下是未定义的。

本地变量isdf未被初始化,这意味着它们的值是不确定的。 正式定义在C标准第3.19节中,如下:

3.19.2

1 不确定的值

可以是未指定的值或陷阱表示形式

3.19.3

1 未指定的值

该相关类型的有效值,在这种情况下,国际标准对选择哪个值不做任何要求。

2 注意:未指定的值不能是陷阱表示。

3.19.4

1 陷阱表示

不需要表示对象类型的值的对象表示形式

这基本上意味着值是不可预测的。 事实上,仅仅读取不确定的值有时会导致未定义的行为。如果不确定的值恰好是如上所定义的陷阱表示形式,则可能会发生这种情况。

如果所讨论的变量从未取其地址,则也可能是未定义行为。 但在这种情况下并不适用,因为您确实已经获取了地址。 此行为在第6.3.2.1p2节中有记录:

除了作为sizeof运算符、_Alignof运算符、一元&运算符、++运算符、--运算符或.运算符的左操作数或赋值运算符的左操作数时,不具有数组类型的lvalue会被转换为存储在指定对象中的值(并且不再是lvalue);这被称为lvalue转换。如果lvalue具有限定类型,则该值具有lvalue类型的未限定版本;此外,如果lvalue具有原子类型,则该值具有lvalue类型的非原子版本;否则,该值具有lvalue的类型。如果lvalue具有不完整类型且没有数组类型,则行为是未定义的。如果lvalue指定具有自动存储期的对象(可以用register存储类声明,从未取其地址),而且该对象未初始化(未使用初始化程序声明且在使用之前未对其进行分配),则行为是未定义的。

因此,假设您的实现没有陷阱表示形式,则sdfi的值是未指定的,这意味着它们可以是任何值,包括0或1。例如,您得到sdfi的值为1和(一些随机值)。当我运行相同的代码时,我得到了这个:

abc  0
4
abc  0
4

如果我使用-O3编译,它会设置更高的优化级别,结果如下:

abc  1446280512
4
abc  0
4

如您所见,运行与您相同的代码读取未指定值可能在不同的机器上产生不同的结果,甚至在同一台机器上使用不同的编译器设置也可能如此。

我得到的值0或者你得到的值1并没有什么特殊之处。它们就像1446280512一样随机。


2
如果a的类型为int(同样地,signed int),并且没有被const限定,则可以定义printf(“%n %d”,& a,a)。在这种情况下,函数调用时a的值会被打印出来,而在函数返回后,a的值将变为0。也就是说,函数调用之前,参数已经被计算并作为值传递。在函数体中执行第一条语句之前,存在一个序列点,因此读取和写入a不会产生任何特殊问题。

[...] when I execute next one, I got a different result.

int main(void)
{
  int i; // not initialize
  printf("abc %n %d", &i, i);
  printf("\n%d\n", i);
}
abc  1
4

I don't know why the result of 'i' in the first printf() is 1.

没有人知道。既没有被初始化也没有被赋值的自动变量的值是不确定的。这里与标题问题相同:在printf的参数列表中,表达式i在任何printf部分执行之前被评估,因此printf稍后将为其分配一个值的事实并不影响它接收到的值。


关于“读取未初始化或赋值的本地变量的不确定值具有未定义行为”的问题:不,它并不是。如果该类型具有陷阱表示,则读取不确定值可能会因此导致陷阱。或者如果其地址尚未被获取,则由于C 6.3.2.1 2,读取自动未初始化对象具有未定义行为。但是,如果获取了地址,则仅读取不确定值本身并不具有未定义行为。 - Eric Postpischil
根据附录J.2,@EricPostpischil,是的,它确实如此:“在以下情况下行为未定义:[...]使用具有自动存储期限的对象的值时,其状态不确定。”然而,我承认附录J仅供参考,并且我倾向于认为它的声明可能是从6.3.2.1.2中得出的,但会有一些精度损失。 - John Bollinger
更新以解决未定义的声明,并使有关变量分类的措辞更加精确。 - John Bollinger
我测试过的所有编译器--clang、gcc、icc、msvc--都将J.2中的语句视为规范,并且似乎没有“具有自动存储期限”的限定符。我预计,如果您将此报告为错误,您会被告知,无论标准文本在多大程度上不支持此功能,该标准都存在缺陷。 - zwol
@zwol:你是如何测试编译器对标准语句的处理方式的? - Eric Postpischil
@EricPostpischil 有条件地读取不确定的值,并查看编译器是否认为该分支是不可达的。如果行为是完全未定义的,标准允许这种假设,而不是仅仅未指定的情况下。 - zwol

0
首先,您不能在同一行中打印%n的结果,i将保留其先前的值。关于随机值,当您未初始化isdf时,它们具有随机值(最有可能为0,但没有保证)。在C中,如果未初始化全局变量,则其值为0,但是局部变量将具有未初始化的值(随机)。

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