在 C++ 中,复制对象时最初使用的是逐成员复制机制:针对对象的每个成员调用其相应的复制操作。对于内置类型这是安全的,但对于动态资源则会出现问题:只是复制了指针,而不是实际数据。
如果一个对象包含指向堆内存的指针,那么复制两个对象后,它们将指向同一内存区域。然后,当一个对象被销毁时,内存将被释放,第二个对象的指针将变得无效(“野指针”)。这会导致运行时错误和内存泄漏。
为了使复制独立,需要进行深拷贝——逐字节复制并分配自己的缓冲区。这可以通过编写自定义的复制构造函数和赋值运算符来实现。
示例代码:
class MyString { char* data; public: MyString(const char* s) { data = new char[strlen(s)+1]; strcpy(data, s); } // 深拷贝构造函数 MyString(const MyString& src) { data = new char[strlen(src.data) + 1]; strcpy(data, src.data); } // 深拷贝赋值 MyString& operator=(const MyString& src) { if (this != &src) { delete[] data; data = new char[strlen(src.data) + 1]; strcpy(data, src.data); } return *this; } ~MyString() { delete[] data; } };
关键特性:
如果类只包含指针,但不分配内存,为什么还需要自定义析构函数?
析构函数仅在您在类中明确分配了内存(或其他资源)时才是必要的。如果指针没有分配内存,默认的析构函数就足够了。
如果在包含动态内存的类中声明了复制构造函数,但未实现operator=会发生什么?
如果您手动定义了复制构造函数,编译器不会自动实现operator=;它将被隐式声明,或者编译器将发出错误/警告(取决于标准)。这将导致赋值时行为不明确:将执行逐成员复制,并出现双重释放或内存泄漏。
示例代码:
MyString a("hi"); MyString b = a; // 可以:您的复制构造函数 MyString c("bye"); c = a; // 问题!如果不手动实现operator=将发生浅拷贝
在手动实现operator=时,自我赋值有什么危险?
如果在不检查this!=&rhs的情况下共享资源,那么在赋值给自己时将执行delete[] data,随后会尝试复制已经被销毁的数组,这会导致segfault。自我保护:总是检查自我赋值。
if (this != &rhs) { ... }
开发者复制了一个带有内置指针的类对象,并未实现深拷贝。复制后多个对象共享同一内存区域。两个析构函数导致双重删除,程序崩溃。
优点:
缺点:
开发者正确实现了复制构造函数、赋值运算符和析构函数。每个对象拥有自己的内存。
优点:
缺点: