
这个问题来源于一道 C++考试题,要求阅读代码写出输出,代码如下:
#include <iostream> #include <iomanip> using namespace std; class sample { private: int x; public: sample(int val = 0) { x= val; cout << "构造" << x << endl; } sample(const sample &obj) { x = obj.x; cout << "拷贝构造" << x << endl; } ~sample() { cout << "析构" << x << endl; } void operator++() { x++; } friend sample operator+(const sample &a, const sample &b) { sample tmp; tmp.x = a.x + b.x; return tmp; } }; void foo(sample i); int main() { sample s1, s2(1); foo(s1); foo(2); cin.get(); return 0; } void foo(sample i) { static sample s3 = i + 1; ++s3; } 问题主要集中在foo函数中
static sample s3 = i + 1; 这一行。当执行到foo(s1)时,我认为函数中关于 s3 的这一句执行顺序是这样的:
我使用 Visual Studio 2017 编译,Debug x86 编译预设编译运行来检验我的设想,实际输出如下:
构造 1 构造 0 拷贝构造 1 析构 1 析构 1 实际输出和我的设想不同之处在于,输出中没有上述 3.的输出。但是问题在于,进行单步调试后,我观察到在这句代码执行完毕进入++s3时,s3 对象确实已经被创建了,那么 s3 对象是用什么样的方式创建的呢?因为无论如何要创建一个新对象一定要调用某个构造函数,但是我没有能得到任何 s3 构造时产生的输出。
请问 s3 对象是以怎样的方式被构造的呢?感激不尽!
1 huaouo 2019-01-01 17:13:38 +08:00 via Android 返回值优化 RVO? |
2 fcten 2019-01-01 17:29:46 +08:00 对象如果是静态局部变量和全局变量,其构造函数调用在执行 main 函之前,析构函数调用在 main 函数结束之后 |
3 fourstring OP @fcten #2 谢谢您,我添加 s3 监视以后发现确实当执行流进入 foo 以后 s3 就已经存在了,但是问题在于我没有得到 s3 构造时的输出。我从监视窗口里看到 s3 是使用了默认构造函数,但是在“拷贝构造 0 ”后并没有“构造 0 ”的输出,这可能是什么原因呢? |
4 fourstring OP @fcten #2 另外我也尝试过删除 static 限定,依然没有得到 s3 的构造输出。而我把 s3=i+1 改为 s3=i 后,就得到了调用拷贝构造函数的输出。 |
5 fourstring OP @huaouo #1 去查了一下这个优化的概念,但是我觉得应该不是这个优化导致的。我开启单步调试后,观察到“拷贝构造 0 ”这一句输出是在加法重载函数 return 时产生的,也就是说将 tmp 的值用于了初始化临时 sample 对象。 |
6 allanzyne 2019-01-01 17:45:12 +08:00 @fourstring 确实是 RTO 优化。s3 的指针被当作"参数"传给 operator+,在 return 的时候调用拷贝构造 |
7 allanzyne 2019-01-01 17:46:24 +08:00 写错了。。RVO |
8 fourstring OP @allanzyne #7 非常感谢!不过我还有一个问题就是如果使用 Release 编译预设会进行优化可以理解,但 Debug 预设也会开启编译优化吗? |
9 allanzyne 2019-01-01 17:55:24 +08:00 @fourstring 大概因为它是一个很基础的优化 |
10 fourstring OP @allanzyne #9 明白了,想问一下哪里可以查到还有什么类似于 RVO 这样“基础”的优化呢?(或者哪些书以及其他途径可以获取这样的信息?) |
11 fcten 2019-01-01 18:09:58 +08:00 @fourstring 仔细瞅了一眼,刚才的回复有误,顺序应该是下面这样的。 构造 0 s1 构造函数 构造 1 s2 构造函数 拷贝构造 0 临时对象 1(i)构造函数 构造 1 临时对象 2(1)构造函数 构造 0 s3 构造函数 析构 1 临时对象 2(1)析构函数 析构 0 临时对象 1(i)析构函数 构造 2 临时对象 3(i)构造函数br />析构 2 临时对象 3(i)析构函数 回车 析构 1 s2 析构函数 析构 0 s1 析构函数 析构 3 s3 析构函数 |
12 fcten 2019-01-01 18:17:01 +08:00 上面提到的 RVO,省略了临时变量 tmp 的构造和析构函数。 |
13 allanzyne 2019-01-01 18:19:12 +08:00 via iPhone |
14 lrxiao 2019-01-02 02:26:45 +08:00 用 gcc 可以-fno-elide-constructors 看下 因为 copy-elision 允许违背 as-if |
15 wwqgtxx 2019-01-02 12:50:05 +08:00 有个简单的方法,可以在 operator+和 foo 中分别打印一下 tmp 和 s3 的地址就会发现两个的地址是完全一样的 |
16 Nasei 2019-01-02 13:01:15 +08:00 via Android @fcten c 和 cpp 的局部静态变量应该不太一样,cpp 的并不是 main 之前初始化的 @fourstring 允许的优化参见 cppreference 中的 copy elision (复制消除) 里的注意部分 |
17 FrankHB 2019-01-10 20:58:11 +08:00 你认为的知识点是所谓的抽象机语义。C++的实现被允许按 as-if rule 做任何保留抽象机语义等价性的变换。但除此之外,还有一些特例允许改变抽象机语义,例如: 12.8/31 When certain criteria are met, ... (算了懒得背了,反正现在也不是 12.8 了。)具体上面都提了,搜 copy elision,具体实现里可能叫 RVO。 C++14 之前这应该是唯一的 as-if 例外。C++14 还有 global new merging。 @wwqgtxx 这个就看脸吧,不管是<<还是%p 都是 impl-def,都不保证跟 object identity 有什么关系,更别指望一定是地址了(即便常见实现确实会给你某种意义上的地址)。 |