V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
fourstring
V2EX  ›  C++

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

  •  
  •   fourstring · 2019-01-01 17:03:25 +08:00 · 3315 次点击
    这是一个创建于 2183 天前的主题,其中的信息可能已经有所发展或是发生改变。

    这个问题来源于一道 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 对象是以怎样的方式被构造的呢?感激不尽!

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


    @fourstring 允许的优化参见 cppreference 中的 copy elision (复制消除) 里的注意部分
    FrankHB
        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 有什么关系,更别指望一定是地址了(即便常见实现确实会给你某种意义上的地址)。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1176 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 18:14 · PVG 02:14 · LAX 10:14 · JFK 13:14
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.