C++中的深复制与浅复制

时间:2022-06-03 04:03:56

在C#与Java的编程技术书中,关于赋值运算符经常会提到深复制与浅复制两个概念,因为C#与Java把对象分为两类:值类型和引用类型,而在C++中则没有明确这两个概念,其实在C++中的指针和引用都是引用类型的变量,C++标准库std::tr1::shared_ptr非常类似C#与Java中的引用变量,只有内置数据类型的变量才是值变量。既然在C++中也有值变量和引用变量,则在C++中也存在深复制与浅复制的问题。

C++中的深复制与浅复制

图?: 深复制与浅复制

如图所示,当自定义的数据类型中存在由程序员维护的资源时,如在堆上动态分配的内存,在复制对象时需要考虑对象的深度复制,即在新对象中重新分配一块堆内存,并将源对象中的数制复制过来。如果没有复制资源,而是只复制了指向该资源的指针,便产生如图所示的情景,当对象A被销毁时,资源被释放,而对象B的指针则指向已经释放的资源,当对象B再次使用已经释放的资源时,后果将不可预料,所以在实现赋值运算符时必须考虑深复制与浅复制的问题。

一个完整、健壮、高效的赋值运算符,通常至少要考虑以下几个问题:

1) 将值赋给变量自己[4]。这种情况下不需要重新配置内存,以提高这种情况下的效率。

2) 完整复制对象[4],避免遗漏数据成员。

3) 返回变量自己的引用。主要用于连续赋值等情况,以简化代码。

4) 如果赋值失败,保持原对象不变[2]。

例如简单字符串类String的赋值运算符:

代码

class String

{

public:

String& operator=(const String& other);

private:

char* _buff;

size_t _size;

};

String& String ::operator=(const String& other)

{

if( this == &other) // 检查自赋值

return *this;

size_t size = strlen(other._buff) + 1;

if( _size < size){

char* newBuff = new char[size]; // 可能抛出std::bad_alloc

char* bakBuff = _buff; // 备份旧内存块地址

_buff = newBuff;

delete bakBuff;

}

strcpy(_buff, other._buff);

_size = size;

return *this;

}

而在C++标准库中为了减少内存的占用,string类使用了copy-on-write技术,当为字符串赋值时并不直接分配内存,而是两个对象的指针指向同一块内存,即进行浅复制,只有当两个对象的任何一方的值发生改变,即要往内存中写入时才进行深复制,为要写入的对象分配空间。如下面的代码:

class OpAssignClass

{

public:

string str;

};

int main( void )

{

OpAssignClass oA;

oA.str = "first string";

OpAssignClass oB(oA);

OpAssignClass oC;

oC = oA;

cout<<"oA.str.c_str( ) = "<<(void*)oA.str.c_str()<

<<"oB.str.c_str() = "<<(void*)oB.str.c_str()<

<<"oC.str.c_str() = "<<(void*)oC.str.c_str()< ;>

oA.str.push_back('1');

cout<<"oA.str.c_str( ) = "<<(void*)oA.str.c_str()<

<<" oB.str.c_str() = "<<(void*) oB.str.c_str()<

<<"oC.str.c_str() = "<<(void*)oC.str.c_str()< ;>

oC.str.push_back('3');

cout<<"oA.str.c_str( ) = "<<(void*)oA.str.c_str()<

<<"oB.str.c_str() = "<<(void*)oB.str.c_str()<

<<"oC.str.c_str() = "<<(void*)oC.str.c_str()< ;>

return 0;

}

运行结果:

oA.str.c_str( ) = 0xce02ec // 尚未写入,三个字符串指向同一块内存

oRef.str.c_str() = 0xce02ec

oC.str.c_str() = 0xce02ec

oA.str.c_str( ) = 0xcf0344 // oA写入,重新分配内存

oRef.str.c_str() = 0xce02ec

oC.str.c_str() = 0xce02ec

oA.str.c_str( ) = 0xcf0344 // oC写入,重新分配内存

oRef.str.c_str() = 0xce02ec

oC.str.c_str() = 0xcf0374

如果不对赋值运算符重载,且内部有指向动态分配的堆内存时,编译器生成的默认赋值运算符将是浅复制,造成多个对象内部指针指向同一块内存,好比C#与Java中的引用变量指向同一个对象,其运行结果将是难以控制的。而在C++中就比较灵活了,我们除了使用赋值运算符进行赋值外,我们还可以使用memcpy和memmove进行对象复制,显然,通过memcpy和memmove可以轻松实现任意类型对象的浅复制。这样我们就可以为任意类型提供深复制和浅复制:

1) 通过memcpy或memmove进行浅复制;

2) 通过赋值运算符进行深复制。