题目
编写程序,阐明 C++ 经典三大件准则“Rule of Three”(三法则):如果一个类需要显式自定义析构函数(通常意味着管理动态裸内存),那么它几乎必定同时需要显式自定义拷贝构造函数与拷贝赋值运算符(operator=)。手写一个安全健全的字符串类 SimpleString,重点演示拷贝赋值运算符中的自我赋值检查(Self-assignment Check if (this == &other))与异常安全写法的必要性。
输入格式:无需外部输入。
输出格式:输出三大法则函数协同运转、自我赋值防护与对象赋值日志。
数据范围与约定:C++ 经典三法则、深拷贝赋值与自我赋值防御。
思路与算法
1. 为什么三者必须共存亡
如果只写了析构函数释放指针,编译器默认合成的拷贝构造与拷贝赋值均是浅拷贝,会导致悬垂指针与双重释放崩溃;因此自定义析构函数时,必须同时自定义拷贝构造与拷贝赋值。
2. 自我赋值陷阱(Self-assignment)
若执行 s = s;,若在赋值运算符中直接先 delete[] data_;,此时 other.data_ 也随之被释放掉了!紧接着去拷贝 other.data_ 就会读取野指针。必须用 if (this == &other) return *this; 或采用 Copy-and-Swap 惯用法。
3. Copy-and-Swap 惯用法
现代推荐赋值写法:SimpleString temp(other); std::swap(data_, temp.data_); return *this;,利用局部临时对象实现强异常安全保证。
完整程序
#include <iostream>
#include <cstring>
class SimpleString {
private:
char* data_;
size_t len_;
public:
// 普通构造
SimpleString(const char* str = "") {
len_ = std::strlen(str);
data_ = new char[len_ + 1];
std::strcpy(data_, str);
std::cout << " [构造] 创建字符串: \"" << data_ << "\"\n";
}
// 1. 三法则之一:析构函数
~SimpleString() {
std::cout << " [析构] 销毁字符串: \"" << (data_ ? data_ : "nullptr") << "\"\n";
delete[] data_;
data_ = nullptr;
}
// 2. 三法则之二:拷贝构造函数
SimpleString(const SimpleString& other) {
len_ = other.len_;
data_ = new char[len_ + 1];
std::strcpy(data_, other.data_);
std::cout << " [拷贝构造] 深拷贝生成: \"" << data_ << "\"\n";
}
// 3. 三法则之三:拷贝赋值运算符
SimpleString& operator=(const SimpleString& other) {
std::cout << " [拷贝赋值] 尝试将 \"" << other.data_ << "\" 赋值给 \"" << data_ << "\"\n";
// 防御 1:自我赋值检测 (Self-assignment Check)
if (this == &other) {
std::cout << " [自我赋值拦截] 检测到 self == this,直接短路跳过\n";
return *this;
}
// 释放原有资源并开辟新资源
char* newData = new char[other.len_ + 1];
std::strcpy(newData, other.data_);
delete[] data_; // 确保分配成功后再释放旧内存(异常安全)
data_ = newData;
len_ = other.len_;
return *this;
}
void print(const std::string& label) const {
std::cout << label << ": " << data_ << "\n";
}
};
int main() {
SimpleString s1("Alpha");
SimpleString s2("Beta");
std::cout << "\n--- 1. 测试拷贝赋值运算符 ---\n";
s2 = s1; // 触发 operator=
s2.print("赋值后 s2");
std::cout << "\n--- 2. 测试自我赋值防御 (s1 = s1) ---\n";
s1 = s1; // 触发自我赋值拦截
s1.print("自赋值后 s1");
std::cout << "\n--- 3. 作用域结束释放 ---\n";
return 0;
}运行示例
输出:
[构造] 创建字符串: "Alpha"
[构造] 创建字符串: "Beta"
--- 1. 测试拷贝赋值运算符 ---
[拷贝赋值] 尝试将 "Alpha" 赋值给 "Beta"
赋值后 s2: Alpha
--- 2. 测试自我赋值防御 (s1 = s1) ---
[拷贝赋值] 尝试将 "Alpha" 赋值给 "Alpha"
[自我赋值拦截] 检测到 self == this,直接短路跳过
自赋值后 s1: Alpha
--- 3. 作用域结束释放 ---
[析构] 销毁字符串: "Alpha"
[析构] 销毁字符串: "Alpha"