前言:当一个类涉及资源管理的时候,如动态分配内存,编译器默认生成的赋值行为可能不符合我们的需求,这时候,我们需要自行实现拷贝赋值运算符,确保资源被正确地处理。
目录
1) 防止自赋值 防止自赋值)
2) 浅拷贝陷阱 浅拷贝陷阱)
3) 三/五/零法则 三/五/零法则)
一、拷贝赋值运算符定义与语法
C++的拷贝赋值运算符 是类中对**赋值运算=**的一种重载函数,它的主要作用是定义一个对象赋值给另一个对象时候应执行的一些具体行为。拷贝赋值运算符是通常被声明为类的成员函数,语法格式如下:
cpp
ClassName& ClassName::operator=(const ClassName& other);
//返回当前对象的引用
下面我们来看一个简单的 String 类,其中,定义了拷贝赋值运算符来管理动态分配的字符数组:
cpp
class String {
private:
char* data;
size_t length;
public:
// 此处省略构造函数、析构函数等...
String& operator=(const String& other) {
if (this != &other) { // 检查自赋值
delete[] data; // 释放原有的内存
length = other.length;
data = new char[length + 1];
std::strcpy(data, other.data); // 深拷贝字符串内容
}
return *this;
}
};
上述例子中,我们在拷贝复制运算中所做的操作包括:
-
检查自赋值:判断赋值运算符的右侧对象是否就是当前对象本身。自赋值检查是必要的,如果检查,直接释放当前对象的资源再进行复制会导致严重的资源问题。
-
释放原有内存空间:如果我们当前对象已经分配了资源(如 动态内存),在赋值新值之前需要先释放这些资源,避免产生内存泄漏。
-
分配新内存并进行深拷贝:最后,我们根据源对象的数据大小分配新的资源,然后将源对象的数据复制到新的内存空间中去。
二、默认拷贝赋值行为
如果我们在类中,没有显示地去定义拷贝赋值运算符,编译器会自动生成一个默认的赋值运算符。默认的拷贝赋值运算符会对成员进行逐一赋值。 对于那些内置类型或没有动态资源的类,默认赋值运算符通常是没有问题的。但是,当我们的类包含了指针或涉及了动态分配的资源 ,默认的赋值运算符行为会导致浅拷贝问题。
我们以类中的指针成员为例,浅拷贝的问题在于,它仅复制了指针本身,而不是复制指针所指向的实际数据。这就会导致多个指针指向了同一块内存,很容易造成资源管理问题。
我们举一个简单的例子,以下类我们没有显示定义拷贝赋值运算符:
cpp
class ShallowCopy {
public:
int* ptr;
ShallowCopy(int val) : ptr(new int(val)) {}
~ShallowCopy() { delete ptr; }
};
当我们执行赋值 obj2 = obj1 时,编译器会逐成员赋值,将 obj1.ptr 的值复制给 obj2.ptr。结果这两个对象的 ptr 指针将指向同一块内存,这里隐藏着一个问题:当其中一个对象被销毁时,析构函数会释放这块内存,而另一个对象的指针仍然指向这块已释放的内存,就成为了悬空指针!
默认的浅拷贝行为稍不注意,会产生严重的资源管理问题,大家切记要显示定义自己的拷贝赋值运算符,满足自己的开发需求!
三、一些注意事项
1) 防止自赋值
自赋值 是指将对象赋值给其自身,例如 obj = obj;。虽然直接显式地写 x = x 很少见,但通过指针或引用赋值时可能无意中会发生自赋值,例如:
cpp
void func(X& a, X& b) {
a = b; // 如果 a 和 b 引用同一个对象,则为自赋值
}
X c;
func(c, c); // 此时在 func 内部发生自赋值
如果我们在拷贝赋值运算符中没有进行自赋值检查,将会导致一系列未定义行为。例如:
cpp
X& X::operator=(const X& rhs) {
delete[] data; // 先释放当前对象的资源(data是类中的一个指针成员)
data = new char[strlen(rhs.data) + 1]; // 分配新资源
strcpy(data, rhs.data); // 复制数据
return *this;
}
如果上述代码发生了自赋值,delete[] data 将释放当前对象的内存,而 rhs.data 正是指向这块内存的指针。接下来我们进行 strcpy ,它试图从一个已被释放的内存地址读取数据,这会导致程序崩溃!
2) 浅拷贝陷阱
我们在上一节中已经进行了相关的分析讲解,再次提醒大家,必要时,一定要显示定义自己的拷贝运算符!
3) 三/五/零法则
C++ 有著名的三法则 和五法则 ,现代 C++ 又推出了零法则,大家可以参考这些法则,辅助自己判断何时需要进行自定义。
三法则说的是,如果某个类需要自定义析构函数 、拷贝构造函数 或拷贝赋值运算符中的任意一个,那么几乎肯定需要自定义全部三个。
五法则是三法则的扩充, 因为C++11 引入了移动语义,多了移动构造函数与移动赋值运算符。
零法则主要的思想是,尽可能让类无需 自定义任何拷贝控制成员,也就是"零"自定义。通常,我们可以通过使用RAII 类(如智能指针、容器等)作为成员来实现零定义。零法则强调资源管理应由专门的类型负责,业务逻辑类不应当直接管理资源,这大大降低了代码复杂性,减少了出错机会。