关于向量:c 和优化中缺少返回的不稳定行为

Erratic behaviour with missing return in c++ and optimizations

假设您在 c 中编写了一个函数,但心不在焉地忘记输入单词 return。在那种情况下会发生什么?我希望编译器会抱怨,或者一旦程序到达那个点,至少会引发分段错误。然而,实际发生的情况要糟糕得多:程序吐出垃圾。不仅如此,实际输出还取决于优化的程度!这是一些演示此问题的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
#include <vector>

using namespace std;

double max_1(double n1,
         double n2)
{
  if(n1>n2)
    n1;
  else
    n2;
}

int max_2(const int n1,
      const int n2)
{
  if(n1>n2)
    n1;
  else
    n2;
}

size_t max_length(const vector<int>& v1,
          const vector<int>& v2)
{
  if(v1.size()>v2.size())
    v1.size();
  else
    v2.size();
}

int main(void)
{
  cout << max_1(3,4) << endl;
  cout << max_1(4,3) << endl;

  cout << max_2(3,4) << endl;
  cout << max_2(4,3) << endl;

  cout << max_length(vector<int>(3,1),vector<int>(4,1)) << endl;
  cout << max_length(vector<int>(4,1),vector<int>(3,1)) << endl;

  return 0;
}

这是我在不同优化级别编译它时得到的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ rm ./a.out; g++ -O0 ./test.cpp && ./a.out
nan
nan
134525024
134525024
4
4
$ rm ./a.out; g++ -O1 ./test.cpp && ./a.out
0
0
0
0
0
0
$ rm ./a.out; g++ -O2 ./test.cpp && ./a.out
0
0
0
0
0
0
$ rm ./a.out; g++ -O3 ./test.cpp && ./a.out
0
0
0
0
0
0

现在假设您正在尝试调试函数 max_length。在生产模式下你会得到错误的答案,所以你在调试模式下重新编译,现在当你运行它时一切正常。

我知道有一些方法可以通过添加适当的警告标志 (-Wreturn-type) 来完全避免这种情况,但我仍然有两个问题

  • 为什么编译器甚至同意编译一个没有返回语句的函数?旧代码是否需要此功能?

  • 为什么输出取决于优化级别?


  • 这是丢弃值返回函数末尾的未定义行为,这在草案 C 标准部分 `6.6.31 的 return 语句中有所介绍:

    Flowing off the end of a function is equivalent to a return with no
    value; this results in undefined behavior in a value-returning
    function.

    编译器不需要发出诊断,我们可以从 1.4 实施合规性部分看到这一点:

    The set of diagnosable rules consists of all syntactic and semantic
    rules in this International Standard except for those rules containing
    an explicit notation that a€?no diagnostic is requireda€? or which are
    described as resulting in a€?undefined behavior.a€?

    尽管编译器通常会尝试捕获各种未定义的行为并产生警告,尽管通常您需要使用正确的标志集。对于 gccclang 我发现以下一组标志很有用:

    -Wall -Wextra -Wconversion -pedantic

    一般来说,我鼓励您使用 -Werror.

    将警告转化为错误

    编译器因在优化阶段利用未定义行为而臭名昭著,请参阅通过查找死代码查找未定义行为错误以获取一些很好的示例,包括在处理此代码时臭名昭著的 Linux 内核空指针检查删除:

    1
    2
    3
    struct foo *s = ...;
    int x = s->f;
    if (!s) return ERROR;

    gcc 推断由于 ss->f; 中被引用,并且由于取消引用空指针是未定义的行为,因此 s 不能为空,因此优化了下一行的 if (!s) 检查(复制从我的回答这里)。

    由于未定义的行为是不可预测的,因此在更激进的设置下,编译器在许多情况下会进行更激进的优化,其中许多可能没有太大的直观意义,但是,嘿,这是未定义的行为,所以无论如何你都不应该有任何期望。

    请注意,尽管在很多情况下编译器可以确定函数在一般情况下没有正确返回,但这是暂停问题。在运行时自动执行此操作会产生违反不为您不使用的理念付费的成本。虽然 gccclang 都实现了清理程序来检查诸如未定义行为之类的事情,例如使用 -fsanitize=undefined 标志将在运行时检查未定义行为。


    您可能想在此处查看此答案

    唯一的原因是编译器允许你没有 return 语句,因为可能有许多不同的执行路径,确保每个都以 return 退出在编译时可能会很棘手,所以编译器会处理它给你。

    要记住的事情:

    如果 main 结束时没有返回,它将总是返回 0。

    如果另一个函数没有返回就结束它总是返回eax寄存器中的最后一个值,通常是最后一条语句

    优化会更改汇编级别的代码。这就是为什么你会出现奇怪的行为,编译器正在为你"修复"你的代码,当执行事情时会改变你的代码,给出不同的最后一个值,从而返回值。

    希望这有帮助!