【C++学习总结2-5】类和对象(封装)—— 返回值优化(RVO)

【C++学习总结2-5】类和对象(封装)—— 返回值优化(RVO),第1张

【C++学习总结2-5】类和对象(封装)—— 返回值优化(RVO) 1. 引子
#include
using namespace std;

class A {
public:
    A() {
        cout << "default constructor" << endl;
    }

    A(int x) : x(x) {
        cout << "transform constructor" << endl;
    }

    A(const A &a) {
        cout << "copy constructor" << endl;
    }

    int x;
};

A func() {
    A temp(69);
    return temp;
}

int main() {
    A a = func();
    cout << a.x << endl;
    return 0;
}

运行结果:

问题:为什么没有调用拷贝构造?如果没有调用拷贝构造那么 a 对象是不是就没被初始化?如果 a 没被初始化什么 a.x = 69?

为了解决上面的问题,打印出对象 a 和对象 temp 的地址:

#include
using namespace std;

class A {
public:
    A() {
        cout << "default constructor" << endl;
    }

    A(int x) : x(x) {
        cout << "transform constructor" << endl;
    }

    A(const A &a) {
        cout << "copy constructor" << endl;
    }

    int x;
};

A func() {
    A temp(69);
    cout << "&temp = " << &temp << endl;
    return temp;
}

int main() {
    A a = func();
    cout << "&a = " << &a << endl;
    cout << a.x << endl;
    return 0;
}

运行结果:

temp 和 a 的地址完全一样,现在来理解一下这个结果:

A a = func(); 会将 func() 函数的返回值原封不动地拷贝给 a 对象,而 func() 函数的返回值是一个局部变量,也就是说局部对象 temp 做的所有的 *** 作,最后都会被原封不动地反映到对象 a 上。

所以编译器就做了一个优化:将 temp 调用过程中的 this 指针全都替换为 a 对象的地址,这就叫做返回值优化。

2. 对象的初始化 3. 函数形参的初始化


当前是值传递,需要将 a 拷贝给参数 b,对于参数 b 的构造过程:开辟对象 b 的数据存储区 => 匹配拷贝构造函数( a 拷贝给 b )=> 完成对象 b 的构造过程。

4. 返回值优化(RVO) 1. 最基本的情况:2次拷贝构造、1次有参构造

理解:“Step5:使用 temp_a 调用临时匿名变量的拷贝构造函数”

func() 函数的返回值对应一个临时匿名变量,return temp_a;就是将 temp_a 拷贝给临时匿名变量,即 临时匿名变量 = temp_a ,调用了临时匿名变量的拷贝构造函数。

在Step1 ~ Step9的过程中发生了两次拷贝构造、一次有参构造行为。

整个过程如下:(+代表拷贝)

解读:

  1. 先开辟对象 a 的数据区;
  2. 然后开辟 temp_a 对象的数据区;
  3. 完成 temp_a 的构造;
  4. 将 temp_a 拷贝给临时匿名变量;
  5. 完成临时匿名变量的构造;
  6. 将临时匿名变量拷贝给对象 a ;
  7. a 对象完成拷贝。

其中的临时匿名变量 anonymous 就是中间商,没有起任何作用。

2. 第一套优化:1次拷贝,1次有参构造(微软系编译器可以看到一次拷贝的现象)

因为临时匿名变量没有任何作用,所以编译器做了第一套优化方案,优化掉anonymous,直接将temp_a对象拷贝给a对象:

在该流程下,发生了一次拷贝(temp_a 拷贝给 a),一次有参构造。

代码执行步骤:

3. 第二套优化:0次拷贝,1次有参构造

又因为 temp_a 是局部对象,如果未来 temp_a 会拷贝给 a 对象,意味着在 temp_a 上做的所有 *** 作都相当于是加在 a 对象上,莫不如直接将 temp_a 当做 a 的别名, *** 作temp_a 就相当于 *** 作 a 对象:

这种流程下,只有一次有参构造,有参构造构造的既是temp_a对象,也是a对象。

4. 关掉返回值优化的方法
//编译的时候添加一个编译选项:
g++ -fno-elide-constructors xx.cpp
#include
using namespace std;

class A {
public:
    A() {
        cout << this << " default constructor" << endl;
    }

    A(int x) : x(x) {
        cout << this << " transform constructor" << endl;
    }

    A(const A &a) {
        cout << this << " copy constructor" << endl;
    }

    int x;
};

A func() {
    A temp(69);
    cout << "&temp = " << &temp << endl;
    return temp;
}

int main() {
    A a = func();
    cout << "&a = " << &a << endl;
    cout << a.x << endl;
    return 0;
}

编译的时候关闭返回值优化的运行结果:

可以看到 temp 和 a 对象的地址不同,一共1次有参构造 + 2次拷贝构造【分别调用了临时匿名对象的拷贝构造(temp拷贝给临时匿名对象)、a对象的拷贝构造】

5. 小结

编译器在拷贝 *** 作上会做一些优化,意味着一段拷贝代码到底调用了几次拷贝构造函数是不确定,也就意味着当进行工程设计的时候不能改变拷贝构造的语义,因为改变了拷贝构造的语义的话只有你自己知道将拷贝构造函数改成了什么功能,但是编译器是不知道你修改了拷贝构造的语义。

#include
using namespace std;

class A {
public:
    A() {
        cout << this << " : default constructor" << endl;
    }

    A(int x) : x(x) {
        cout << this << " : transform constructor" << endl;
    }

    A(const A &a) {
        cout << this << " : copy constructor" << endl;
    }

    A &operator=(const A &a) {
        cout << this << " : operator=" << endl;
        return *this;
    }

    int x;
};

A func() {
    A temp(69);
    cout << "&temp = " << &temp << endl;
    return temp;
}

int main() {
    A a = func();
    cout << "&a = " << &a << endl;
    cout << a.x << endl;
    cout << "===========" << endl;
    a = func();
    return 0;
}

运行结果:

赋值运算符的语义是可以修改的,绝大多数编译器会对拷贝构造进行优化,不会对赋值运算符进行优化。

拷贝构造的语义:原封不动地拷贝过来

如下这段代码没有改变拷贝构造的语义:原封不动地将 a 对象的 x 拷贝过去

而如下代码则改变了拷贝构造的语义:每次拷贝构造的时候都+1,就不是原封不动地进行拷贝了

改变了拷贝构造的语义,加上编译器的优化,那么程序的结果就是不可预期的,和拷贝次数有关。

如果初始化列表中没有对成员属性进行初始化,那么会调用成员属性的默认构造函数,如果想调用拷贝构造函数,就需要显式地调用拷贝构造。

编译器是通过替换指针实现返回值优化的。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/4994803.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-11-14
下一篇 2022-11-14

发表评论

登录后才能评论

评论列表(0条)

保存