您的位置:首页 > 编程语言 > C语言/C++

C++深拷贝和浅拷贝区别

2012-04-12 11:17 232 查看
对于普通类型的对象来说,它们之间的复制是很简单的,例如:

int a=88;

int b=a;

而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。下面看一个类对象拷贝的简单例子。


#include <iostream>


using namespace std;




class CExample {


private:


 int a;


public:


 CExample(int b)


 { a=b;}


 void Show ()


 {


cout<<a<<endl;


}


};




int main()


{


 CExample A(100);


 CExample B=A;


 B.Show ();


 return 0;


}

运行程序,屏幕输出100。从以上代码的运行结果可以看出,系统为对象B分配了内存并完成了与对象A的复制过程。就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的。下面举例说明拷贝构造函数的工作过程。


#include <iostream>


using namespace std;




class CExample {


private:


int a;


public:


CExample(int b)


{ a=b;}




CExample(const CExample& C)


{


a=C.a;


}


void Show ()


{


cout<<a<<endl;


}


};




int main()


{


CExample A(100);


CExample B=A;


B.Show ();


return 0;


}

CExample(const CExample&
C)就是我们自定义的拷贝构造函数。可见,拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它的唯一的一个参数是本类型的一个引用变量,该参数是const类型,不可变的。例如:类X的拷贝构造函数的形式为X(X& x)。

当用一个已初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用。也就是说,当类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数:

一个对象以值传递的方式传入函数体

一个对象以值传递的方式从函数返回

一个对象需要通过另外一个对象进行初始化。

如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝,后面将进行说明。

自定义拷贝构造函数是一种良好的编程风格,它可以阻止编译器形成默认的拷贝构造函数,提高源码效率。

浅拷贝和深拷贝

  在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
  深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。

c++默认的拷贝构造函数是浅拷贝

浅拷贝就是对象的数据成员之间的简单赋值,如你设计了一个类而没有提供它的复制构造函数,当用该类的一个对象去给令一个对象赋值时所执行的过程就是浅拷贝,如:

class A {public:A(int _data) : data(_data){} A(){}

private: int data; };

int main() { A a(5), b = a; // 仅仅是数据成员之间的赋值 }

这一句b = a;就是浅拷贝,执行完这句后b.data = 5;

如果对象中没有其他的资源(如:堆,文件,系统资源等),则深拷贝和浅拷贝没有什么区别,但当对象中有这些资源时,例子:

class A { public: A(int _size) : size(_size){data = new int[size];} // 假如其中有一段动态分配的内存 A(){}; ~A(){delete [] data;} // 析构时释放资源

private: int* data; int size; }

int main() { A a(5), b = a; // 注意这一句 }

这里的b = a会造成未定义行为,因为类A中的复制构造函数是编译器生成的,所以b = a执行的是一个浅拷贝过程。我说过浅拷贝是对象数据之间的简单赋值,比如:

b.size = a.size; b.data = a.data; // Oops!

这里b的指针data和a的指针指向了堆上的同一块内存,a和b析构时,b先把其data指向的动态分配的内存释放了一次,而后a析构时又将这块已经被释放过的内存再释放一次。对同一块动态内存执行2次以上释放的结果是未定义的,所以这将导致内存泄露或程序崩溃。

所以这里就需要深拷贝来解决这个问题,深拷贝指的就是当拷贝对象中有对其他资源(如堆、文件、系统等)的引用时(引用可以是指针或引用)时,对象的另开辟一块新的资源,而不再对拷贝对象中有对其他资源的引用的指针或引用进行单纯的赋值。如:

class A { public: A(int _size) : size(_size){data = new int[size];} // 假如其中有一段动态分配的内存

A(){}; A(const A& _A) : size(_A.size){data = new int[size];} // 深拷贝

~A(){delete [] data;} // 析构时释放资源

private: int* data; int size; }

int main() { A a(5), b = a; // 这次就没问题了 }

CSDN上转载的一个不错的比喻:

老师给你们留了作业,你们班只有小A一个人会写,其他人都需要copy他的作业。

如果你们都是通过浅拷贝来完成作业,那么就相当与“在你需要的时候,小A会把作业借给你”。

例如,老师对小B说“去把你的作业拿来给我看看”,小B赶快找小A借作业给老师看。老师又要看小C的作业,小C也只能找小A借过来应付老师。总之老师如果一个一个的检查作业,你们都能应付,因为你们只有一本作业互相借着看一下就OK了。如果老师说“全班把作业一起交上来”,那你们就完蛋了。

深拷贝就是每个人都实实在在的抄了一份作业,每个人的作业都是自己抄来的,所以老师收全班的作业你们也不怕,要多少本就给他多少本,呵呵。

所以使用浅拷贝,对于对象的引用成员,仅仅是拷贝了它的引用,修改拷贝对象的引用成员也会同时修改原对象的相应成员。而深拷贝则是一直拷贝到值类型成员为止。个人理解,如果要对一个链表进行深拷贝,则需要创建这个链表中的所有表节点对象,再将这些对象依照原来的关系连接起来,这时存在两条链表,内存中增加了一倍的存储(增加了n个对象引用,以及为这些对象分配的n个空间)。而如果进行浅拷贝,只是拷贝了一个链表对象引用,并为这个引用分配了相应的空间,内存中仅仅是多了一个对象的引用(4个字节,栈区分配)以及一个对象(堆区分配)。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: