Effective C++ 条款14:在资源管理类中小心 copying 行为
Effective C++ 条款14:在资源管理类中小心 copying 行为
复制 RAII 对象必须一并复制它所管理的资源,所以资源的 copying 行为决定了 RAII 对象的 copying 行为。普遍的 RAII class copying 行为是:抑制 copying、引用计数、深度拷贝、转移底部资源拥有权。
一、问题的核心
当我们设计一个 RAII(资源获取即初始化)类时,一个关键问题浮现出来:
当 RAII 对象被复制时,它管理的资源应该如何处理?
资源的不同特性决定了不同的 copying 策略。让我们通过一个文件句柄管理类来理解这个问题:
class FileHandle {
public:
explicit FileHandle(const char* filename)
: fd_(fopen(filename, "r")) {
if (!fd_) throw std::runtime_error("Failed to open file");
}
~FileHandle() { if (fd_) fclose(fd_); }
// 问题来了:拷贝构造函数和赋值运算符该怎么实现?
// FileHandle(const FileHandle& rhs); // ???
// FileHandle& operator=(const FileHandle& rhs); // ???
private:
FILE* fd_;
};
如果我们不定义 copying 函数,编译器会生成默认版本,执行浅拷贝——两个 FileHandle 对象将持有同一个 FILE*。当它们析构时,同一个文件句柄会被 fclose 两次,导致未定义行为!
二、四种普遍的 copying 策略
策略一:抑制 copying(Prohibit Copying)
适用场景:资源不应该被复制,如文件句柄、锁、网络连接等。
class FileHandle {
public:
explicit FileHandle(const char* filename)
: fd_(fopen(filename, "r")) {
if (!fd_) throw std::runtime_error("Failed to open file");
}
~FileHandle() { if (fd_) fclose(fd_); }
// 显式删除拷贝操作
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// 允许移动(C++11)
FileHandle(FileHandle&& other) noexcept
: fd_(other.fd_) {
other.fd_ = nullptr;
}
FileHandle& operator=(FileHandle&& other) noexcept {
if (this != &other) {
if (fd_) fclose(fd_);
fd_ = other.fd_;
other.fd_ = nullptr;
}
return *this;
}
private:
FILE* fd_;
};
特点:
| 特性 | 说明 |
|---|---|
| 拷贝 | 禁止 |
| 移动 | 允许(转移所有权) |
| 资源释放 | 只有一个所有者负责释放 |
| 类似标准类 | std::unique_ptr |
策略二:引用计数(Reference Counting)
适用场景:资源可以被多个对象共享,直到最后一个使用者释放它。
#include <memory>
class SharedResource {
public:
explicit SharedResource(int* data)
: data_(data), ref_count_(new size_t(1)) {}
// 拷贝构造函数 - 增加引用计数
SharedResource(const SharedResource& rhs)
: data_(rhs.data_), ref_count_(rhs.ref_count_) {
++(*ref_count_);
}
// 赋值运算符
SharedResource& operator=(const SharedResource& rhs) {
if (this != &rhs) {
release(); // 释放当前资源
data_ = rhs.data_;
ref_count_ = rhs.ref_count_;
++(*ref_count_);
}
return *this;
}
~SharedResource() { release(); }
size_t use_count() const { return *ref_count_; }
private:
void release() {
if (--(*ref_count_) == 0) {
delete data_;
delete ref_count_;
}
}
int* data_;
size_t* ref_count_;
};
现代 C++ 简化版:
#include <memory>
class ModernShared {
public:
explicit ModernShared(int value)
: data_(std::make_shared<int>(value)) {}
// 编译器生成的拷贝构造函数和赋值运算符
// 自动处理 shared_ptr 的引用计数
ModernShared(const ModernShared&) = default;
ModernShared& operator=(const ModernShared&) = default;
size_t use_count() const { return data_.use_count(); }
private:
std::shared_ptr<int> data_;
};
特点:
| 特性 | 说明 |
|---|---|
| 拷贝 | 允许,引用计数 +1 |
| 资源释放 | 最后一个对象析构时释放 |
| 线程安全 | 引用计数操作需要同步 |
| 类似标准类 | std::shared_ptr |
策略三:深度拷贝(Deep Copy)
适用场景:每个对象应该拥有资源的独立副本,如字符串、容器等。
class DeepCopyBuffer {
public:
explicit DeepCopyBuffer(size_t size)
: size_(size), data_(new char[size]) {}
// 拷贝构造函数 - 深拷贝
DeepCopyBuffer(const DeepCopyBuffer& rhs)
: size_(rhs.size_), data_(new char[rhs.size_]) {
std::copy(rhs.data_, rhs.data_ + rhs.size_, data_);
}
// 赋值运算符
DeepCopyBuffer& operator=(const DeepCopyBuffer& rhs) {
if (this != &rhs) {
// copy-and-swap 惯用法
DeepCopyBuffer temp(rhs);
swap(temp);
}
return *this;
}
~DeepCopyBuffer() { delete[] data_; }
void swap(DeepCopyBuffer& other) noexcept {
using std::swap;
swap(size_, other.size_);
swap(data_, other.data_);
}
private:
size_t size_;
char* data_;
};
特点:
| 特性 | 说明 |
|---|---|
| 拷贝 | 允许,创建资源副本 |
| 独立性 | 每个对象有自己的资源副本 |
| 开销 | 拷贝成本较高 |
| 类似标准类 | std::string, std::vector |
策略四:转移底部资源拥有权(Transfer Ownership)
适用场景:只有一个对象应该拥有资源,拷贝时转移所有权给新对象(旧对象不再拥有)。
C++11 之前
std::auto_ptr采用此策略,现已被std::unique_ptr取代。
// C++98 风格的 auto_ptr(已废弃,仅供理解)
template<typename T>
class AutoPtr {
public:
explicit AutoPtr(T* ptr = nullptr) : ptr_(ptr) {}
// 拷贝构造函数 - 转移所有权!
AutoPtr(AutoPtr& rhs) : ptr_(rhs.ptr_) {
rhs.ptr_ = nullptr; // 原对象失去所有权
}
// 赋值运算符 - 转移所有权
AutoPtr& operator=(AutoPtr& rhs) {
if (this != &rhs) {
delete ptr_;
ptr_ = rhs.ptr_;
rhs.ptr_ = nullptr;
}
return *this;
}
~AutoPtr() { delete ptr_; }
T* get() const { return ptr_; }
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
private:
T* ptr_;
};
问题:这种"拷贝"语义非常反直觉——拷贝后原对象被修改了!这导致了很多 bug,因此 std::auto_ptr 已被废弃。
现代 C++ 的正确做法:使用移动语义
#include <memory>
// C++11 起使用 unique_ptr + 移动语义
std::unique_ptr<int> ptr1(new int(42));
// std::unique_ptr<int> ptr2 = ptr1; // 错误!不能拷贝
std::unique_ptr<int> ptr2 = std::move(ptr1); // 正确!显式转移
// 现在 ptr1 为 nullptr,ptr2 拥有资源
三、四种策略对比
| 策略 | 拷贝语义 | 资源释放时机 | 适用场景 | 标准库对应 |
|---|---|---|---|---|
| 抑制 copying | 禁止 | 唯一所有者析构 | 文件句柄、锁 | std::unique_ptr |
| 引用计数 | 共享 | 最后一个析构 | 共享数据、缓存 | std::shared_ptr |
| 深度拷贝 | 独立副本 | 各自析构 | 字符串、容器 | std::string |
| 转移所有权 | 移动(C++11) | 新所有者析构 | 独占资源转移 | std::unique_ptr(移动) |
四、实际应用场景
场景1:互斥锁(抑制 copying)
#include <mutex>
class LockGuard {
public:
explicit LockGuard(std::mutex& m) : mutex_(m) { mutex_.lock(); }
~LockGuard() { mutex_.unlock(); }
// 锁不能被复制
LockGuard(const LockGuard&) = delete;
LockGuard& operator=(const LockGuard&) = delete;
private:
std::mutex& mutex_;
};
// 使用
std::mutex mtx;
void threadSafeFunction() {
LockGuard lock(mtx); // 加锁
// ... 临界区 ...
} // 自动解锁
场景2:图像共享缓存(引用计数)
#include <memory>
class ImageCache {
public:
using ImagePtr = std::shared_ptr<class Image>;
ImagePtr loadImage(const std::string& path) {
auto it = cache_.find(path);
if (it != cache_.end()) {
return it->second; // 返回共享的图像
}
auto img = std::make_shared<Image>(path);
cache_[path] = img;
return img;
}
void clearUnused() {
for (auto it = cache_.begin(); it != cache_.end(); ) {
if (it->second.use_count() == 1) { // 只有缓存持有
it = cache_.erase(it);
} else {
++it;
}
}
}
private:
std::map<std::string, ImagePtr> cache_;
};
场景3:矩阵类(深度拷贝)
class Matrix {
public:
Matrix(size_t rows, size_t cols)
: rows_(rows), cols_(cols), data_(new double[rows * cols]()) {}
// 深拷贝
Matrix(const Matrix& rhs)
: rows_(rhs.rows_), cols_(rhs.cols_),
data_(new double[rhs.rows_ * rhs.cols_]) {
std::copy(rhs.data_, rhs.data_ + rows_ * cols_, data_);
}
Matrix& operator=(const Matrix& rhs) {
if (this != &rhs) {
Matrix temp(rhs);
swap(temp);
}
return *this;
}
// 移动构造(C++11)
Matrix(Matrix&& other) noexcept
: rows_(other.rows_), cols_(other.cols_), data_(other.data_) {
other.data_ = nullptr;
other.rows_ = other.cols_ = 0;
}
~Matrix() { delete[] data_; }
void swap(Matrix& other) noexcept {
using std::swap;
swap(rows_, other.rows_);
swap(cols_, other.cols_);
swap(data_, other.data_);
}
private:
size_t rows_, cols_;
double* data_;
};
场景4:网络连接池(移动语义)
class NetworkConnection {
public:
explicit NetworkConnection(const std::string& endpoint);
~NetworkConnection() { if (socket_ != -1) close(socket_); }
// 禁止拷贝
NetworkConnection(const NetworkConnection&) = delete;
NetworkConnection& operator=(const NetworkConnection&) = delete;
// 允许移动
NetworkConnection(NetworkConnection&& other) noexcept
: socket_(other.socket_), endpoint_(std::move(other.endpoint_)) {
other.socket_ = -1;
}
NetworkConnection& operator=(NetworkConnection&& other) noexcept {
if (this != &other) {
if (socket_ != -1) close(socket_);
socket_ = other.socket_;
endpoint_ = std::move(other.endpoint_);
other.socket_ = -1;
}
return *this;
}
void send(const std::vector<uint8_t>& data);
private:
int socket_;
std::string endpoint_;
};
// 连接池返回可移动但不可拷贝的连接
class ConnectionPool {
public:
NetworkConnection acquire() {
if (!available_.empty()) {
auto conn = std::move(available_.back());
available_.pop_back();
return conn; // 移动返回
}
return NetworkConnection("default");
}
void release(NetworkConnection conn) {
available_.push_back(std::move(conn));
}
private:
std::vector<NetworkConnection> available_;
};
五、设计决策流程
当你设计 RAII 类时,按以下流程选择 copying 策略:
资源是否可被共享?
├── 否 → 资源是否可拷贝?
│ ├── 否 → 抑制 copying,提供移动语义(如文件句柄)
│ └── 是 → 深度拷贝(如字符串、矩阵)
└── 是 → 引用计数(如共享缓存、图像资源)
六、常见陷阱
陷阱1:忘记处理 copying 函数
class BadRAII {
public:
explicit BadRAII(int* p) : ptr_(p) {}
~BadRAII() { delete ptr_; }
// 糟糕!没有定义拷贝构造函数和赋值运算符
// 编译器生成的默认版本会浅拷贝,导致 double free
private:
int* ptr_;
};
BadRAII a(new int(42));
BadRAII b = a; // 两个对象持有同一指针!
// a 和 b 析构时都会 delete 同一个指针 → UB!
陷阱2:混合使用不同策略
// 不要这样做:有时深拷贝,有时浅拷贝
class Confusing {
public:
Confusing(const Confusing& rhs) {
if (some_condition) {
// 深拷贝
} else {
// 浅拷贝?
}
}
};
陷阱3:在引用计数中忘记处理自我赋值
SharedResource& operator=(const SharedResource& rhs) {
// 错误!没有处理自我赋值
release();
data_ = rhs.data_;
ref_count_ = rhs.ref_count_;
++(*ref_count_);
return *this;
}
// 正确做法
SharedResource& operator=(const SharedResource& rhs) {
if (this != &rhs) {
release();
data_ = rhs.data_;
ref_count_ = rhs.ref_count_;
++(*ref_count_);
}
return *this;
}
七、现代 C++ 最佳实践
1. 优先使用标准库智能指针
// 独占所有权
std::unique_ptr<Resource> exclusive;
// 共享所有权
std::shared_ptr<Resource> shared;
// 弱引用
std::weak_ptr<Resource> weak;
2. 使用 Rule of Zero / Rule of Five
// Rule of Zero:如果类只包含可以正确管理的成员,让编译器生成所有函数
class ModernClass {
std::unique_ptr<int> data_; // 编译器生成的拷贝/移动/析构都正确
};
// Rule of Five:如果需要自定义其中一个,通常需要定义全部五个
class CustomClass {
public:
CustomClass(); // 默认构造
~CustomClass(); // 析构
CustomClass(const CustomClass&); // 拷贝构造
CustomClass& operator=(const CustomClass&); // 拷贝赋值
CustomClass(CustomClass&&) noexcept; // 移动构造
CustomClass& operator=(CustomClass&&) noexcept; // 移动赋值
};
3. 使用显式默认和删除
class ExplicitControl {
public:
ExplicitControl() = default;
~ExplicitControl() = default;
ExplicitControl(const ExplicitControl&) = delete; // 禁止拷贝
ExplicitControl& operator=(const ExplicitControl&) = delete;
ExplicitControl(ExplicitControl&&) = default; // 允许移动
ExplicitControl& operator=(ExplicitControl&&) = default;
};
八、总结
请记住:
- 复制 RAII 对象必须一并复制它所管理的资源
- 资源的 copying 行为决定了 RAII 对象的 copying 行为
- 普遍的 RAII class copying 行为:
- 抑制 copying:资源不可共享(
std::unique_ptr)- 引用计数:资源可共享(
std::shared_ptr)- 深度拷贝:每个对象需要独立副本(
std::string)- 转移所有权:C++11 移动语义(
std::unique_ptr移动)- 优先使用标准库智能指针,避免重复造轮子
RAII 类的 copying 行为设计是 C++ 资源管理的核心。选择正确的策略不仅能保证程序正确性,还能提升代码的可维护性和性能。现代 C++ 的标准库已经提供了成熟的解决方案,理解这些策略的原理,才能在需要自定义资源管理类时做出正确的设计决策。
参考阅读:
- 《Effective C++》第3版,Scott Meyers,条款14
- 《Effective Modern C++》,Scott Meyers
- C++ Core Guidelines: C.21 (Rule of Zero/Five)
- cppreference.com: 拷贝控制、移动语义
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)