也谈C++深拷贝、浅拷贝和函数返回值作参数及其临时变量的生存期

时间:2021-08-08 21:59:58

    为什么会要会想要谈谈这个话题呢,因为最近在看书的时候发现一本书上的一个例程有关于用函数返回值赋值一个对象时,注释说先清除临时对象,再清除函数内作返回值的局部对象。考虑了下,有些怀疑。于是写了几个程序想验证,结果注释掉了复制构造函数的声明作对比。然而,结果却让自己困惑了很久,特别是程序6。最后就作了下面的讨论。当然,也证明了书上说的是错误的。

    这段测试程序代码如下,打开和关闭注释可得6个程序:

#include<iostream>
using namespace std;
int count;
class test
{
private:
 int x,y;
public:
 test(){ count=count+1;cout<<"count="<<count<<" "<<"initializing..."<<&x<<endl;};
 test(int a,int b):x(a),y(b){ count=count+1; cout<<"count="<<count<<" "<<"initializing..."<<&x<<endl;}
 test(const test &);
 void Show();
 ~test(){ count=count-1; cout<<"count="<<count<<" "<<"delete"<<x<<","<<y<<&x<<endl;}
};
test::test(const test &t)
{
 x=t.x;
 y=t.y;
 count=count+1; cout<<"count="<<count<<" "<<"copying..."<<&x<<endl;
}
void test::Show()
{
 cout<<"this is "<<x<<","<<y<<endl;
}
test func()
{
 cout<<"entering func"<<endl;
 test A(1,1);
 A.Show();
 return A;
}
void display(test a)
{
 cout<<"entering display"<<endl;
 test A(2,2);
 A.Show();
}
int main()
{
 //打开复制构造函数注释则是程序1,注释掉复制构造数声明及定义是程序4
 test C;
 C=func(); 
 /*
 //打开复制构造函数注释则是程序2,注释掉复制构造数声明及定义是程序5

 test C=func();
 */ 
 /*
 //打开复制构造函数注释则是程序3,注释掉复制构造数声明及定义是程序6

 display(func());
    */
 cout<<"out display"<<endl;
 return 0;
}

 

程序运行结果如下。

 

也谈C++深拷贝、浅拷贝和函数返回值作参数及其临时变量的生存期

                   test C;

                   C=func2();

图1.程序1()和程序4()

 

也谈C++深拷贝、浅拷贝和函数返回值作参数及其临时变量的生存期

       test C=func2();

图2.程序2()和程序5()

 

也谈C++深拷贝、浅拷贝和函数返回值作参数及其临时变量的生存期

    display(func2());

图3.程序3()和程序6():

要理解的几点:
1.深拷贝和浅拷贝的定义可以简单理解成:是当拷贝对象中有对其他资源(如堆、文件、系统等)的引用指针或引用时,对象的另开辟一块新的资源,而不再对拷贝对象中有对其他资源的引用的指针或引用进行单纯的赋值。反之,浅拷贝就是对象的数据成员之间的简单赋值。
2.在C++中。如果对象在声明对象的同时进行的初始化操作,则进行拷贝运算。
例如:
 test("a");
 test B=A;
此时其实际进行的是B(A)这样的浅拷贝。
如果在对象声明之后,再进行的赋值运算,则进行赋值运算。例如:
 test A("a");
 test B;
 B=A;
此时实际调用的是类的缺省赋值函数B.operator=(A);拷贝构造函数和赋值运算,都有缺省的定义。
3.构造函数和拷贝构造函数都用于初始化。而赋值运算是在对象初始化之后才能调用。
 

下面开始讨论:
在有显示定义复制构造函数时,分别执行以下程序。

注:下面分析都不包括func2()以及局部对象的建立和结束时构造与析构动作,即对象A。

 

程序1(用函数返回值赋值):
test C;
C=func2();

 

结果:有一次构造函数(C的构造函数),一次深度复制构造函数(临时对象的)和两次析构函数(一次C的,一次临时对象的)。
判断:先深拷贝,后默认赋值运算=。先把func2()中的局部类对象深拷贝到函数返回的临时对象;然后,把func2()返回的临时对象通过赋值运算给赋值对象C。
执行顺序:
func2()的先开始,中途执行完深拷贝给临时变量之后,再执行赋值运算到C,再返回到func2()结束。之后在表达式结束时析构临时对象,在main()结束时析构C。
分析:
1.少了一次临时对象复制到C的显示复制构造函数的的调用,是赋值运算。
2.临时对象的销毁在表达式结束的最后。所以才会有func2()中的局部对象虽然先建立,但是却先销毁;而临时变量后建立,却后销毁。
3.C在析构时,可见内存位置没有变化,所以也不可能是引用。而运行结果没有C调用显示复制构造函数的输出;且C已经声明在先,不能再构造一次。所以不可能是深拷贝,而应是赋值运算。


程序2(用函数返回值初始化):
test C=func2();

 

结果:没有构造函数,有一次深度复制构造函数和一次析构函数调用。

判断:先深拷贝,后引用(可被看成一次深度)。
执行顺序:
不于同程序1。func2()的先开始,中途执行完深拷贝之后,再返回到func2()并结束,之后执行优化的浅拷贝。然后在表达式结束时不会析构临时对象(因为其身份已改变),在main()结束时析构C。

 

分析:
1.误解:没有临时对象产生。因为少了一次构造函数调用,也少一次析构函数,就认为没有临时对象产生。因而会被误认为只进行了一次深度复制。
    实际上,并非如此。之所以没有对象C构造函数的调用,是因为:C++的代码优化,如果一个临时对象作为返回值被立即赋给另一个未构造对象,这个临时对象本身将被构造到被赋值对象在内存中的位置(优化的浅拷贝),即引用。所以少了一次构造函数的调用。然而,实际上是减少了一次C的构造和一次临时对象的析构。相当于没有产生临时对象,但事实上是产生了临时对象。调用复制构造函数正其实真正产生的是临时对象。
2.给变量C作拷贝,不会是深拷贝。因为没有调用构造函数或深度复制构造函数,所以只可能是某个已有对象的引用。


程序3(用函数返回值作参数):
display(func2());

 

结果:没有构造函数,有一次深度复制构造函数和一次析构函数调用。

判断:先深度,后引用(可被看成一次深度)。在参数传递中相当于执行的是test C=func2()。


程序4略

程序5用(函数返回值初始化):

没有显示定义复制构造函数时,执行以下程序。
test C=func2();

 

结果:没有构造函数,没有深度复制构造函数和一次析构函数调用。
判断:先深度,后引用(可被看成一次深度)。情况同程序3


程序6(用函数返回值作参数):
没有显示定义复制构造函数时,执行以下程序。
display(func2());

 

结果:没有构造函数,没有深度复制构造函数和两次析构函数调用。
判断:先浅拷贝,再浅拷贝。先把局部对象浅拷贝给临时对象,再把临时对象浅拷贝给C。

分析:
因为,没有定义显示复制构造函数没有定义,也没有类对象的声明。然而,程序输出地址显示,产生了两个不同的类对象。故两次都是浅拷贝。且临时变量的生存期被延长到了调用它的函数结束时,最后一个被销毁。

 

程序6的分析值得商榷!!呵呵.都是愚见,还望斧正!