请教关于 C++中类的构造/析构的一些问题

2019-01-01 17:03:25 +08:00
 fourstring

这个问题来源于一道 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 的这一句执行顺序是这样的:

  1. 表达式 i+1 中 i 与 1 类型不匹配,由于 sample 的构造函数重载之一 sample(int val=0)没有 explicit 参数,并且 sample 类对+的重载实现要求+的操作数为两个 sample 对象,故编译器使用该构造函数重载将 1 转换为一个临时 sample 对象,这里输出“构造 1 ”
  2. 执行 i 与由 1 转换来的临时对象的加法。在加法函数中声明局部 sample 对象 tmp,输出“构造 0 ”。然后在加法重载函数返回时,由于返回值类项为 sample,因此新建一个临时 sample 对象并将 tmp 的值用于初始化这个临时 sample 对象,输出“拷贝构造 1 ”,此后,局部变量 tmp 被回收,输出“析构 1 ”。
  3. 加法重载函数将上一步中最后生成的临时对象返回到调用处,s3 使用该临时对象初始化,输出“拷贝构造 1 ”。
  4. 最后这一行代码中为了类型转换而生成的临时对象被销毁,输出“析构 1 ”。

我使用 Visual Studio 2017 编译,Debug x86 编译预设编译运行来检验我的设想,实际输出如下:

构造 1
构造 0
拷贝构造 1
析构 1
析构 1

实际输出和我的设想不同之处在于,输出中没有上述 3.的输出。但是问题在于,进行单步调试后,我观察到在这句代码执行完毕进入++s3时,s3 对象确实已经被创建了,那么 s3 对象是用什么样的方式创建的呢?因为无论如何要创建一个新对象一定要调用某个构造函数,但是我没有能得到任何 s3 构造时产生的输出。

请问 s3 对象是以怎样的方式被构造的呢?感激不尽!

3057 次点击
所在节点    C++
17 条回复
huaouo
2019-01-01 17:13:38 +08:00
返回值优化 RVO?
fcten
2019-01-01 17:29:46 +08:00
对象如果是静态局部变量和全局变量,其构造函数调用在执行 main 函之前,析构函数调用在 main 函数结束之后
fourstring
2019-01-01 17:34:18 +08:00
@fcten #2 谢谢您,我添加 s3 监视以后发现确实当执行流进入 foo 以后 s3 就已经存在了,但是问题在于我没有得到 s3 构造时的输出。我从监视窗口里看到 s3 是使用了默认构造函数,但是在“拷贝构造 0 ”后并没有“构造 0 ”的输出,这可能是什么原因呢?
fourstring
2019-01-01 17:35:30 +08:00
@fcten #2 另外我也尝试过删除 static 限定,依然没有得到 s3 的构造输出。而我把 s3=i+1 改为 s3=i 后,就得到了调用拷贝构造函数的输出。
fourstring
2019-01-01 17:38:27 +08:00
@huaouo #1 去查了一下这个优化的概念,但是我觉得应该不是这个优化导致的。我开启单步调试后,观察到“拷贝构造 0 ”这一句输出是在加法重载函数 return 时产生的,也就是说将 tmp 的值用于了初始化临时 sample 对象。
allanzyne
2019-01-01 17:45:12 +08:00
@fourstring 确实是 RTO 优化。s3 的指针被当作"参数"传给 operator+,在 return 的时候调用拷贝构造
allanzyne
2019-01-01 17:46:24 +08:00
写错了。。RVO
fourstring
2019-01-01 17:50:30 +08:00
@allanzyne #7 非常感谢!不过我还有一个问题就是如果使用 Release 编译预设会进行优化可以理解,但 Debug 预设也会开启编译优化吗?
allanzyne
2019-01-01 17:55:24 +08:00
@fourstring 大概因为它是一个很基础的优化
fourstring
2019-01-01 18:01:42 +08:00
@allanzyne #9 明白了,想问一下哪里可以查到还有什么类似于 RVO 这样“基础”的优化呢?(或者哪些书以及其他途径可以获取这样的信息?)
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)构造函数
析构 2 临时对象 3(i)析构函数
回车
析构 1 s2 析构函数
析构 0 s1 析构函数
析构 3 s3 析构函数
fcten
2019-01-01 18:17:01 +08:00
上面提到的 RVO,省略了临时变量 tmp 的构造和析构函数。
allanzyne
2019-01-01 18:19:12 +08:00
lrxiao
2019-01-02 02:26:45 +08:00
用 gcc 可以-fno-elide-constructors 看下
因为 copy-elision 允许违背 as-if
wwqgtxx
2019-01-02 12:50:05 +08:00
有个简单的方法,可以在 operator+和 foo 中分别打印一下 tmp 和 s3 的地址就会发现两个的地址是完全一样的
Nasei
2019-01-02 13:01:15 +08:00
@fcten c 和 cpp 的局部静态变量应该不太一样,cpp 的并不是 main 之前初始化的


@fourstring 允许的优化参见 cppreference 中的 copy elision (复制消除) 里的注意部分
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 有什么关系,更别指望一定是地址了(即便常见实现确实会给你某种意义上的地址)。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/522892

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX