第2章:构造/析构/赋值(Constructors, Destructors, and Assignment Operators)
一、导读
1.1 本章学习目标
第2章深入讲解 C++ 对象生命周期的核心:构造函数、析构函数和赋值操作符。Meyers 在此给出了许多经典的建议,帮助读者避免常见的陷阱。
本章核心学习目标:
- 掌握拷贝构造和拷贝赋值的区别:什么时候调用哪个?
- 理解 Rule of Three/Five/Zero:什么时候需要自定义这些函数?
- 掌握移动语义:C++11 的移动构造和移动赋值
- 理解虚析构函数的必要性:为什么基类需要虚析构?
- 掌握异常安全的构造函数:构造失败时如何清理资源?
1.2 为什么本章如此重要
对象的创建和销毁是 C++ 中最基本的操作。错误的拷贝语义会导致资源泄漏、双重释放等严重问题。理解这些概念是写出正确 C++ 代码的基础。
1.3 核心问题引导
拷贝构造和拷贝赋值在什么时候被调用?
为什么含有指针成员的类通常需要自定义拷贝操作?
移动语义如何避免不必要的拷贝?
二、核心概念详解
2.1 拷贝构造 vs 拷贝赋值
cpp
class Widget {
std::string name;
int* data;
size_t size;
public:
// 拷贝构造: 从另一个对象创建新对象
Widget(const Widget& other)
: name(other.name), size(other.size) {
data = new int[size];
std::copy(other.data, other.data + size, data);
}
// 拷贝赋值: 将另一个对象的值赋给已有对象
Widget& operator=(const Widget& other) {
if (this != &other) { // 自赋值检查
delete[] data; // 释放旧资源
name = other.name;
size = other.size;
data = new int[size];
std::copy(other.data, other.data + size, data);
}
return *this;
}
};
Widget w1("hello");
Widget w2(w1); // 拷贝构造
Widget w3 = w1; // 拷贝构造(不是赋值!)
w3 = w1; // 拷贝赋值2.2 Rule of Three / Five / Zero
cpp
// Rule of Three: 如果自定义了以下之一,通常需要自定义全部三个
// 1. 析构函数
// 2. 拷贝构造函数
// 3. 拷贝赋值操作符
// Rule of Five (C++11): 加上移动语义
// 4. 移动构造函数
// 5. 移动赋值操作符
// Rule of Zero: 最好的做法是不需要自定义
// 使用智能指针和标准容器管理资源
class ModernWidget {
std::string name; // 自动管理
std::vector<int> data; // 自动管理
std::unique_ptr<Resource> resource; // 自动管理
// 不需要自定义任何特殊成员函数!
};
// 使用智能指针实现深拷贝
class SafeWidget {
std::unique_ptr<int[]> data;
size_t size;
public:
SafeWidget(size_t n) : data(new int[n]()), size(n) {}
// 拷贝构造
SafeWidget(const SafeWidget& other)
: data(new int[other.size]), size(other.size) {
std::copy(other.data.get(), other.data.get() + size, data.get());
}
// 移动构造
SafeWidget(SafeWidget&& other) noexcept
: data(std::move(other.data)), size(other.size) {
other.size = 0;
}
};2.3 虚析构函数
cpp
// 错误: 基类没有虚析构
class Base {
std::string name;
public:
Base(std::string n) : name(n) {}
~Base() { /* 清理 Base 资源 */ }
};
class Derived : public Base {
int* data;
public:
Derived(std::string n, int size) : Base(n), data(new int[size]) {}
~Derived() { delete[] data; /* 清理 Derived 资源 */ }
};
Base* ptr = new Derived("hello", 100);
delete ptr; // 只调用 ~Base(),不调用 ~Derived()!
// data 泄漏!
// 正确: 基类使用虚析构
class GoodBase {
std::string name;
public:
GoodBase(std::string n) : name(n) {}
virtual ~GoodBase() = default; // 虚析构
};
class GoodDerived : public GoodBase {
int* data;
public:
GoodDerived(std::string n, int size) : GoodBase(n), data(new int[size]) {}
~GoodDerived() override { delete[] data; }
};
GoodBase* ptr2 = new GoodDerived("hello", 100);
delete ptr2; // 正确调用 ~GoodDerived() 然后 ~GoodBase()2.4 移动语义
cpp
// 移动构造: 窃取资源而非复制
class Buffer {
std::unique_ptr<int[]> data;
size_t size;
public:
Buffer(size_t n) : data(new int[n]), size(n) {}
// 移动构造
Buffer(Buffer&& other) noexcept
: data(std::move(other.data)), size(other.size) {
other.size = 0;
// other.data 现在是 nullptr
}
// 移动赋值
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
data = std::move(other.data);
size = other.size;
other.size = 0;
}
return *this;
}
};
Buffer createBuffer() {
Buffer b(1000000);
return b; // 移动构造(或 NRVO 直接构造)
}
Buffer b1(100);
Buffer b2 = std::move(b1); // b1 被移动,b1.data 变为 nullptr
// b1 现在处于"有效但未指定"状态三、实战案例
3.1 案例一:实现异常安全的赋值操作符
cpp
class Image {
std::string filename;
std::unique_ptr<char[]> pixels;
size_t width, height;
void swap(Image& other) noexcept {
using std::swap;
swap(filename, other.filename);
swap(pixels, other.pixels);
swap(width, other.width);
swap(height, other.height);
}
public:
Image(std::string fn, size_t w, size_t h)
: filename(std::move(fn)), pixels(new char[w * h]()),
width(w), height(h) {}
// 异常安全的赋值: copy-and-swap 惯用法
Image& operator=(Image other) { // 按值传递(触发拷贝/移动构造)
swap(other); // 交换资源
return *this;
// other 在函数结束时析构,释放旧资源
}
};
// 为什么这是异常安全的?
// 1. 参数 other 通过值传递,在函数体外完成拷贝
// 如果拷贝抛出异常,当前对象不受影响
// 2. swap 不抛出异常(noexcept)
// 3. 旧资源在 other 析构时自动释放四、本章小结
拷贝构造创建新对象,拷贝赋值修改已有对象
Rule of Zero:优先使用智能指针和标准容器,避免自定义特殊成员函数
虚析构是基类的必备,否则通过基类指针删除派生类对象会导致资源泄漏
移动语义通过窃取资源避免不必要的拷贝,大幅提升性能
五、思考与练习
思考题
为什么移动操作应该标记为 noexcept?
copy-and-swap 惯用法为什么是异常安全的?
编程练习
实现一个字符串类,包含完整的 Rule of Five 成员函数。
实现一个智能指针类(类似 unique_ptr),支持移动语义。