C++拷贝构造与赋值运算符详解与解决方案
拷贝构造和赋值运算符是C++中最容易出错的部分之一,错误实现会导致资源泄漏、重复释放、浅拷贝等问题。
1. 基本概念与默认行为
1.1 拷贝构造与赋值运算符的区别
class BasicExample {
private:
int* data;
public:
// 构造函数
BasicExample(int value = 0) {
data = new int(value);
}
// 拷贝构造函数:创建新对象时使用
BasicExample(const BasicExample& other) {
data = new int(*other.data); // 深拷贝
}
// 赋值运算符:已存在对象赋值时使用
BasicExample& operator=(const BasicExample& other) {
if (this != &other) { // 自赋值检查
delete data; // 释放原有资源
data = new int(*other.data); // 深拷贝
}
return *this;
}
~BasicExample() {
delete data;
}
};
void demonstrate_difference() {
BasicExample a(10);
BasicExample b = a; // 调用拷贝构造函数
BasicExample c(a); // 调用拷贝构造函数
BasicExample d;
d = a; // 调用赋值运算符
}
1.2 编译器生成的默认版本
class DefaultGenerated {
private:
int* data;
std::string name;
public:
DefaultGenerated(const char* n, int value) : name(n) {
data = new int(value);
}
// 编译器会自动生成:
// 1. 拷贝构造函数:浅拷贝所有成员
// 2. 赋值运算符:浅拷贝所有成员
// 3. 析构函数:不delete指针成员!
// 问题:data会被浅拷贝,导致重复delete
};
2. 常见问题与陷阱
2.1 浅拷贝问题(经典问题)
// ❌ 错误的实现:浅拷贝
class ShallowCopy {
private:
char* buffer;
size_t size;
public:
ShallowCopy(const char* str) {
size = strlen(str);
buffer = new char[size + 1];
strcpy(buffer, str);
}
// 错误:使用编译器生成的拷贝构造和赋值
// 或者手动实现但只拷贝指针
~ShallowCopy() {
delete[] buffer;
}
};
void shallow_copy_problem() {
ShallowCopy obj1("Hello");
ShallowCopy obj2 = obj1; // 浅拷贝:两个对象共享同一buffer
// obj1和obj2析构时都会delete[]同一个buffer → 未定义行为
}
2.2 自赋值问题
// ❌ 错误的赋值运算符:无自赋值检查
class NoSelfAssignmentCheck {
private:
int* data;
public:
NoSelfAssignmentCheck(int value = 0) : data(new int(value)) {}
NoSelfAssignmentCheck& operator=(const NoSelfAssignmentCheck& other) {
delete data; // 如果是自赋值,这里就删除了自己的数据
data = new int(*other.data); // 然后使用已删除的数据!
return *this;
}
~NoSelfAssignmentCheck() {
delete data;
}
};
void self_assignment_problem() {
NoSelfAssignmentCheck obj(42);
obj = obj; // 自赋值:未定义行为!
}
2.3 异常不安全问题
// ❌ 异常不安全的赋值运算符
class ExceptionUnsafe {
private:
int* data;
std::string name;
public:
ExceptionUnsafe(const std::string& n, int value) : name(n) {
data = new int(value);
}
ExceptionUnsafe& operator=(const ExceptionUnsafe& other) {
delete data; // 1. 先删除原有资源
data = new int(*other.data); // 2. 可能抛出std::bad_alloc
name = other.name; // 3. 如果异常发生,对象处于无效状态
return *this;
}
~ExceptionUnsafe() {
delete data;
}
};
2.4 资源泄漏问题
// ❌ 资源泄漏的拷贝操作
class ResourceLeak {
private:
FILE* file;
int* data;
public:
ResourceLeak(const char* filename) {
file = fopen(filename, "r");
data = new int(100);
}
// 错误:没有定义拷贝构造函数和赋值运算符
// 但编译器会生成浅拷贝版本
~ResourceLeak() {
if (file) fclose(file);
delete data;
}
};
void resource_leak_demo() {
ResourceLeak original("test.txt");
ResourceLeak copy = original; // 浅拷贝
// original和copy析构时都会尝试关闭同一个FILE* → 未定义行为
// 并且其中一个data指针会泄漏
}
3. 解决方案与最佳实践
3.1 规则三(Rule of Three)
如果一个类需要自定义以下任何一个,那么它通常需要全部三个:
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符
// ✅ 遵循规则三的正确实现
class RuleOfThree {
private:
char* buffer;
size_t size;
// 辅助函数:深拷贝
void copyFrom(const RuleOfThree& other) {
size = other.size;
buffer = new char[size + 1];
strcpy(buffer, other.buffer);
}
// 辅助函数:清理资源
void cleanup() {
delete[] buffer;
buffer = nullptr;
size = 0;
}
public:
// 构造函数
RuleOfThree(const char* str = "") {
size = strlen(str);
buffer = new char[size + 1];
strcpy(buffer, str);
}
// 拷贝构造函数
RuleOfThree(const RuleOfThree& other) {
copyFrom(other);
}
// 拷贝赋值运算符
RuleOfThree& operator=(const RuleOfThree& other) {
if (this != &other) { // 自赋值检查
cleanup(); // 释放原有资源
copyFrom(other); // 深拷贝新资源
}
return *this;
}
// 析构函数
~RuleOfThree() {
cleanup();
}
// 移动构造函数(C++11)
RuleOfThree(RuleOfThree&& other) noexcept
: buffer(other.buffer), size(other.size) {
other.buffer = nullptr;
other.size = 0;
}
// 移动赋值运算符(C++11)
RuleOfThree& operator=(RuleOfThree&& other) noexcept {
if (this != &other) {
cleanup();
buffer = other.buffer;
size = other.size;
other.buffer = nullptr;
other.size = 0;
}
return *this;
}
};
3.2 拷贝并交换惯用法(Copy-and-Swap)
#include <utility>
// ✅ 使用拷贝并交换惯用法
class CopyAndSwap {
private:
char* buffer;
size_t size;
// 交换函数
friend void swap(CopyAndSwap& first, CopyAndSwap& second) noexcept {
using std::swap;
swap(first.buffer, second.buffer);
swap(first.size, second.size);
}
public:
// 构造函数
CopyAndSwap(const char* str = "") : buffer(nullptr), size(0) {
if (str) {
size = strlen(str);
buffer = new char[size + 1];
strcpy(buffer, str);
}
}
// 拷贝构造函数
CopyAndSwap(const CopyAndSwap& other)
: buffer(nullptr), size(0) {
if (other.buffer) {
size = other.size;
buffer = new char[size + 1];
strcpy(buffer, other.buffer);
}
}
// 赋值运算符:拷贝并交换(异常安全且处理自赋值)
CopyAndSwap& operator=(CopyAndSwap other) { // 按值传递!调用拷贝构造函数
swap(*this, other); // 交换当前对象和临时对象
return *this;
// other离开作用域,自动清理旧资源
}
// 移动构造函数
CopyAndSwap(CopyAndSwap&& other) noexcept
: CopyAndSwap() { // 委托默认构造
swap(*this, other);
}
// 注意:不需要显式定义移动赋值运算符,因为按值传递的赋值运算符已经处理了
~CopyAndSwap() {
delete[] buffer;
}
};
3.3 使用RAII和智能指针
#include <memory>
#include <vector>
// ✅ 使用RAII和智能指针,避免手动资源管理
class RAIIBased {
private:
std::unique_ptr<char[]> buffer; // 自动管理内存
std::vector<int> data; // 自动管理内存
std::string name; // 自动管理内存
public:
// 构造函数
RAIIBased(const char* str, const std::string& n)
: name(n) {
if (str) {
size_t len = strlen(str);
buffer = std::make_unique<char[]>(len + 1);
strcpy(buffer.get(), str);
}
data = {1, 2, 3, 4, 5};
}
// 不需要自定义拷贝构造、赋值运算符、析构函数!
// 编译器生成的版本会正确拷贝/移动所有成员
// 显式默认特殊成员函数(文档化意图)
RAIIBased(const RAIIBased&) = default;
RAIIBased& operator=(const RAIIBased&) = default;
RAIIBased(RAIIBased&&) = default;
RAIIBased& operator=(RAIIBased&&) = default;
~RAIIBased() = default;
// 自定义拷贝语义(如果需要)
RAIIBased deepCopy() const {
RAIIBased copy = *this; // 浅拷贝
// 对需要深拷贝的成员进行额外处理
if (buffer) {
size_t len = strlen(buffer.get());
copy.buffer = std::make_unique<char[]>(len + 1);
strcpy(copy.buffer.get(), buffer.get());
}
return copy;
}
};
3.4 不可拷贝类
// ✅ 明确禁止拷贝的类
class NonCopyable {
private:
std::unique_ptr<int> resource;
public:
NonCopyable(int value = 0) : resource(std::make_unique<int>(value)) {}
// 明确删除拷贝操作
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
// 允许移动操作
NonCopyable(NonCopyable&&) = default;
NonCopyable& operator=(NonCopyable&&) = default;
~NonCopyable() = default;
// 提供访问接口
int getValue() const { return *resource; }
void setValue(int value) { *resource = value; }
};
// 使用继承的方式(传统方法)
class TraditionalNonCopyable {
protected:
TraditionalNonCopyable() = default;
~TraditionalNonCopyable() = default;
// 禁止拷贝
TraditionalNonCopyable(const TraditionalNonCopyable&) = delete;
TraditionalNonCopyable& operator=(const TraditionalNonCopyable&) = delete;
// 允许移动
TraditionalNonCopyable(TraditionalNonCopyable&&) = default;
TraditionalNonCopyable& operator=(TraditionalNonCopyable&&) = default;
};
class Derived : private TraditionalNonCopyable {
// 自动获得非拷贝语义
};
4. 现代C++解决方案(规则五/规则零)
4.1 规则五(Rule of Five)
C++11引入移动语义后,规则三扩展为规则五:
// ✅ 遵循规则五的完整实现
class RuleOfFive {
private:
std::unique_ptr<int[]> data;
size_t size;
public:
// 构造函数
RuleOfFive(size_t s = 0) : size(s) {
if (size > 0) {
data = std::make_unique<int[]>(size);
}
}
// 1. 拷贝构造函数
RuleOfFive(const RuleOfFive& other) : size(other.size) {
if (size > 0) {
data = std::make_unique<int[]>(size);
std::copy(other.data.get(), other.data.get() + size, data.get());
}
}
// 2. 拷贝赋值运算符
RuleOfFive& operator=(const RuleOfFive& other) {
if (this != &other) {
auto temp = std::make_unique<int[]>(other.size);
if (other.size > 0) {
std::copy(other.data.get(), other.data.get() + other.size, temp.get());
}
data = std::move(temp);
size = other.size;
}
return *this;
}
// 3. 移动构造函数
RuleOfFive(RuleOfFive&& other) noexcept
: data(std::move(other.data)), size(other.size) {
other.size = 0;
}
// 4. 移动赋值运算符
RuleOfFive& operator=(RuleOfFive&& other) noexcept {
if (this != &other) {
data = std::move(other.data);
size = other.size;
other.size = 0;
}
return *this;
}
// 5. 析构函数
~RuleOfFive() = default; // unique_ptr自动清理
};
4.2 规则零(Rule of Zero)
理想情况下,类不应该自定义拷贝/移动操作,而应该依赖编译器生成的版本:
// ✅ 遵循规则零的理想类
class RuleOfZero {
private:
std::vector<int> data; // 自动管理内存
std::string name; // 自动管理内存
std::shared_ptr<Config> config; // 自动管理内存
public:
RuleOfZero(std::string n, std::initializer_list<int> init)
: name(std::move(n)), data(init) {}
// 不需要任何特殊成员函数的自定义版本!
// 编译器生成的拷贝/移动/析构都是正确的
void print() const {
std::cout << name << ": ";
for (int val : data) {
std::cout << val << " ";
}
std::cout << std::endl;
}
};
5. 特定场景的解决方案
5.1 多态基类的拷贝问题
// ✅ 多态基类的正确设计
class CloneableBase {
public:
virtual ~CloneableBase() = default;
// 禁止拷贝,但提供克隆接口
CloneableBase(const CloneableBase&) = delete;
CloneableBase& operator=(const CloneableBase&) = delete;
// 允许移动
CloneableBase(CloneableBase&&) = default;
CloneableBase& operator=(CloneableBase&&) = default;
// 克隆接口
virtual std::unique_ptr<CloneableBase> clone() const = 0;
};
class DerivedType : public CloneableBase {
private:
std::vector<int> data;
std::string name;
public:
DerivedType(std::string n, std::vector<int> d)
: name(std::move(n)), data(std::move(d)) {}
std::unique_ptr<CloneableBase> clone() const override {
return std::make_unique<DerivedType>(*this);
}
// 注意:DerivedType需要可拷贝,但基类不可拷贝
DerivedType(const DerivedType&) = default;
DerivedType& operator=(const DerivedType&) = default;
DerivedType(DerivedType&&) = default;
DerivedType& operator=(DerivedType&&) = default;
};
5.2 包含不可拷贝成员的处理
// ✅ 处理包含不可拷贝成员的类
class WithUniqueMember {
private:
std::unique_ptr<int> unique_data;
std::vector<int> copyable_data;
public:
WithUniqueMember(int value, std::vector<int> data)
: unique_data(std::make_unique<int>(value)), copyable_data(std::move(data)) {}
// 拷贝构造函数:需要特殊处理unique_ptr
WithUniqueMember(const WithUniqueMember& other)
: unique_data(other.unique_data ? std::make_unique<int>(*other.unique_data) : nullptr),
copyable_data(other.copyable_data) {}
// 拷贝赋值运算符
WithUniqueMember& operator=(const WithUniqueMember& other) {
if (this != &other) {
if (other.unique_data) {
unique_data = std::make_unique<int>(*other.unique_data);
} else {
unique_data.reset();
}
copyable_data = other.copyable_data;
}
return *this;
}
// 移动操作可以使用默认版本
WithUniqueMember(WithUniqueMember&&) = default;
WithUniqueMember& operator=(WithUniqueMember&&) = default;
~WithUniqueMember() = default;
};
6. 测试和验证
6.1 拷贝语义测试
#include <cassert>
void test_copy_semantics() {
// 测试拷贝构造
{
RuleOfThree original("Hello");
RuleOfThree copy = original; // 拷贝构造
assert(copy.getSize() == original.getSize());
// 应该有不同的内存地址
assert(copy.getBuffer() != original.getBuffer());
}
// 测试赋值运算符
{
RuleOfThree a("Hello");
RuleOfThree b("World");
b = a; // 赋值运算
assert(b.getSize() == a.getSize());
assert(b.getBuffer() != a.getBuffer()); // 深拷贝
}
// 测试自赋值
{
RuleOfThree obj("Test");
obj = obj; // 自赋值应该安全
assert(obj.getSize() == 4); // "Test"的长度
}
// 测试异常安全
{
ExceptionSafe obj("Test", 100);
try {
// 模拟在赋值过程中发生异常
obj = ExceptionSafe("", 200); // 如果实现正确,应该保持有效状态
assert(obj.isValid()); // 应该仍然有效
} catch (...) {
assert(obj.isValid()); // 即使异常,也应该保持有效
}
}
std::cout << "All copy semantics tests passed!" << std::endl;
}
6.2 性能测试
#include <chrono>
void benchmark_copy_vs_move() {
const int iterations = 100000;
std::vector<RuleOfFive> objects;
objects.reserve(iterations);
// 测试拷贝性能
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
RuleOfFive obj(1000);
objects.push_back(obj); // 拷贝
}
auto copy_time = std::chrono::high_resolution_clock::now() - start;
objects.clear();
// 测试移动性能
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
RuleOfFive obj(1000);
objects.push_back(std::move(obj)); // 移动
}
auto move_time = std::chrono::high_resolution_clock::now() - start;
std::cout << "Copy time: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(copy_time).count()
<< " ms" << std::endl;
std::cout << "Move time: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(move_time).count()
<< " ms" << std::endl;
}
7. 最佳实践总结
7.1 决策流程
// 拷贝语义设计决策树
class CopyDesignGuide {
public:
/*
设计决策流程:
1. 类是否管理资源?
│
├─ 否 → 使用规则零,依赖编译器生成的版本
│
└─ 是 →
│
├─ 2. 资源是否应该共享?
│ │
│ ├─ 是 → 使用shared_ptr,遵循规则零
│ │
│ └─ 否 →
│ │
│ ├─ 3. 类是否应该可拷贝?
│ │ │
│ │ ├─ 否 → 删除拷贝操作,只允许移动(规则五)
│ │ │
│ │ └─ 是 → 实现完整的规则五,使用拷贝并交换惯用法
│ │
│ └─ 4. 考虑使用RAII包装器替代手动管理
*/
};
7.2 代码模板
// 可拷贝类的模板
template<typename T>
class CopyableTemplate {
private:
std::vector<T> data;
std::string name;
// 交换函数
friend void swap(CopyableTemplate& first, CopyableTemplate& second) noexcept {
using std::swap;
swap(first.data, second.data);
swap(first.name, second.name);
}
public:
// 构造函数
CopyableTemplate(std::string n, std::vector<T> d)
: name(std::move(n)), data(std::move(d)) {}
// 拷贝构造函数
CopyableTemplate(const CopyableTemplate&) = default;
// 拷贝赋值运算符(拷贝并交换)
CopyableTemplate& operator=(CopyableTemplate other) {
swap(*this, other);
return *this;
}
// 移动构造函数
CopyableTemplate(CopyableTemplate&&) = default;
// 移动赋值运算符
CopyableTemplate& operator=(CopyableTemplate&&) = default;
// 析构函数
~CopyableTemplate() = default;
};
通过遵循这些模式和最佳实践,可以避免拷贝构造和赋值运算符的常见陷阱,创建出安全、高效且易于维护的C++类。关键是根据类的具体需求选择合适的策略:优先使用规则零,必要时使用规则五,并始终考虑异常安全和资源管理。
2829

被折叠的 条评论
为什么被折叠?



