很抱歉这个问题的标题含糊不清,但我不确定如何准确地提出这个问题。
以下代码在 Arduino 微处理器(为 ATMega328 微处理器编译的 c++)上执行时运行良好。返回值显示在代码的注释中:
// Return the index of the first semicolon in a string
int detectSemicolon(const char* str) {
int i = 0;
Serial.print("i = ");
Serial.println(i); // prints "i = 0"
while (i <= strlen(str)) {
if (str[i] == ';') {
Serial.print("Found at i = ");
Serial.println(i); // prints "Found at i = 2"
return i;
}
i++;
}
Serial.println("Error"); // Does not execute
return -999;
}
void main() {
Serial.begin(250000);
Serial.println(detectSemicolon("TE;ST")); // Prints "2"
}
如预期的那样,这会输出“2”作为第一个分号的位置。
但是,如果我将 detectSemicolon
函数的第一行更改为 int i;
即没有显式初始化,我会遇到问题。具体来说,输出是“i = 0”(好)、“Found at i = 2”(好)、“-999”(坏!)。
因此,尽管在 return 2;
行之前立即执行了 print 语句,并且尽管从未在 return -999;
之前立即执行 print 语句,但该函数仍返回 -999 > 线。
有人可以帮助我了解这里发生的事情吗?我知道 c 函数中的变量理论上可以包含任何旧的垃圾,除非它们被初始化,但在这里我专门检查打印语句这还没有发生,但是......
编辑:感谢所有参与的人,特别是感谢 underscore_d 的出色回答。似乎未定义的行为确实导致编译器跳过任何涉及 i
的内容。下面是一些在 detectSemicolon 中注释掉了 serial.prints 的程序集:
void setup() {
Serial.begin(250000);
Serial.println(detectSemicolon("TE;ST")); // Prints "2"
d0: 4a e0 ldi r20, 0x0A ; 10
d2: 50 e0 ldi r21, 0x00 ; 0
d4: 69 e1 ldi r22, 0x19 ; 25
d6: 7c ef ldi r23, 0xFC ; 252
d8: 82 e2 ldi r24, 0x22 ; 34
da: 91 e0 ldi r25, 0x01 ; 1
dc: 0c 94 3d 03 jmp 0x67a ; 0x67a <_ZN5Print7printlnEii>
看起来编译器实际上完全无视 while 循环并断定输出将始终为“-999”,因此它甚至不去调用函数,而是硬编码 0xFC19。我将在启用 serial.prints 的情况下再看一眼,以便仍会调用该函数,但我认为这是一个强有力的指针。
编辑 2:
对于那些真正关心的人,这里有一个完全如上所示(在 UB 情况下)的反汇编代码的链接:
如果仔细观察,编译器似乎将寄存器 28 指定为 i
的位置,并在 d8
行将其“初始化”为零。在 while 循环、if 语句等中,该寄存器被视为包含 i
,这就是为什么代码看起来有效并且 print 语句按预期输出的原因(例如第 122 行,其中“i”得到递增)。
然而,当谈到返回这个伪变量时,这对我们久经考验的编译器来说太过分了;它画线,并将我们转储到另一个返回语句(第 120 行跳转到第 132 行,在返回 main()
之前将“-999”加载到寄存器 24 和 25 中)。
或者至少,这是我对汇编的有限掌握所能达到的程度。这个故事的寓意是当您的代码行为未定义时会发生奇怪的事情。
最佳答案
与所有非static
存储持续时间的基本类型一样,声明但不定义int
不会导致默认初始化。它使变量未初始化。那不意味着 i
只是保存一个随机值。它没有(已知的、有效的)值,因此您还不能阅读它。
这是来自 C++11 标准的相关引用,来自评论中的 Angew。这不是新的限制,从那以后也没有改变:
C++11 4.1/1, talking about an lvalue-to-rvalue conversion (basically reading a variable's value): "If the object to which the glvalue refers is ... uninitialized, a program that necessitates this conversion has undefined behavior."
任何对单元化变量的读取都会导致未定义的行为,因此任何事情都可能发生。编译器可以让它做任何事情,而不是你的程序使用一些未知的默认值继续按预期运行,因为行为是未定义的,并且标准对在这种情况下应该发生什么没有强加要求.
实际上,这通常意味着优化编译器可能会简单地删除任何以任何方式依赖 UB 的代码。没有办法就做什么做出正确的决定,所以决定什么都不做是完全有效的(这恰好也是对大小和速度的优化)。或者正如评论者所提到的,它可能会保留代码,但会用手头最近的不相关值或不同语句中的不同常量等替换读取 i
的尝试。
打印变量并不像您想象的那样算作“检查它”,所以这没有区别。没有办法“检查”一个未初始化的变量,从而使自己免受 UB 的侵害。读取变量的行为仅在程序已经写入特定值时才定义。
我们没有必要推测为什么会出现特定的任意类型的 UB:您只需要修复您的代码,使其确定性地运行。
你为什么要使用未初始化的呢?这只是“学术”吗?
关于c++ - 初始化 int 影响函数返回值,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38149969/