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