这个问题来源于一道 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 1
返回值优化 RVO?
|
2
fcten 2019-01-01 17:29:46 +08:00 1
对象如果是静态局部变量和全局变量,其构造函数调用在执行 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 1
@fourstring 确实是 RTO 优化。s3 的指针被当作"参数"传给 operator+,在 return 的时候调用拷贝构造
|
7
allanzyne 2019-01-01 17:46:24 +08:00 1
写错了。。RVO
|
8
fourstring OP @allanzyne #7 非常感谢!不过我还有一个问题就是如果使用 Release 编译预设会进行优化可以理解,但 Debug 预设也会开启编译优化吗?
|
9
allanzyne 2019-01-01 17:55:24 +08:00 1
@fourstring 大概因为它是一个很基础的优化
|
10
fourstring OP @allanzyne #9 明白了,想问一下哪里可以查到还有什么类似于 RVO 这样“基础”的优化呢?(或者哪些书以及其他途径可以获取这样的信息?)
|
11
fcten 2019-01-01 18:09:58 +08:00 1
@fourstring 仔细瞅了一眼,刚才的回复有误,顺序应该是下面这样的。
构造 0 s1 构造函数 构造 1 s2 构造函数 拷贝构造 0 临时对象 1(i)构造函数 构造 1 临时对象 2(1)构造函数 构造 0 s3 构造函数 析构 1 临时对象 2(1)析构函数 析构 0 临时对象 1(i)析构函数 构造 2 临时对象 3(i)构造函数 析构 2 临时对象 3(i)析构函数 回车 析构 1 s2 析构函数 析构 0 s1 析构函数 析构 3 s3 析构函数 |
12
fcten 2019-01-01 18:17:01 +08:00 1
上面提到的 RVO,省略了临时变量 tmp 的构造和析构函数。
|
13
allanzyne 2019-01-01 18:19:12 +08:00 via iPhone 1
|
14
lrxiao 2019-01-02 02:26:45 +08:00 1
用 gcc 可以-fno-elide-constructors 看下
因为 copy-elision 允许违背 as-if |
15
wwqgtxx 2019-01-02 12:50:05 +08:00 1
有个简单的方法,可以在 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 1
你认为的知识点是所谓的抽象机语义。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 有什么关系,更别指望一定是地址了(即便常见实现确实会给你某种意义上的地址)。 |