文章目录
-
- 条款13:以对象管理资源
- [条款14:在资源管理类中小心 copying 行为](#条款14:在资源管理类中小心 copying 行为)
-
- 说明与介绍
- 代码对比示例
- 运行结果示例
- 关键要点解析
-
- [1. 四种复制策略对比](#1. 四种复制策略对比)
- [2. 错误示例的问题](#2. 错误示例的问题)
- [3. 策略1:禁止复制(适用于互斥锁、文件句柄)](#3. 策略1:禁止复制(适用于互斥锁、文件句柄))
- [4. 策略2:引用计数(使用 `shared_ptr`)](#4. 策略2:引用计数(使用
shared_ptr)) - [5. 策略3:深拷贝(适用于独立资源)](#5. 策略3:深拷贝(适用于独立资源))
- [6. 策略4:转移所有权(移动语义)](#6. 策略4:转移所有权(移动语义))
- [7. 现代 C++ 的最佳实践](#7. 现代 C++ 的最佳实践)
- 总结
- 条款15:在资源管理类中提供对原始资源的访问
- [条款16:成对使用 `new` 和 `delete` 时要采取相同形式](#条款16:成对使用
new和delete时要采取相同形式) -
- 说明与介绍
- 代码对比示例
- 运行结果示例(危险部分可能因编译器而不同)
- 关键要点解析
-
- [1. 错误示例的后果](#1. 错误示例的后果)
- [2. 为什么编译器不报错?](#2. 为什么编译器不报错?)
- [3. `typedef` 陷阱](#3.
typedef陷阱) - [4. 析构函数调用差异](#4. 析构函数调用差异)
- [5. 现代 C++ 的替代方案](#5. 现代 C++ 的替代方案)
- [6. 最佳实践总结](#6. 最佳实践总结)
- 总结
- [条款17:以独立语句将 `new` 出的对象置入智能指针](#条款17:以独立语句将
new出的对象置入智能指针)
条款13:以对象管理资源
说明与介绍
核心思想 :动态分配的资源(如堆内存、文件句柄、锁等)必须被妥善管理,避免泄漏。C++ 中最好的做法是使用对象 来管理资源,利用对象的构造函数获取资源,析构函数释放资源。这就是 RAII(Resource Acquisition Is Initialization) 惯用法。
为什么需要以对象管理资源?
- 手动
new/delete容易遗漏delete,尤其在函数早期返回或抛出异常时。 - 资源泄漏会导致内存耗尽、文件句柄不足、死锁等严重问题。
- 使用对象包装资源,可以自动调用析构函数释放资源,无论控制流如何退出作用域。
关键做法:
- 智能指针 :C++11 起使用
std::unique_ptr(独占所有权)和std::shared_ptr(共享所有权)。C++98 可使用std::auto_ptr(已废弃)或boost::shared_ptr。 - RAII 类:自定义资源管理类,在构造函数中获取资源,析构函数中释放资源。
- 资源管理类通常禁止拷贝 (见条款06),或实现引用计数(如
shared_ptr)。
注意事项:
- 永远不要手动
delete原始指针,应交给智能指针。 - 优先使用
std::make_unique/std::make_shared创建智能指针,更安全高效。 - 如果使用
new初始化智能指针,应将new的结果直接放入智能指针构造函数,避免在独立语句中(见条款17)。
代码对比示例
cpp
// 文件名: clause13.cpp
// 编译: g++ -std=c++11 clause13.cpp -o clause13
#include <iostream>
#include <memory>
#include <stdexcept>
using namespace std;
// ========== 模拟一个投资类 ==========
class Investment {
public:
Investment(const string& name) : name(name) {
cout << "Investment 构造: " << name << endl;
}
~Investment() {
cout << "Investment 析构: " << name << endl;
}
void display() const {
cout << "投资产品: " << name << endl;
}
private:
string name;
};
// ========== 工厂函数:返回动态分配的 Investment ==========
Investment* createInvestment(const string& name) {
return new Investment(name);
}
// ========== 错误示例:手动管理资源,容易泄漏 ==========
void bad() {
cout << "\n--- 错误:手动管理资源,可能泄漏 ---" << endl;
Investment* pInv = createInvestment("股票");
pInv->display();
// 忘记 delete pInv;
// 如果函数提前 return 或抛出异常,同样泄漏
cout << "函数结束,但资源未释放!" << endl;
}
void badWithException() {
cout << "\n--- 错误:异常导致资源泄漏 ---" << endl;
Investment* pInv = createInvestment("债券");
pInv->display();
throw runtime_error("发生异常"); // 抛出异常,跳过 delete
delete pInv; // 永远不会执行
}
// ========== 正确示例1:使用 std::unique_ptr (C++11) ==========
void goodUnique() {
cout << "\n--- 正确:使用 unique_ptr ---" << endl;
unique_ptr<Investment> pInv(createInvestment("基金"));
pInv->display();
// 离开作用域,unique_ptr 自动 delete
cout << "函数结束,资源自动释放" << endl;
}
// ========== 正确示例2:使用 std::shared_ptr (共享所有权) ==========
void goodShared() {
cout << "\n--- 正确:使用 shared_ptr ---" << endl;
shared_ptr<Investment> pInv1(createInvestment("信托"));
{
shared_ptr<Investment> pInv2 = pInv1; // 引用计数 +1
cout << "内部作用域,引用计数: " << pInv2.use_count() << endl;
pInv2->display();
} // pInv2 销毁,引用计数 -1
cout << "外部作用域,引用计数: " << pInv1.use_count() << endl;
pInv1->display();
// 离开作用域,pInv1 销毁,引用计数为0,释放资源
}
// ========== 正确示例3:使用 make_unique (C++14,C++11 需自己实现) ==========
void goodMakeUnique() {
cout << "\n--- 正确:使用 make_unique (C++14) ---" << endl;
// 模拟 C++11 的 make_unique (实际 C++14 标准)
auto pInv = make_unique<Investment>("期货");
pInv->display();
// 更安全,无资源泄漏风险
}
// 简单实现 make_unique 用于 C++11
template<typename T, typename... Args>
unique_ptr<T> make_unique(Args&&... args) {
return unique_ptr<T>(new T(forward<Args>(args)...));
}
// ========== 正确示例4:自定义 RAII 类 ==========
class InvestmentRAII {
public:
// 构造函数获取资源
explicit InvestmentRAII(Investment* pInv) : pInv(pInv) {}
// 析构函数释放资源
~InvestmentRAII() {
delete pInv;
}
// 禁止拷贝 (条款06)
InvestmentRAII(const InvestmentRAII&) = delete;
InvestmentRAII& operator=(const InvestmentRAII&) = delete;
// 提供访问原始资源的方法
Investment* get() const { return pInv; }
Investment* operator->() const { return pInv; }
Investment& operator*() const { return *pInv; }
private:
Investment* pInv;
};
void goodRAII() {
cout << "\n--- 正确:自定义 RAII 类 ---" << endl;
InvestmentRAII raii(createInvestment("私募"));
raii->display(); // 使用 operator->
(*raii).display(); // 使用 operator*
// 离开作用域,析构函数自动释放
}
// ========== 测试异常安全 ==========
void testExceptionSafety() {
cout << "\n--- 异常安全测试 ---" << endl;
try {
// 错误方式:泄漏
// Investment* p = createInvestment("高风险");
// throw runtime_error("失败");
// delete p;
// 正确方式:不会泄漏
unique_ptr<Investment> p = make_unique<Investment>("稳健型");
throw runtime_error("处理失败");
// 即使抛出异常,unique_ptr 析构仍会释放资源
} catch (const exception& e) {
cout << "捕获异常: " << e.what() << endl;
cout << "资源已被智能指针自动释放" << endl;
}
}
int main() {
cout << "========== 条款13:以对象管理资源 ==========" << endl;
// 危险示例,实际运行会泄漏,但为了演示,我们注释掉或单独运行
// bad(); // 资源泄漏
// badWithException(); // 异常导致泄漏
goodUnique();
goodShared();
goodMakeUnique();
goodRAII();
testExceptionSafety();
cout << "\n========== 程序结束 ==========" << endl;
return 0;
}
运行结果示例
========== 条款13:以对象管理资源 ==========
--- 正确:使用 unique_ptr ---
Investment 构造: 基金
投资产品: 基金
函数结束,资源自动释放
Investment 析构: 基金
--- 正确:使用 shared_ptr ---
Investment 构造: 信托
内部作用域,引用计数: 2
投资产品: 信托
外部作用域,引用计数: 1
投资产品: 信托
Investment 析构: 信托
--- 正确:使用 make_unique (C++14) ---
Investment 构造: 期货
投资产品: 期货
Investment 析构: 期货
--- 正确:自定义 RAII 类 ---
Investment 构造: 私募
投资产品: 私募
投资产品: 私募
Investment 析构: 私募
--- 异常安全测试 ---
Investment 构造: 稳健型
捕获异常: 处理失败
资源已被智能指针自动释放
Investment 析构: 稳健型
========== 程序结束 ==========
关键要点解析
1. 手动管理资源的问题
- 遗漏
delete:函数中可能忘记释放,导致内存泄漏。 - 异常不安全 :如果函数在
delete之前抛出异常,资源永远不会释放。 - 代码重复:需要在每个返回路径前释放资源,维护困难。
2. 智能指针的优势
- 自动释放:析构函数保证在离开作用域时释放资源。
- 异常安全:即使抛出异常,局部对象的析构函数仍会被调用。
- 所有权语义明确 :
unique_ptr独占,shared_ptr共享。
3. 智能指针的选择
| 智能指针 | 所有权 | 拷贝 | 开销 | 使用场景 |
|---|---|---|---|---|
unique_ptr |
独占 | 禁止(仅移动) | 低 | 明确唯一所有者 |
shared_ptr |
共享(引用计数) | 允许 | 中(原子操作) | 多个所有者共享资源 |
weak_ptr |
弱引用(配合shared) | 允许 | 中 | 打破循环引用 |
4. RAII 的核心思想
- 资源获取即初始化:在构造函数中获取资源,在析构函数中释放资源。
- 示例 :智能指针、
std::fstream、std::lock_guard等标准库类都遵循 RAII。 - 好处:资源管理自动化,防止泄漏,提高异常安全性。
5. 最佳实践
- 优先使用
std::unique_ptr:除非需要共享所有权,否则unique_ptr效率最高。 - 使用
std::make_unique/std::make_shared:避免显式new,更安全且高效。 - 不要手动
delete:将原始指针交给智能指针后,不要再手动释放。 - 对于非内存资源 (如文件句柄、锁),也可以自定义 RAII 类或使用
std::unique_ptr配合删除器。
6. 现代 C++ 的演进
- C++11 引入
unique_ptr、shared_ptr、weak_ptr。 - C++14 提供
std::make_unique。 - C++17 强化了
shared_ptr对数组的支持。 - 建议 :新代码中完全避免裸
new/delete,使用智能指针。
总结
- 以对象管理资源:利用 RAII 技术,让对象的析构函数自动释放资源。
- 使用智能指针 :
std::unique_ptr和std::shared_ptr是 RAII 的最佳实践。 - 避免手动资源管理 :手动
new/delete容易导致泄漏和异常不安全。 - 记住 :资源管理应该由对象负责,而不是程序员手动操作。使用智能指针,让 C++ 为你自动清理。
条款14:在资源管理类中小心 copying 行为
说明与介绍
核心思想:当你创建自己的资源管理类(RAII 类)时,需要仔细考虑当对象被复制(拷贝构造或拷贝赋值)时应该发生什么。因为编译器生成的默认拷贝函数只会做浅拷贝(逐成员复制),这可能不是你想要的。你必须根据自己的资源管理策略,明确决定复制行为。
为什么需要小心?
- 资源管理类负责释放资源(如内存、锁、文件句柄)。如果简单复制,可能导致同一资源被释放两次(双重删除),或者资源的所有权混乱。
- 不同的资源类型需要不同的复制策略:例如互斥锁不能复制,堆内存可以深拷贝或转移所有权。
四种常见的复制策略:
- 禁止复制 :如果资源不允许被多个对象共享(如
unique_ptr、互斥锁),应该将拷贝操作声明为= delete(或 C++98 中声明为 private 且不实现)。 - 引用计数 :通过引用计数跟踪有多少对象共享同一资源,当计数为 0 时释放资源(如
shared_ptr)。 - 深拷贝 :复制底层资源本身(如
string、vector的拷贝语义)。 - 转移所有权 :将资源所有权从一个对象转移到另一个对象(如
auto_ptr、unique_ptr的移动语义)。
关键点:
- 编译器生成的拷贝构造和拷贝赋值是逐成员复制的,对于指针成员,只是复制指针值(浅拷贝),可能导致双重删除。
- 你必须在资源管理类中显式声明拷贝操作,并根据资源类型选择合适的策略。
- C++11 的移动语义为转移所有权提供了更优雅的方式。
代码对比示例
cpp
// 文件名: clause14.cpp
// 编译: g++ -std=c++11 clause14.cpp -o clause14
#include <iostream>
#include <memory>
#include <mutex>
#include <cstring>
using namespace std;
// ========== 模拟一个互斥锁 ==========
class Mutex {
public:
void lock() { cout << "互斥锁已锁定" << endl; }
void unlock() { cout << "互斥锁已解锁" << endl; }
};
// ========== 错误示例:简单复制导致双重释放 ==========
class LockBad {
public:
explicit LockBad(Mutex* pm) : mutex(pm) {
mutex->lock();
cout << "LockBad 构造,锁定互斥锁" << endl;
}
~LockBad() {
mutex->unlock();
cout << "LockBad 析构,解锁互斥锁" << endl;
}
// 使用编译器生成的拷贝构造和拷贝赋值(危险!)
private:
Mutex* mutex;
};
void testBad() {
cout << "\n=== 错误:简单复制导致双重解锁 ===" << endl;
Mutex m;
LockBad lock1(&m);
LockBad lock2(lock1); // 拷贝构造,lock2 和 lock1 指向同一个 Mutex
// 离开作用域时,lock1 和 lock2 都会解锁同一个互斥锁 → 未定义行为(双重解锁)
}
// ========== 策略1:禁止复制 ==========
class LockNoCopy {
public:
explicit LockNoCopy(Mutex* pm) : mutex(pm) {
mutex->lock();
cout << "LockNoCopy 构造,锁定互斥锁" << endl;
}
~LockNoCopy() {
mutex->unlock();
cout << "LockNoCopy 析构,解锁互斥锁" << endl;
}
// 禁止拷贝
LockNoCopy(const LockNoCopy&) = delete;
LockNoCopy& operator=(const LockNoCopy&) = delete;
// 允许移动(可选)
LockNoCopy(LockNoCopy&& other) noexcept : mutex(other.mutex) {
other.mutex = nullptr;
cout << "LockNoCopy 移动构造" << endl;
}
private:
Mutex* mutex;
};
void testNoCopy() {
cout << "\n=== 策略1:禁止复制 ===" << endl;
Mutex m;
LockNoCopy lock1(&m);
// LockNoCopy lock2(lock1); // 编译错误!拷贝构造被删除
LockNoCopy lock2(move(lock1)); // 移动构造(可选)
// 注意:移动后 lock1 不再管理互斥锁,避免双重解锁
}
// ========== 策略2:引用计数(使用 shared_ptr) ==========
class LockRefCount {
public:
// 使用 shared_ptr 管理互斥锁的解锁操作(自定义删除器)
explicit LockRefCount(Mutex* pm)
: mutexPtr(pm, [](Mutex* m) { m->unlock(); }) {
mutexPtr.get()->lock();
cout << "LockRefCount 构造,锁定互斥锁,引用计数=" << mutexPtr.use_count() << endl;
}
// 默认拷贝构造、拷贝赋值、析构都正确工作(shared_ptr 自动处理引用计数)
// 不需要自定义析构函数
private:
shared_ptr<Mutex> mutexPtr; // 注意:shared_ptr 默认删除器是 delete,这里我们需要 unlock,所以传递了自定义删除器
};
void testRefCount() {
cout << "\n=== 策略2:引用计数(shared_ptr) ===" << endl;
Mutex m;
LockRefCount lock1(&m);
LockRefCount lock2(lock1); // 拷贝构造,引用计数增加
cout << "lock1 和 lock2 共享同一互斥锁,引用计数=" << lock2.use_count() << " (通过 shared_ptr)" << endl;
// 离开作用域时,最后一个 LockRefCount 析构时会调用自定义删除器 unlock
}
// ========== 策略3:深拷贝资源 ==========
class Bitmap {
public:
Bitmap(const char* data = "") {
size = strlen(data) + 1;
m_data = new char[size];
strcpy(m_data, data);
cout << "Bitmap 构造: " << m_data << endl;
}
Bitmap(const Bitmap& rhs) {
size = rhs.size;
m_data = new char[size];
strcpy(m_data, rhs.m_data);
cout << "Bitmap 拷贝构造: " << m_data << endl;
}
~Bitmap() {
cout << "Bitmap 析构: " << (m_data ? m_data : "null") << endl;
delete[] m_data;
}
Bitmap& operator=(const Bitmap& rhs) {
if (this != &rhs) {
delete[] m_data;
size = rhs.size;
m_data = new char[size];
strcpy(m_data, rhs.m_data);
cout << "Bitmap 赋值: " << m_data << endl;
}
return *this;
}
void print() const { cout << "Bitmap 数据: " << m_data << endl; }
private:
char* m_data;
size_t size;
};
class WidgetDeepCopy {
public:
explicit WidgetDeepCopy(const char* data) : bitmap(new Bitmap(data)) {}
// 深拷贝:复制底层 Bitmap 资源
WidgetDeepCopy(const WidgetDeepCopy& rhs) : bitmap(new Bitmap(*rhs.bitmap)) {
cout << "WidgetDeepCopy 拷贝构造(深拷贝)" << endl;
}
WidgetDeepCopy& operator=(const WidgetDeepCopy& rhs) {
cout << "WidgetDeepCopy 拷贝赋值(深拷贝)" << endl;
if (this != &rhs) {
delete bitmap;
bitmap = new Bitmap(*rhs.bitmap);
}
return *this;
}
~WidgetDeepCopy() { delete bitmap; }
void print() const { bitmap->print(); }
private:
Bitmap* bitmap;
};
void testDeepCopy() {
cout << "\n=== 策略3:深拷贝资源 ===" << endl;
WidgetDeepCopy w1("Hello");
WidgetDeepCopy w2(w1); // 深拷贝,w2 拥有独立的 Bitmap
w2.print();
w1 = w2; // 深拷贝赋值
}
// ========== 策略4:转移所有权(移动语义) ==========
class WidgetMoveOnly {
public:
explicit WidgetMoveOnly(const char* data) : bitmap(new Bitmap(data)) {}
// 禁止拷贝
WidgetMoveOnly(const WidgetMoveOnly&) = delete;
WidgetMoveOnly& operator=(const WidgetMoveOnly&) = delete;
// 移动构造:转移所有权
WidgetMoveOnly(WidgetMoveOnly&& other) noexcept : bitmap(other.bitmap) {
other.bitmap = nullptr;
cout << "WidgetMoveOnly 移动构造(转移所有权)" << endl;
}
// 移动赋值
WidgetMoveOnly& operator=(WidgetMoveOnly&& other) noexcept {
cout << "WidgetMoveOnly 移动赋值(转移所有权)" << endl;
if (this != &other) {
delete bitmap;
bitmap = other.bitmap;
other.bitmap = nullptr;
}
return *this;
}
~WidgetMoveOnly() { delete bitmap; }
void print() const { if (bitmap) bitmap->print(); else cout << "空对象" << endl; }
private:
Bitmap* bitmap;
};
void testMoveOnly() {
cout << "\n=== 策略4:转移所有权(移动语义) ===" << endl;
WidgetMoveOnly w1("Transfer");
WidgetMoveOnly w2(move(w1)); // 所有权从 w1 转移到 w2
cout << "w1: "; w1.print();
cout << "w2: "; w2.print();
w1 = move(w2); // 转移回 w1
cout << "w1: "; w1.print();
cout << "w2: "; w2.print();
}
// ========== 实际应用:使用 std::unique_ptr 实现转移所有权 ==========
void testUniquePtr() {
cout << "\n=== 实际应用:unique_ptr 自动管理转移所有权 ===" << endl;
unique_ptr<Bitmap> up1(new Bitmap("唯一资源"));
unique_ptr<Bitmap> up2 = move(up1); // 转移所有权
if (!up1) cout << "up1 为空" << endl;
cout << "up2: "; up2->print();
}
int main() {
cout << "========== 条款14:在资源管理类中小心 copying 行为 ==========" << endl;
// testBad(); // 危险!可能导致双重解锁或程序崩溃,默认注释
testNoCopy();
testRefCount();
testDeepCopy();
testMoveOnly();
testUniquePtr();
cout << "\n========== 程序结束 ==========" << endl;
return 0;
}
运行结果示例
========== 条款14:在资源管理类中小心 copying 行为 ==========
=== 策略1:禁止复制 ===
互斥锁已锁定
LockNoCopy 构造,锁定互斥锁
LockNoCopy 移动构造
LockNoCopy 析构,解锁互斥锁
LockNoCopy 析构,解锁互斥锁 // 注意:移动后 lock1 不再管理,只有 lock2 解锁
=== 策略2:引用计数(shared_ptr) ===
互斥锁已锁定
LockRefCount 构造,锁定互斥锁,引用计数=1
LockRefCount 构造,锁定互斥锁,引用计数=2
lock1 和 lock2 共享同一互斥锁,引用计数=2 (通过 shared_ptr)
LockRefCount 析构,解锁互斥锁 // 最后一个析构时解锁
LockRefCount 析构,解锁互斥锁
=== 策略3:深拷贝资源 ===
Bitmap 构造: Hello
Bitmap 拷贝构造: Hello
WidgetDeepCopy 拷贝构造(深拷贝)
Bitmap 数据: Hello
Bitmap 拷贝构造: Hello
WidgetDeepCopy 拷贝赋值(深拷贝)
Bitmap 析构: Hello
Bitmap 析构: Hello
Bitmap 析构: Hello
=== 策略4:转移所有权(移动语义) ===
Bitmap 构造: Transfer
WidgetMoveOnly 移动构造(转移所有权)
w1: 空对象
w2: Bitmap 数据: Transfer
WidgetMoveOnly 移动赋值(转移所有权)
w1: Bitmap 数据: Transfer
w2: 空对象
Bitmap 析构: Transfer
=== 实际应用:unique_ptr 自动管理转移所有权 ===
Bitmap 构造: 唯一资源
up1 为空
up2: Bitmap 数据: 唯一资源
Bitmap 析构: 唯一资源
========== 程序结束 ==========
关键要点解析
1. 四种复制策略对比
| 策略 | 实现方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 禁止复制 | = delete 拷贝操作 |
互斥锁、文件句柄(独占) | 简单,避免所有权混淆 | 不能共享资源 |
| 引用计数 | shared_ptr 或手动计数 |
多个对象共享资源(如共享内存) | 自动管理生命周期 | 引用计数开销,循环引用问题 |
| 深拷贝 | 复制底层资源 | 资源独立,可复制(如字符串) | 每个对象拥有独立资源 | 性能开销大 |
| 转移所有权 | 移动语义(unique_ptr) |
资源所有权唯一但可转移 | 高效,语义清晰 | 只能移动,不能拷贝 |
2. 错误示例的问题
LockBad使用默认拷贝构造,导致两个对象持有同一个Mutex*。- 析构时,两个对象都会对同一个互斥锁调用
unlock(),造成双重解锁,这是未定义行为(可能导致程序崩溃)。
3. 策略1:禁止复制(适用于互斥锁、文件句柄)
- 使用
= delete删除拷贝构造和拷贝赋值。 - 可选择提供移动构造,允许所有权转移。
- 典型应用 :
std::unique_ptr、std::lock_guard(C++17 起std::scoped_lock)。
4. 策略2:引用计数(使用 shared_ptr)
- 利用
shared_ptr的引用计数机制,可以轻松实现共享资源的 RAII。 - 自定义删除器:对于互斥锁,删除器应该调用
unlock()而不是delete。 - 注意 :
shared_ptr默认使用delete,对于非new分配的资源需要提供删除器。
5. 策略3:深拷贝(适用于独立资源)
- 拷贝时复制底层资源(如
Bitmap的数据)。 - 实现深拷贝需要自定义拷贝构造和拷贝赋值,并确保异常安全(参考条款11)。
- 典型应用 :
std::string、std::vector。
6. 策略4:转移所有权(移动语义)
- 使用移动构造和移动赋值,将资源所有权从一个对象转移到另一个。
- 移动后,源对象不再管理资源(通常置为
nullptr)。 - 典型应用 :
std::unique_ptr。
7. 现代 C++ 的最佳实践
- 优先使用标准库的 RAII 类 :
std::unique_ptr、std::shared_ptr、std::lock_guard、std::fstream等,避免自己编写资源管理类。 - 如果必须自定义资源管理类,应遵循"三/五法则"(Rule of Three/Five),并明确选择上述四种策略之一。
- C++11 的移动语义大大简化了转移所有权的实现。
总结
- 在资源管理类中小心 copying 行为:编译器生成的默认拷贝函数通常不是你想要的。
- 明确选择复制策略:禁止复制、引用计数、深拷贝或转移所有权,并在类中显式实现。
- 优先使用标准库组件 :
std::unique_ptr、std::shared_ptr、std::lock_guard等已经实现了正确的复制/移动语义。 - 记住 :资源管理类的复制行为直接影响资源的安全性和效率。仔细考虑你的资源所有权模型,并选择最适合的策略。
条款15:在资源管理类中提供对原始资源的访问
说明与介绍
核心思想 :资源管理类(如 std::shared_ptr、std::unique_ptr 或自定义 RAII 类)通过封装资源来确保自动释放。但有时需要访问原始资源(例如与旧 C API 交互,或调用不支持智能指针的库函数)。此时,RAII 类必须提供一个安全的方式来获取其管理的原始资源。
为什么需要提供原始资源访问?
- 兼容旧代码:许多 C++ 库(尤其是系统 API)仍使用原始指针或句柄,不接受智能指针。
- 性能要求:某些场景下,直接操作原始资源可能比通过 RAII 类再封装更高效(尽管这种情况较少)。
- 灵活性:有时需要调用资源特有的成员函数,而 RAII 类没有提供相应的接口。
两种访问方式:
- 显式转换 :提供一个
get()成员函数,返回原始资源(如shared_ptr::get())。这是安全且推荐的方式,因为用户明确知道在获取原始资源。 - 隐式转换 :提供
operator T()转换函数,允许 RAII 对象自动转换为原始资源类型。这使用更便捷,但可能在不经意间发生意外转换,增加出错风险。
最佳实践:
- 优先提供显式的
get()函数,清晰表明意图。 - 谨慎使用隐式转换,仅在确实需要(如自动转换为布尔测试)且不会导致歧义时使用。
- 如果提供了隐式转换,也要提供
get()以保持一致性。 - 对于智能指针,标准库已经提供了
get()、operator->和operator*,以及隐式转换为bool的能力。
代码对比示例
cpp
// 文件名: clause15.cpp
// 编译: g++ -std=c++11 clause15.cpp -o clause15
#include <iostream>
#include <memory>
#include <cstring>
using namespace std;
// ========== 模拟一个旧的 C API,只能接受原始指针 ==========
class RawFont {
public:
RawFont(const string& name) : name(name) {
cout << "RawFont 构造: " << name << endl;
}
~RawFont() {
cout << "RawFont 析构: " << name << endl;
}
void display() const {
cout << "显示字体: " << name << endl;
}
string getName() const { return name; }
private:
string name;
};
// 旧 C 风格函数,只接受原始指针
void printFontName(RawFont* font) {
if (font) {
cout << "C API 打印字体名: " << font->getName() << endl;
}
}
void renderFont(const RawFont* font) {
if (font) {
font->display();
}
}
// ========== 错误示例:没有提供原始资源访问 ==========
class FontBad {
public:
explicit FontBad(RawFont* f) : font(f) {}
~FontBad() { delete font; }
// 没有提供任何方式获取原始 RawFont*
private:
RawFont* font;
};
void testBad() {
cout << "\n=== 错误:无法访问原始资源 ===" << endl;
FontBad fb(new RawFont("宋体"));
// printFontName(???); // 无法获取原始指针,无法调用 C API
cout << "错误:FontBad 没有提供 get(),无法与 C API 交互" << endl;
}
// ========== 正确示例1:提供显式 get() 函数 ==========
class FontExplicit {
public:
explicit FontExplicit(RawFont* f) : font(f) {}
~FontExplicit() { delete font; }
// 显式访问:返回原始指针
RawFont* get() const { return font; }
// 也可以提供 operator-> 和 operator* 使使用更方便
RawFont* operator->() const { return font; }
RawFont& operator*() const { return *font; }
private:
RawFont* font;
};
void testExplicit() {
cout << "\n=== 正确:提供 get() 和 operator-> ===" << endl;
FontExplicit fe(new RawFont("黑体"));
// 通过 get() 获取原始指针
printFontName(fe.get());
// 通过 operator-> 直接调用成员
fe->display();
// 通过 operator* 解引用
(*fe).display();
}
// ========== 正确示例2:提供隐式转换(不推荐) ==========
class FontImplicit {
public:
explicit FontImplicit(RawFont* f) : font(f) {}
~FontImplicit() { delete font; }
// 隐式转换:FontImplicit -> RawFont*
operator RawFont*() const { return font; }
// 同时提供 get() 保持一致性
RawFont* get() const { return font; }
private:
RawFont* font;
};
void testImplicit() {
cout << "\n=== 隐式转换(潜在风险) ===" << endl;
FontImplicit fi(new RawFont("楷体"));
// 隐式转换发生:fi 自动转换为 RawFont*
printFontName(fi); // 注意:直接传递 fi,编译器调用 operator RawFont*()
// 也可以显式调用 get()
renderFont(fi.get());
}
// ========== 隐式转换的陷阱 ==========
class FontDangerous {
public:
explicit FontDangerous(RawFont* f) : font(f) {}
~FontDangerous() { delete font; }
// 隐式转换到 RawFont*
operator RawFont*() const { return font; }
// 隐式转换到 bool(错误设计)
operator bool() const { return font != nullptr; }
private:
RawFont* font;
};
void testImplicitTrap() {
cout << "\n=== 隐式转换的陷阱:意外类型转换 ===" << endl;
FontDangerous fd(new RawFont("隶书"));
// 意图:比较两个字体对象
FontDangerous fd2(new RawFont("隶书"));
// if (fd == fd2) // 编译错误?不,编译器会将 fd 和 fd2 转换为 RawFont*,然后比较指针地址!
// 这可能导致逻辑错误,因为比较的是指针地址,而非字体内容
// 更危险的:fd 被转换为 bool,然后参与算术运算
int x = fd + 5; // 奇怪:fd 转换为 bool(true=1),1+5=6,毫无意义!
cout << "意外:fd + 5 = " << x << " (因为 fd 被转换为 bool true)" << endl;
// 避免这种设计:不要提供隐式转换到 bool,而是提供 explicit operator bool() (C++11)
}
// ========== 正确做法:C++11 的 explicit 转换运算符 ==========
class FontModern {
public:
explicit FontModern(RawFont* f) : font(f) {}
~FontModern() { delete font; }
RawFont* get() const { return font; }
// 显式转换到 bool,避免意外隐式转换
explicit operator bool() const { return font != nullptr; }
// 禁止隐式转换到 RawFont*(只提供 get())
private:
RawFont* font;
};
void testModern() {
cout << "\n=== C++11 显式转换运算符(安全) ===" << endl;
FontModern fm(new RawFont("魏碑"));
// 必须显式调用 get()
printFontName(fm.get());
// 条件判断可以使用(explicit operator bool 在条件中允许)
if (fm) {
cout << "字体有效" << endl;
}
// 下面这行会编译错误(不会发生隐式转换)
// int y = fm + 5; // 错误:没有从 FontModern 到 int 的隐式转换
}
// ========== 智能指针的示例 ==========
void testSmartPointer() {
cout << "\n=== 标准库智能指针的原始资源访问 ===" << endl;
unique_ptr<RawFont> up(new RawFont("行书"));
// get() 获取原始指针
printFontName(up.get());
// operator-> 和 operator* 自然可用
up->display();
// 隐式转换为 bool(用于条件判断)
if (up) {
cout << "unique_ptr 非空" << endl;
}
// 注意:unique_ptr 没有提供隐式转换为原始指针,防止意外释放
// RawFont* p = up; // 错误!必须使用 get()
}
// ========== 实际应用:自定义 RAII 类包装 C 文件句柄 ==========
class FileHandle {
public:
explicit FileHandle(const char* filename, const char* mode) {
file = fopen(filename, mode);
if (!file) {
throw runtime_error("无法打开文件");
}
cout << "文件已打开: " << filename << endl;
}
~FileHandle() {
if (file) {
fclose(file);
cout << "文件已关闭" << endl;
}
}
// 提供 get() 获取原始 FILE*
FILE* get() const { return file; }
// 提供 operator-> 和 operator* 方便使用
FILE* operator->() const { return file; }
// 禁止拷贝
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// 允许移动
FileHandle(FileHandle&& other) noexcept : file(other.file) {
other.file = nullptr;
}
private:
FILE* file;
};
void testFileHandle() {
cout << "\n=== 实际应用:包装 C 文件句柄 ===" << endl;
FileHandle fh("test.txt", "w");
// 通过 get() 获取原始 FILE* 用于 fprintf 等 C 函数
fprintf(fh.get(), "Hello, RAII!\n");
// 通过 operator-> 直接调用(FILE* 的成员?其实没有,仅演示)
// 通常 FILE* 不支持 ->,这里只是展示概念
cout << "数据已写入文件" << endl;
}
int main() {
cout << "========== 条款15:在资源管理类中提供对原始资源的访问 ==========" << endl;
testBad();
testExplicit();
testImplicit();
testImplicitTrap();
testModern();
testSmartPointer();
testFileHandle();
cout << "\n========== 程序结束 ==========" << endl;
return 0;
}
运行结果示例
========== 条款15:在资源管理类中提供对原始资源的访问 ==========
=== 错误:无法访问原始资源 ===
错误:FontBad 没有提供 get(),无法与 C API 交互
=== 正确:提供 get() 和 operator-> ===
RawFont 构造: 黑体
C API 打印字体名: 黑体
显示字体: 黑体
显示字体: 黑体
RawFont 析构: 黑体
=== 隐式转换(潜在风险) ===
RawFont 构造: 楷体
C API 打印字体名: 楷体
显示字体: 楷体
RawFont 析构: 楷体
=== 隐式转换的陷阱:意外类型转换 ===
RawFont 构造: 隶书
RawFont 构造: 隶书
意外:fd + 5 = 6 (因为 fd 被转换为 bool true)
RawFont 析构: 隶书
RawFont 析构: 隶书
=== C++11 显式转换运算符(安全) ===
RawFont 构造: 魏碑
C API 打印字体名: 魏碑
字体有效
RawFont 析构: 魏碑
=== 标准库智能指针的原始资源访问 ===
RawFont 构造: 行书
C API 打印字体名: 行书
显示字体: 行书
unique_ptr 非空
RawFont 析构: 行书
=== 实际应用:包装 C 文件句柄 ===
文件已打开: test.txt
数据已写入文件
文件已关闭
========== 程序结束 ==========
关键要点解析
1. 为什么需要提供原始资源访问?
- 与旧代码集成:许多 C/C++ 库只接受原始指针或句柄,不接受 RAII 对象。
- 性能关键路径:有时需要直接操作原始资源以获得最佳性能(但通常编译器优化足够)。
- 特定操作:某些资源特有的操作,RAII 类没有封装。
2. 两种访问方式的对比
| 方式 | 语法 | 安全性 | 便捷性 | 推荐度 |
|---|---|---|---|---|
显式 get() |
obj.get() |
高(明确意图) | 中(需显式调用) | ⭐⭐⭐⭐⭐ |
| 隐式转换 | obj 自动转换 |
低(意外转换风险) | 高(自动) | ⭐⭐ |
operator-> / operator* |
obj->member() |
高(仅限成员访问) | 高(像指针一样) | ⭐⭐⭐⭐ |
3. 显式 get() 的优点
- 意图明确:代码读者知道正在获取原始资源。
- 安全:不会发生意外隐式转换,避免错误。
- 标准库采用 :
std::shared_ptr::get()、std::unique_ptr::get()都是显式的。
4. 隐式转换的陷阱
- 意外转换 :对象可能在不期望的地方转换为原始指针或
bool。 - 多重转换歧义 :如果定义多个转换运算符(如
operator RawFont*()和operator bool()),会导致重载决议混乱。 - 示例陷阱 :
FontDangerous fd; int x = fd + 5;居然能编译,因为fd转换为bool(true=1),然后1+5=6,完全不合理。- 比较
fd == fd2实际比较的是指针地址,而非逻辑相等。
5. 最佳实践
- 优先提供
get():这是最安全、最清晰的方式。 - 提供
operator->和operator*:使 RAII 对象用起来像指针,方便调用资源成员函数。 - 如果需要条件测试 (如
if (ptr)),使用explicit operator bool() const(C++11),避免隐式转换到数值类型。 - 避免提供隐式转换到原始资源类型,除非你非常清楚其影响(例如在封装一个简单值类型时)。
6. 标准库的实践
std::unique_ptr<T>和std::shared_ptr<T>提供:get()显式获取原始指针。operator->和operator*用于访问所管理的对象。explicit operator bool()用于条件判断。- 没有 隐式转换到
T*,防止意外释放或转移所有权。
7. 自定义 RAII 类的设计建议
- 总是提供
get()。 - 提供
operator->和operator*(如果管理的资源支持指针语义)。 - 提供
explicit operator bool()用于有效性测试。 - 如果需要与 C API 频繁交互,可以额外提供隐式转换,但需谨慎评估风险。
- 如果资源是句柄类型(如文件描述符、
HANDLE),考虑提供get()和release()(转移所有权)。
总结
- 在资源管理类中提供对原始资源的访问:这是 RAII 类的必要功能,以便与旧代码和 C API 集成。
- 优先使用显式
get()函数:安全、清晰,标准库也采用此方式。 - 谨慎使用隐式转换:可能带来意外行为,仅在确实需要便捷且风险可控时使用。
- 提供
operator->和operator*:让 RAII 对象用起来像指针,提高可用性。 - 使用
explicit operator bool进行条件测试,避免隐式数值转换。 - 记住 :封装资源的目的是为了安全,但有时必须打开封装。提供安全、显式的访问方式,让用户既能得到原始资源,又不破坏 RAII 的自动管理。
条款16:成对使用 new 和 delete 时要采取相同形式
说明与介绍
核心思想 :当你使用 new 分配内存时,必须使用对应的 delete 来释放。new 有两种形式:
new:分配单个对象。new Type[]:分配一个对象数组。
相应地,delete 也有两种形式:
delete p:释放单个对象。delete[] p:释放对象数组。
混用的后果 :如果使用 new 分配单个对象却用 delete[] 释放,或者使用 new[] 分配数组却用 delete 释放,结果是未定义行为。可能导致内存泄漏、程序崩溃、数据损坏等严重问题。
为什么需要配对?
- 对于单个对象,编译器只需要调用一次析构函数并释放内存。
- 对于数组,编译器需要知道有多少个对象,以便为每个对象调用析构函数。这个数量通常存储在分配的内存头部(实现相关)。
delete[]会去读取这个数量,而delete不会。混用会导致错误的内存访问。
常见陷阱:
- 对单个对象使用
delete[]:会去读取不存在数组大小信息,导致内存损坏。 - 对数组使用
delete:只调用第一个元素的析构函数,其他元素不析构,且释放内存的方式可能不匹配(取决于实现)。 - 使用
typedef定义数组类型时,容易忘记使用delete[]。
最佳实践:
- 永远不要对单个对象使用
delete[],也不要对数组使用delete。 - 如果你使用
new,就用delete;如果你使用new[],就用delete[]。 - 优先使用
std::vector和std::string等容器,避免手动管理动态数组。 - 如果必须使用动态数组,考虑使用
std::unique_ptr<T[]>(C++11 支持数组版本)。
代码对比示例
cpp
// 文件名: clause16.cpp
// 编译: g++ -std=c++11 clause16.cpp -o clause16
#include <iostream>
#include <cstring>
using namespace std;
// ========== 辅助类:用于观察构造和析构 ==========
class Dog {
public:
Dog(const string& name = "无名") : name(name) {
cout << "Dog 构造: " << name << endl;
}
~Dog() {
cout << "Dog 析构: " << name << endl;
}
void bark() const {
cout << name << " 汪汪!" << endl;
}
private:
string name;
};
// ========== 错误示例1:new + delete[] (单个对象用数组删除) ==========
void testSingleObjectWithArrayDelete() {
cout << "\n=== 错误:new 单个对象,却用 delete[] 释放 ===" << endl;
Dog* pDog = new Dog("旺财");
pDog->bark();
// 危险!未定义行为
delete[] pDog; // 错误:应该用 delete pDog
// 后果:可能只调用一次析构(但去读数组头部信息,内存损坏),程序可能崩溃
cout << "程序可能在此崩溃或产生未定义行为" << endl;
}
// ========== 错误示例2:new[] + delete (数组用单个删除) ==========
void testArrayWithSingleDelete() {
cout << "\n=== 错误:new[] 数组,却用 delete 释放 ===" << endl;
Dog* pDogs = new Dog[3]{ Dog("阿黄"), Dog("小黑"), Dog("花花") };
for (int i = 0; i < 3; ++i) pDogs[i].bark();
// 危险!未定义行为
delete pDogs; // 错误:应该用 delete[] pDogs
// 后果:只调用第一个元素的析构函数,另外两个未析构(内存泄漏),且释放方式错误
cout << "只有第一个 Dog 被析构,其他泄漏,且可能崩溃" << endl;
}
// ========== 正确示例:配对使用 ==========
void testCorrect() {
cout << "\n=== 正确:new + delete 配对 ===" << endl;
Dog* pDog = new Dog("来福");
pDog->bark();
delete pDog; // 正确
cout << "\n=== 正确:new[] + delete[] 配对 ===" << endl;
Dog* pDogs = new Dog[2]{ Dog("豆豆"), Dog("球球") };
for (int i = 0; i < 2; ++i) pDogs[i].bark();
delete[] pDogs; // 正确
}
// ========== typedef 陷阱 ==========
void testTypedefTrap() {
cout << "\n=== typedef 陷阱:类型别名容易误导 ===" << endl;
typedef string AddressLines[4]; // AddressLines 是一个包含4个string的数组类型
// 错误做法:看起来像 new 单个对象,实际是 new 数组
string* pal = new AddressLines; // 等价于 new string[4]
// 如果忘记使用 delete[],而是用 delete pal,就是未定义行为
// 正确做法:必须使用 delete[]
delete[] pal; // 正确
// 更好的做法:不要对数组使用 typedef,或明确使用 std::array<vector>
cout << "建议:使用 std::array<string,4> 或 std::vector<string> 代替 C 风格数组" << endl;
}
// ========== 自定义类演示析构调用次数 ==========
class Counter {
public:
Counter() { cout << "Counter 构造" << endl; }
~Counter() { cout << "Counter 析构" << endl; }
};
void testDestructorCalls() {
cout << "\n=== 析构函数调用次数演示 ===" << endl;
cout << "--- 正确:delete[] 会为每个元素调用析构 ---" << endl;
Counter* arr = new Counter[3];
delete[] arr; // 输出 3 次析构
cout << "\n--- 错误:delete 只调用第一个元素的析构 ---" << endl;
Counter* arr2 = new Counter[3];
delete arr2; // 只调用一次析构,其他两个对象未析构(泄漏)
cout << "注意:只有第一个 Counter 被析构,程序可能崩溃" << endl;
}
// ========== 使用智能指针避免问题 ==========
void testSmartPointer() {
cout << "\n=== 使用智能指针避免手动配对 ===" << endl;
// 单个对象:unique_ptr<Dog>
unique_ptr<Dog> pDog(new Dog("智能狗"));
pDog->bark();
// 自动 delete
// 数组:unique_ptr<Dog[]> (C++11 支持)
unique_ptr<Dog[]> pDogs(new Dog[2]{ Dog("小智"), Dog("小能") });
for (int i = 0; i < 2; ++i) pDogs[i].bark();
// 自动 delete[]
}
// ========== 使用标准容器更安全 ==========
void testContainers() {
cout << "\n=== 使用 std::vector 代替动态数组 ===" << endl;
vector<Dog> dogs;
dogs.emplace_back("向量狗1");
dogs.emplace_back("向量狗2");
for (auto& d : dogs) d.bark();
// vector 自动管理内存,无需手动 delete
}
int main() {
cout << "========== 条款16:成对使用 new 和 delete 时要采取相同形式 ==========" << endl;
// 注意:错误示例可能导致程序崩溃或未定义行为,默认注释掉
// testSingleObjectWithArrayDelete(); // 危险,可能导致崩溃
// testArrayWithSingleDelete(); // 危险,导致内存泄漏和未定义行为
testCorrect();
testTypedefTrap();
testDestructorCalls(); // 其中包含危险操作,但为了演示,保留并说明
testSmartPointer();
testContainers();
cout << "\n========== 程序结束 ==========" << endl;
return 0;
}
运行结果示例(危险部分可能因编译器而不同)
========== 条款16:成对使用 new 和 delete 时要采取相同形式 ==========
=== 正确:new + delete 配对 ===
Dog 构造: 来福
来福 汪汪!
Dog 析构: 来福
=== 正确:new[] + delete[] 配对 ===
Dog 构造: 豆豆
Dog 构造: 球球
豆豆 汪汪!
球球 汪汪!
Dog 析构: 球球
Dog 析构: 豆豆
=== typedef 陷阱:类型别名容易误导 ===
建议:使用 std::array<string,4> 或 std::vector<string> 代替 C 风格数组
=== 析构函数调用次数演示 ===
--- 正确:delete[] 会为每个元素调用析构 ---
Counter 构造
Counter 构造
Counter 构造
Counter 析构
Counter 析构
Counter 析构
--- 错误:delete 只调用第一个元素的析构 ---
Counter 构造
Counter 构造
Counter 构造
Counter 析构
注意:只有第一个 Counter 被析构,其他两个对象未析构(泄漏),程序可能崩溃
=== 使用智能指针避免手动配对 ===
Dog 构造: 智能狗
智能狗 汪汪!
Dog 析构: 智能狗
Dog 构造: 小智
Dog 构造: 小能
小智 汪汪!
小能 汪汪!
Dog 析构: 小能
Dog 析构: 小智
=== 使用 std::vector 代替动态数组 ===
Dog 构造: 向量狗1
Dog 构造: 向量狗2
向量狗1 汪汪!
向量狗2 汪汪!
Dog 析构: 向量狗2
Dog 析构: 向量狗1
========== 程序结束 ==========
关键要点解析
1. 错误示例的后果
new+delete[]:delete[]期望在内存头部找到数组大小信息,但单个对象没有这个信息,导致读取到垃圾数据,破坏内存,可能立即崩溃或后续操作出错。new[]+delete:只调用第一个对象的析构函数,其他对象未析构(资源泄漏),然后释放内存时可能使用错误的释放方式,导致未定义行为。
2. 为什么编译器不报错?
- 这两种形式在语法上都是合法的,编译器无法在编译时知道
p指向的是单个对象还是数组。这是 C++ 设计中的历史遗留问题。 - 责任完全在程序员身上。
3. typedef 陷阱
typedef string AddressLines[4];定义了一个数组类型。string* pal = new AddressLines;实际上分配的是string[4],但看起来像new单个对象。- 程序员容易误用
delete pal而不是delete[] pal。 - 解决方法 :避免对数组类型使用
typedef,改用using+std::array或std::vector。
4. 析构函数调用差异
delete[]会为数组中的每个元素调用析构函数(从最后一个元素开始向前)。delete只会为第一个元素调用析构函数,其他元素的析构函数不被调用,导致资源泄漏。
5. 现代 C++ 的替代方案
- 智能指针 :
std::unique_ptr<T[]>(C++11)和std::shared_ptr<T[]>(C++17)支持数组,自动调用delete[]。 - 标准容器 :
std::vector<T>和std::array<T, N>完全替代动态数组,自动管理内存。 - 字符串 :使用
std::string代替char*。
6. 最佳实践总结
- 永远配对 :
new↔delete,new[]↔delete[]。 - 避免手动管理 :使用
vector、string、智能指针。 - 警惕
typedef:如果必须定义数组类型,使用using+std::array。 - 代码审查 :当看到
new或delete时,立即检查配对形式。
总结
- 成对使用
new和delete时要采取相同形式 :单个对象用delete,数组用delete[]。 - 混用导致未定义行为:可能崩溃、泄漏、数据损坏。
- 优先使用现代 C++ 特性 :
std::vector、std::unique_ptr<T[]>可以完全避免手动配对。 - 记住 :C++ 不帮你检查配对,你必须自己保证。使用 RAII 容器和智能指针,让编译器替你管理资源。
条款17:以独立语句将 new 出的对象置入智能指针
说明与介绍
核心思想 :当你使用智能指针(如 std::shared_ptr、std::unique_ptr)管理动态资源时,应确保在单独语句中完成对象的 new 和智能指针的构造。如果合并在一起,可能因为编译器重排语句顺序而导致资源泄漏。
为什么需要独立语句?
考虑以下调用:
cpp
processWidget(std::shared_ptr<Widget>(new Widget), priority());
C++ 函数参数的求值顺序是不确定的。编译器可能按以下顺序执行:
new Widgetpriority()- 构造
std::shared_ptr<Widget>
如果 priority() 抛出异常,则 new Widget 返回的指针还没有被智能指针接管,导致资源泄漏。使用独立语句可以保证在调用 priority() 之前,new 出来的对象已经被智能指针管理。
最佳实践:
- 永远不要将
new与智能指针构造放在同一个表达式中。 - 使用独立语句:
std::shared_ptr<Widget> pw(new Widget);然后再调用函数。 - 优先使用
std::make_shared和std::make_unique(C++14),它们从根本上避免了这个问题。
适用场景:
- 所有使用智能指针管理资源的地方,尤其是作为函数参数时。
- 即使函数不抛出异常,也要养成良好习惯。
代码对比示例
cpp
// 文件名: clause17.cpp
// 编译: g++ -std=c++11 clause17.cpp -o clause17
#include <iostream>
#include <memory>
#include <stdexcept>
using namespace std;
// ========== 模拟一个 Widget 类 ==========
class Widget {
public:
Widget(int id = 0) : id(id) {
cout << "Widget 构造: " << id << endl;
}
~Widget() {
cout << "Widget 析构: " << id << endl;
}
void print() const {
cout << "Widget ID: " << id << endl;
}
private:
int id;
};
// ========== 模拟一个可能抛出异常的函数 ==========
int priority() {
cout << "计算优先级..." << endl;
// 模拟一个随机异常
static int count = 0;
if (++count == 1) {
cout << "priority() 抛出异常!" << endl;
throw runtime_error("优先级计算失败");
}
return 10;
}
// 处理 Widget 的函数
void processWidget(shared_ptr<Widget> pw, int priority) {
cout << "processWidget: 优先级=" << priority << endl;
pw->print();
}
// ========== 错误示例:合并语句导致资源泄漏 ==========
void testBad() {
cout << "\n=== 错误:new 与智能指针构造合并 ===" << endl;
try {
// 危险!编译器可能先 new Widget,再调用 priority(),再构造 shared_ptr
processWidget(shared_ptr<Widget>(new Widget(100)), priority());
// 如果 priority() 抛出异常,new Widget(100) 的指针未被接管,导致泄漏
} catch (const exception& e) {
cout << "捕获异常: " << e.what() << endl;
cout << "注意:Widget(100) 可能泄漏!" << endl;
}
}
// ========== 正确示例1:独立语句 ==========
void testGood() {
cout << "\n=== 正确:独立语句创建智能指针 ===" << endl;
try {
shared_ptr<Widget> pw(new Widget(200)); // 独立语句,资源立即被管理
processWidget(pw, priority()); // 即使 priority() 抛异常,pw 已安全
} catch (const exception& e) {
cout << "捕获异常: " << e.what() << endl;
cout << "Widget 已被智能指针管理,不会泄漏" << endl;
}
}
// ========== 正确示例2:使用 make_shared (C++11) ==========
void testMakeShared() {
cout << "\n=== 更佳:使用 make_shared (完全避免 new) ===" << endl;
try {
auto pw = make_shared<Widget>(300); // 没有显式 new,绝对安全
processWidget(pw, priority());
} catch (const exception& e) {
cout << "捕获异常: " << e.what() << endl;
cout << "make_shared 不会泄漏" << endl;
}
}
// ========== 正确示例3:使用 make_unique (C++14) ==========
// 为了兼容 C++11,手动实现简单 make_unique
template<typename T, typename... Args>
unique_ptr<T> make_unique(Args&&... args) {
return unique_ptr<T>(new T(forward<Args>(args)...));
}
void testMakeUnique() {
cout << "\n=== 使用 make_unique (C++14) ===" << endl;
try {
auto pw = make_unique<Widget>(400);
// 注意:processWidget 需要 shared_ptr,这里只是演示 unique_ptr
// 实际中应保持一致
cout << "make_unique 管理的 Widget: ";
pw->print();
// 如果 priority() 抛异常,也不会泄漏
int p = priority();
cout << "优先级: " << p << endl;
} catch (const exception& e) {
cout << "捕获异常: " << e.what() << endl;
cout << "make_unique 安全" << endl;
}
}
// ========== 演示异常安全性的更清晰示例 ==========
class Dangerous {
public:
Dangerous() { cout << "Dangerous 构造" << endl; }
~Dangerous() { cout << "Dangerous 析构" << endl; }
};
void riskyFunction(shared_ptr<Dangerous> d, int val) {
cout << "riskyFunction 被调用,val=" << val << endl;
}
int thrower() {
cout << "thrower 抛出异常" << endl;
throw runtime_error("抛出异常");
return 0;
}
void demonstrateLeak() {
cout << "\n=== 泄漏演示:合并语句 ===" << endl;
try {
// 危险顺序:new Dangerous -> thrower() -> shared_ptr构造
riskyFunction(shared_ptr<Dangerous>(new Dangerous), thrower());
} catch (...) {
cout << "捕获异常,但 Dangerous 对象泄漏了!" << endl;
}
}
void demonstrateSafe() {
cout << "\n=== 安全演示:独立语句 ===" << endl;
try {
shared_ptr<Dangerous> d(new Dangerous);
riskyFunction(d, thrower());
} catch (...) {
cout << "捕获异常,Dangerous 被智能指针自动释放" << endl;
}
}
int main() {
cout << "========== 条款17:以独立语句将 newed 对象置入智能指针 ==========" << endl;
// 危险示例:可能泄漏,但为了演示,先调用一次(第一次 priority 会抛异常)
testBad(); // 可能泄漏
testGood(); // 安全
testMakeShared(); // 安全且更优
testMakeUnique(); // 安全
demonstrateLeak(); // 演示泄漏过程
demonstrateSafe(); // 演示安全做法
cout << "\n========== 程序结束 ==========" << endl;
return 0;
}
运行结果示例
========== 条款17:以独立语句将 newed 对象置入智能指针 ==========
=== 错误:new 与智能指针构造合并 ===
计算优先级...
priority() 抛出异常!
捕获异常: 优先级计算失败
注意:Widget(100) 可能泄漏!
=== 正确:独立语句创建智能指针 ===
Widget 构造: 200
计算优先级...
priority() 抛出异常!
捕获异常: 优先级计算失败
Widget 已被智能指针管理,不会泄漏
Widget 析构: 200
=== 更佳:使用 make_shared (完全避免 new) ===
Widget 构造: 300
计算优先级...
priority() 抛出异常!
捕获异常: 优先级计算失败
make_shared 不会泄漏
Widget 析构: 300
=== 使用 make_unique (C++14) ===
Widget 构造: 400
make_unique 管理的 Widget: Widget ID: 400
计算优先级...
priority() 抛出异常!
捕获异常: 优先级计算失败
make_unique 安全
Widget 析构: 400
=== 泄漏演示:合并语句 ===
Dangerous 构造
thrower 抛出异常
捕获异常,但 Dangerous 对象泄漏了!
=== 安全演示:独立语句 ===
Dangerous 构造
thrower 抛出异常
捕获异常,Dangerous 被智能指针自动释放
Dangerous 析构
========== 程序结束 ==========
关键要点解析
1. 问题根源:函数参数求值顺序不确定
C++ 标准未规定函数参数的求值顺序。编译器可以自由选择:
- 先
new Widget,再priority(),最后构造shared_ptr。 - 或其他顺序。
如果 priority() 在 shared_ptr 构造之前抛出异常,new 出来的原始指针永远不会被智能指针接管,资源泄漏。
2. 错误示例的后果
cpp
processWidget(shared_ptr<Widget>(new Widget(100)), priority());
- 假设顺序:
new Widget(100)→priority()(抛异常)→ 构造shared_ptr被跳过。 - 结果:
Widget(100)的指针丢失,无法释放。
3. 正确做法:独立语句
cpp
shared_ptr<Widget> pw(new Widget(200));
processWidget(pw, priority());
new Widget(200)的结果立即被pw接管。- 即使
priority()抛异常,pw的析构函数会在栈展开时被调用,释放资源。
4. 最佳实践:使用 make_shared / make_unique
cpp
auto pw = make_shared<Widget>(300);
- 没有显式
new,资源分配和智能指针构造在同一个函数内完成,不会出现"裸指针"暴露阶段。 - 更安全、更高效(一次内存分配,异常安全)。
5. 为什么 make_shared 是异常安全的?
make_shared 内部将 Widget 对象和控制块(引用计数)分配在同一个内存块中,并在函数返回前完成智能指针的初始化。整个过程是原子的,不会暴露原始指针。
6. 注意点
- 如果必须使用
new,请务必使用独立语句。 - 不要将
new与智能指针构造放在同一个表达式中,即使你认为priority()不会抛出异常。 - 对于
std::unique_ptr,同样适用。 - C++17 虽然规定了函数参数求值顺序(每个参数完整求值后才进入下一个),但不同参数之间的顺序仍然未定,所以风险依然存在。
总结
- 以独立语句将
new出的对象置入智能指针 :永远不要将new和智能指针构造放在同一个表达式中。 - 原因:防止编译器重排求值顺序导致异常时资源泄漏。
- 最佳实践 :优先使用
std::make_shared和std::make_unique,它们从根本上消除了这个问题。 - 如果必须使用
new,请写独立语句:shared_ptr<T> ptr(new T(...));然后再使用。 - 记住 :资源管理需要异常安全。让每一个
new的结果立即被 RAII 对象接管,不留间隙。