【问题标题】:Initialising int affects function return value初始化 int 影响函数返回值
【发布时间】:2016-07-01 16:19:30
【问题描述】:

很抱歉这个问题的标题含糊不清,但我不知道如何准确地问这个问题。

以下代码在 Arduino 微处理器(为 ATMega328 微处理器编译的 c++)上执行时可以正常工作。返回值在代码中以 cmets 显示:

// 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 情况下):

https://justpaste.it/vwu8

如果您仔细观察,编译器似乎将寄存器 28 指定为i 的位置,并在d8 行中将其“初始化”为零。该寄存器被视为在 while 循环、if 语句等中始终包含i,这就是代码似乎可以工作并且打印语句按预期输出的原因(例如,第 122 行,“i”被递增)。

然而,当涉及到返回这个伪变量时,这对于我们久经考验的编译器来说太过分了;它画线,并将我们转储到另一个 return 语句(第 120 行跳转到第 132 行,将“-999”加载到寄存器 24 和 25,然后返回到main())。

或者至少,这是我对装配的有限掌握所能做到的。故事的寓意是,当您的代码行为未定义时,就会发生奇怪的事情。

【问题讨论】:

  • 一个函数只有一个返回点是一种很好的做法。我建议重构代码。同样正如其他人指出的那样,永远不要使用未初始化的变量。如果您确定它会在使用之前在其他地方初始化,则不必在声明的位置对其进行初始化。
  • @RealtimeRik 我不认为这是一个普遍接受的观点。我发现提前返回代码比人工拉伸到单个返回点的代码更具可读性。
  • @RealtimeRik 请务必将我们链接到参考资料,或者至少总结一下您看到多个return 点被使用的常见情况以及您将采取的措施。 “良好实践”的含义是它是普遍接受的经验法则,但我不记得阅读过任何与此相关的具体建议。
  • @CharlieB 绝对!我最终会把那台 Mega2560 从柜子里拿出来,当我这样做的时候……它会很光荣。另外,近年来我在 Z80 和 68000 领域进行了一些非常有趣的尝试,需要想另一个借口回去。 :D
  • @underscore_d 已上传完整版供您欣赏。

标签: c++ arduino embedded


【解决方案1】:

与所有非static 存储持续时间的基本类型一样,声明但不定义int 不会导致默认初始化。它使变量未初始化。这意味着i 只是持有一个随机值。它拥有 no(已知、有效)值,因此您还不能读取它。

这是来自 C++11 标准的相关引用,来自 cmets 中的 Angew。这不是一个新的限制,从那时起也没有改变:

C++11 4.1/1,谈论左值到右值的转换(基本上是读取变量的值):“如果泛左值所指的对象是......未初始化,则需要这种转换的程序有未定义的行为。”

任何对统一变量的读取都会导致未定义的行为,因此任何事情都可能发生。与您的程序使用一些未知的默认值继续按预期运行不同,编译器可以让它做任何事情,因为行为是未定义的,并且标准对在这种情况下应该发生的事情没有任何要求.

实际上,这通常意味着优化编译器可能会简单地删除以任何方式依赖于 UB 的任何代码。没有办法对做什么做出正确的决定,所以决定什么都不做是完全有效的(这恰好也是对大小和速度的优化)。或者正如评论者所提到的,它可能会保留代码,但用手头最接近的不相关值替换读取 i 的尝试,或者在不同的语句中使用不同的常量,等等。

打印一个变量并不像你想象的那样算作“检查它”,所以这没有区别。没有办法“检查”一个未初始化的变量,从而为自己接种 UB。读取变量的行为只有在程序已经写入特定值的情况下才被定义。

我们没有必要推测为什么会出现特定的任意类型的 UB:您只需要修复您的代码,使其能够确定地运行。

你为什么要在未初始化的情况下使用它?这只是“学术”吗?

【讨论】:

  • 发现错字后,我现在已经修复了我的代码,这个问题更像是一个事后分析,以帮助我更好地了解编译器的底层发生了什么。我的理解是 int 占用了一定的空间。如果不初始化它,它将包含随机垃圾,但这仍然应该代表一些整数。这并不能解释我所看到的奇怪行为
  • @CharlieB 查看我的编辑。 UB 准确地解释了这里发生了什么。
  • @CharlieB 它还没有“解决”了 UB——UB 意味着代码的执行没有行为保证。如果您想更准确地了解到底发生了什么,请发布已编译代码的反汇编:)
  • @CharlieB 仅仅因为您看到 0 被打印出来,并不排除传递给 println 的一些垃圾数据恰好被解释为 0 以供显示的可能性。
  • @CharlieB 同样,我坚持认为“我们没有必要推测为什么会出现特定的任意类型的 UB”。但是,如果您希望通过这种确切的风向在您的确切编译器上了解这个确切的实例... oldrinb 是正确的。我还建议考虑一下 James 刚才提到的想法以及 Angew 对另一个答案的有趣评论,因为尽管 anything 可能再次发生,现实通常稍微更容易预测。
【解决方案2】:

当你不初始化变量时,它有一个随机值,无论内存地址是什么,所以while (i &lt;=strlen(str)) 的行为将无法预测。 您应该始终初始化。

(Visual Studio Debug 配置自动初始化变量。)

【讨论】:

  • 干杯马特,我意识到我需要初始化变量,这确实是一个错字。但是,出于兴趣,我试图了解这里发生了什么。当然,无论i中的垃圾是什么,比较都应该总是返回true或false,while循环也会相应地表现出来?
  • @CharlieB 不。编译器完全有权说“我可以看到 i 从未初始化,所以我可以假设它具有我认为最方便的任何值。”例如在将i 传递给函数时不更改寄存器,并假设所有涉及i 的条件都是错误的。 (注意:我不知道编译器是否真的这样做,但它会解释你所看到的行为)。
  • 它没有随机值。它调用未定义的行为。两者根本不同,没有义务产生相似的结果。想要随机数的人使用标准库的随机数生成器类
  • 另外,“(Visual Studio Debug 配置自动初始化变量。)” - 什么,好像用户已经默认初始化了它们?这将以某种方式改变代码行为,使调试用户产生一种虚假的安全感,然后在他们切换到发布模式并打开优化器时将它们扔给狼群,从而赋予 UB 双重任务。我确定是误会了?
  • @underscore_d 不,你没有误会。这就是 VS 调试构建的默认行为。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-07-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-02-16
  • 1970-01-01
相关资源
最近更新 更多