专业编程基础技术教程

网站首页 > 基础教程 正文

C++|拷贝构造、拷贝赋值生成的临时对象与编译器的优化

ccvgpt 2024-10-12 13:50:29 基础教程 6 ℃

C++面向对象的资源获取即初始化RAII与运行时类型识别RTTI虽然获得了安全性与易扩展性,但也带来了性能的一些损耗,编译器也是想方设法来利用一些临时对象(拷贝构造与拷贝赋值产生的)及避免不必要的拷贝构造与拷贝赋值函数的调用。

1 拷贝构造与临时对象

#include <iostream>
using namespace std;

class CDem
{
public:
    CDem(){  // 构造函数
        cout<<this<<" "<<"is constructed!"<<endl;
    }
    CDem(CDem& cd){ // 拷贝构造函数
        cout<<this<<" "<<"is copied constructed!"<<endl;
    }
    ~CDem(){ // 析构函数
        cout<<this<<" "<<"is destructed!"<<endl;
    }
};
CDem operator+(const CDem &cd,const CDem &cm)
{
    CDem d; // 构造函数被调用
    cout<<"d: "<<&d<<endl;
    return d;
}  // 在作用域结束点,栈上定义的对象的析构函数会被调用
void test()
{
    CDem a;// 构造函数被调用
    cout<<"a: "<<&a<<endl;
    CDem b;// 构造函数被调用
    cout<<"b: "<<&b<<endl;
    CDem c = a+b;  // 调用operator+和拷贝构造(这里不会调用构造函数,是直接拷贝构造)
    cout<<"c: "<<&c<<endl;
} // 在作用域结束点,栈上定义的对象的析构函数会被调用
int main()
{
    test();
    getchar();
    return 0;
}
/*
0012FEE0 is constructed!
a: 0012FEE0
0012FEDC is constructed!
b: 0012FEDC
0012FE68 is constructed!
d: 0012FE68
0012FED8 is copied constructed!
0012FE68 is destructed!
c: 0012FED8
0012FED8 is destructed!
0012FEDC is destructed!
0012FEE0 is destructed!
*/

operator+的最后一行代码的汇编:

C++|拷贝构造、拷贝赋值生成的临时对象与编译器的优化

21:       return d;
0040160C   lea         ecx,[ebp-10h]
0040160F   push        ecx
00401610   mov         ecx,dword ptr [ebp+8]
00401613   call        @ILT+25(CDem::CDem) (0040101e) // 这里是拷贝构造
00401618   mov         edx,dword ptr [ebp-14h]
0040161B   or          edx,1
0040161E   mov         dword ptr [ebp-14h],edx
00401621   mov         byte ptr [ebp-4],0
00401625   lea         ecx,[ebp-10h]
00401628   call        @ILT+5(CDem::~CDem) (0040100a) // 这里析构
0040162D   mov         eax,dword ptr [ebp+8]

可以看出,return d后是调用了拷贝构造函数,构造的一个临时对象直接转移给了c。

一些编译器可以提供更深度的一些优化选择,将上例中的d直接转移给c,避免一次拷贝构造与析构调用。

2 拷贝赋值与临时对象

#include <iostream>
using namespace std;

class CDem
{
public:
    CDem(){ // 构造函数
        cout<<this<<" "<<"is constructed!"<<endl;
    }
    CDem(CDem& cd){ // 拷贝构造
        cout<<this<<" "<<"is copied constructed!"<<endl;
    }
    CDem& operator=(CDem& cd){ // 拷贝赋值符重载
        cout<<this<<" "<<"is copied assigned!"<<endl;
        return *this;
    }
    ~CDem(){ // 析构函数
        cout<<this<<" "<<"is destructed!"<<endl;
    }
};
CDem operator+(const CDem &cd,const CDem &cm)
{
    CDem d; // 构造函数被调用
    cout<<"d: "<<&d<<endl;
    return d;
}
void test()
{
    CDem a; // 构造函数被调用
    cout<<"a: "<<&a<<endl;
    CDem b; // 构造函数被调用
    cout<<"b: "<<&b<<endl;
    CDem c; // 构造函数被调用
    cout<<"c: "<<&c<<endl;
    c = a+b; // 调用operator+、=
}
int main()
{
    test();
    getchar();
    return 0;
}
/*
0012FEE0 is constructed!
a: 0012FEE0
0012FEDC is constructed!
b: 0012FEDC
0012FED8 is constructed!
c: 0012FED8
0012FE5C is constructed!
d: 0012FE5C
0012FED4 is copied constructed!
0012FE5C is destructed!
0012FED8 is copied assigned!
0012FED4 is destructed!
0012FED8 is destructed!
0012FEDC is destructed!
0012FEE0 is destructed!
*/

如果开启编译器优化选项,也是可以利用临时对象,减少一次拷贝构造。

事实上,对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,在C++11中,引入了移动构造与移动拷贝的语义。当然,其更多的是针对深拷贝(而非浅拷贝),将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,提高了初始化的执行效率。

3 对象传参与返回及返回时的临时对象

函数的对象值传递和值返回都会调用拷贝构造函数,参数对象传值时拷贝构造给对象形参名,返回时会构造一个临时对象(匿名对象)。

#include <iostream>
using namespace std;

class CDem
{
public:
    CDem(){         // 构造函数
        cout<<this<<" "<<"is constructed!"<<endl;
    }
    CDem(CDem& cd){ // 拷贝构造
        cout<<this<<" "<<"is copied constructed!"<<endl;
    }
    CDem& operator=(CDem& cd){ // 拷贝赋值符重载
        cout<<this<<" "<<"is copied assigned!"<<endl;
        return *this;
    }
    ~CDem(){                    // 析构函数
        cout<<this<<" "<<"is destructed!"<<endl;
    }
};
                // 函数对象传参与返回
CDem foo(CDem a)// 对象传值时会调用拷贝构造函数
{
    cout<<"a: "<<&a<<endl;
    CDem b;
    cout<<"b: "<<&b<<endl;
    return b;   // 对象返回时会调用拷贝构造函数构造一个临时对象
}
void test()
{
    CDem c; // 拷贝构造
    cout<<"c: "<<&c<<endl;
    CDem d; // 拷贝构造
    cout<<"d: "<<&d<<endl;
    d = foo(c);// 函数返回时会将临时对象拷贝构造给d
}
int main()
{
    test();
    getchar();
    return 0;
}
/*
0012FEE0 is constructed!
c: 0012FEE0
0012FEDC is constructed!
d: 0012FEDC
0012FE78 is copied constructed!
a: 0012FE78
0012FE5C is constructed!
b: 0012FE5C
0012FED4 is copied constructed!
0012FE5C is destructed!
0012FE78 is destructed!
0012FEDC is copied assigned!
0012FED4 is destructed!
0012FEDC is destructed!
0012FEE0 is destructed!
*/

我们知道,对于函数返回整型或指针类型,可以通过寄存器返回。函数返回浮点型时,通过浮点寄存器返回。对于包括对象在内的复合类型,无法通过寄存器返回,编译器一般的做法是,主调函数函数会在其存储局部变量的栈帧空间内安排一块空间来存放返回的复合类型的值,这块空间的首地址会在函数调用时在将被调函数的参数压栈后执行压栈操作。

/*
      ESP==>|           :             |
            |           .             |
            +-------------------------+
            |被调用者保存的寄存器现场 |
            |EBX,ESI和EDI(根据需要)|
            +-------------------------+
            |  临时空间               |
            +-------------------------+
            |  临时空间               |
            +-------------------------+
            |  局部变量#2             | [EBP - 8]
            +-------------------------+
            |  局部变量#1             | [EBP - 4]
            +-------------------------+
      EBP==>|  被调函数的EBP          |
            +-------------------------+
            |  返回地址               | [EBP + 4]
            +-------------------------+
            |  复合值返回地址(如果是)| [EBP + 8] // 在这里压返回复合类型的值的首地址
            +-------------------------+
            |  实际参数#1 (如果有)    | [EBP + 12]
            +-------------------------+
            |  实际参数#2 (如果有)    | [EBP + 14]
            +-------------------------+
            |调用者保存的寄存器现场   |
            |ebx,esi和edi(根据需要)|
            +-------------------------+
            |            :            |
            |            .            |
           
一个典型的栈帧,
图中,栈顶在上,地址空间往下增长。
*/

4 直接计算过程中的临时对象

#include <iostream>
using namespace std;

class CDem
{
public:
    int m_i;
    CDem(int a){
        cout<<this<<" "<<"is constructed!"<<endl;
        m_i = a;
    }
    CDem(CDem& cd){
        cout<<this<<" "<<"is copied constructed!"<<endl;
        m_i = cd.m_i;
    }
    CDem& operator=(CDem& cd){
        cout<<this<<" "<<"is copied assigned!"<<endl;
        m_i=cd.m_i;
        return *this;
    }
    ~CDem(){
        cout<<this<<" "<<"is destructed!"<<endl;
    }
};
CDem operator+(const CDem &cd,const CDem &cm)
{
    CDem d(0);
    cout<<"d: "<<&d<<endl;
    d.m_i = cd.m_i+cm.m_i;
    return d;
}
void test()
{
    CDem a(3);
    cout<<"a: "<<&a<<endl;
    CDem b(4);
    cout<<"b: "<<&b<<endl;
    cout<<(a+b).m_i<<endl;  // 临时对象的使用与析构
}
int main()
{
    test();
    getchar();
    return 0;
}
/*
0012FEE0 is constructed!
a: 0012FEE0
0012FEDC is constructed!
b: 0012FEDC
0012FE5C is constructed!
d: 0012FE5C
0012FED8 is copied constructed!
0012FE5C is destructed!
7
0012FED8 is destructed!
0012FEDC is destructed!
0012FEE0 is destructed!
*/

小心临时对象的生命周期,如果临时对象没有被引用,临时对象会执行当前行代码后即刻有被覆盖的可能:

#include <iostream>
#include <string>
using namespace std;

int main()
{
    const char *p = (string("abc")+string("def")).c_str();
    string str = string("123") + string("456");
    cout<<p<<endl;
    cout<<str.c_str()<<endl;
    getchar();
    return 0;
}
/*
456
123456
*/

还有下面这种复合赋值运算符与非复合赋值运算符。一方面,非复合赋值表达式要多写一个变量名,会麻烦一些,也容易出错,且也占代码空间,另外,早期的编译器,会对等式右边的表达式生成一个临时对象后再赋值给左值,但现在的编译期则优化了临时对象,直接更新了左值,两者有了同样的汇编代码。

7:        sum_i = sum_i + num_i;
00401056   mov         eax,dword ptr [ebp-4]
00401059   add         eax,dword ptr [ebp-8]
0040105C   mov         dword ptr [ebp-4],eax
8:        sum_i += num_i;
0040105F   mov         ecx,dword ptr [ebp-4]
00401062   add         ecx,dword ptr [ebp-8]
00401065   mov         dword ptr [ebp-4],ecx

ref

王键伟《C++对象模型探索》

https://www.bilibili.com/video/BV1xq4y1y7S1?p=50

-End-

最近发表
标签列表