EffctiveC++_第三章_资源管理

文章目录

    • 条款13:以对象管理资源
      • 说明与介绍
      • 代码对比示例
      • 运行结果示例
      • 关键要点解析
        • [1. 手动管理资源的问题](#1. 手动管理资源的问题)
        • [2. 智能指针的优势](#2. 智能指针的优势)
        • [3. 智能指针的选择](#3. 智能指针的选择)
        • [4. RAII 的核心思想](#4. RAII 的核心思想)
        • [5. 最佳实践](#5. 最佳实践)
        • [6. 现代 C++ 的演进](#6. 现代 C++ 的演进)
      • 总结
    • [条款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:在资源管理类中提供对原始资源的访问
      • 说明与介绍
      • 代码对比示例
      • 运行结果示例
      • 关键要点解析
        • [1. 为什么需要提供原始资源访问?](#1. 为什么需要提供原始资源访问?)
        • [2. 两种访问方式的对比](#2. 两种访问方式的对比)
        • [3. 显式 `get()` 的优点](#3. 显式 get() 的优点)
        • [4. 隐式转换的陷阱](#4. 隐式转换的陷阱)
        • [5. 最佳实践](#5. 最佳实践)
        • [6. 标准库的实践](#6. 标准库的实践)
        • [7. 自定义 RAII 类的设计建议](#7. 自定义 RAII 类的设计建议)
      • 总结
    • [条款16:成对使用 `new` 和 `delete` 时要采取相同形式](#条款16:成对使用 newdelete 时要采取相同形式)
    • [条款17:以独立语句将 `new` 出的对象置入智能指针](#条款17:以独立语句将 new 出的对象置入智能指针)
      • 说明与介绍
      • 代码对比示例
      • 运行结果示例
      • 关键要点解析
        • [1. 问题根源:函数参数求值顺序不确定](#1. 问题根源:函数参数求值顺序不确定)
        • [2. 错误示例的后果](#2. 错误示例的后果)
        • [3. 正确做法:独立语句](#3. 正确做法:独立语句)
        • [4. 最佳实践:使用 `make_shared` / `make_unique`](#4. 最佳实践:使用 make_shared / make_unique)
        • [5. 为什么 `make_shared` 是异常安全的?](#5. 为什么 make_shared 是异常安全的?)
        • [6. 注意点](#6. 注意点)
      • 总结

条款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::fstreamstd::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_ptrshared_ptrweak_ptr
  • C++14 提供 std::make_unique
  • C++17 强化了 shared_ptr 对数组的支持。
  • 建议 :新代码中完全避免裸 new/delete,使用智能指针。

总结

  • 以对象管理资源:利用 RAII 技术,让对象的析构函数自动释放资源。
  • 使用智能指针std::unique_ptrstd::shared_ptr 是 RAII 的最佳实践。
  • 避免手动资源管理 :手动 new/delete 容易导致泄漏和异常不安全。
  • 记住 :资源管理应该由对象负责,而不是程序员手动操作。使用智能指针,让 C++ 为你自动清理。

条款14:在资源管理类中小心 copying 行为

说明与介绍

核心思想:当你创建自己的资源管理类(RAII 类)时,需要仔细考虑当对象被复制(拷贝构造或拷贝赋值)时应该发生什么。因为编译器生成的默认拷贝函数只会做浅拷贝(逐成员复制),这可能不是你想要的。你必须根据自己的资源管理策略,明确决定复制行为。

为什么需要小心?

  • 资源管理类负责释放资源(如内存、锁、文件句柄)。如果简单复制,可能导致同一资源被释放两次(双重删除),或者资源的所有权混乱。
  • 不同的资源类型需要不同的复制策略:例如互斥锁不能复制,堆内存可以深拷贝或转移所有权。

四种常见的复制策略

  1. 禁止复制 :如果资源不允许被多个对象共享(如 unique_ptr、互斥锁),应该将拷贝操作声明为 = delete(或 C++98 中声明为 private 且不实现)。
  2. 引用计数 :通过引用计数跟踪有多少对象共享同一资源,当计数为 0 时释放资源(如 shared_ptr)。
  3. 深拷贝 :复制底层资源本身(如 stringvector 的拷贝语义)。
  4. 转移所有权 :将资源所有权从一个对象转移到另一个对象(如 auto_ptrunique_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_ptrstd::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::stringstd::vector
6. 策略4:转移所有权(移动语义)
  • 使用移动构造和移动赋值,将资源所有权从一个对象转移到另一个。
  • 移动后,源对象不再管理资源(通常置为 nullptr)。
  • 典型应用std::unique_ptr
7. 现代 C++ 的最佳实践
  • 优先使用标准库的 RAII 类std::unique_ptrstd::shared_ptrstd::lock_guardstd::fstream 等,避免自己编写资源管理类。
  • 如果必须自定义资源管理类,应遵循"三/五法则"(Rule of Three/Five),并明确选择上述四种策略之一。
  • C++11 的移动语义大大简化了转移所有权的实现。

总结

  • 在资源管理类中小心 copying 行为:编译器生成的默认拷贝函数通常不是你想要的。
  • 明确选择复制策略:禁止复制、引用计数、深拷贝或转移所有权,并在类中显式实现。
  • 优先使用标准库组件std::unique_ptrstd::shared_ptrstd::lock_guard 等已经实现了正确的复制/移动语义。
  • 记住 :资源管理类的复制行为直接影响资源的安全性和效率。仔细考虑你的资源所有权模型,并选择最适合的策略。

条款15:在资源管理类中提供对原始资源的访问

说明与介绍

核心思想 :资源管理类(如 std::shared_ptrstd::unique_ptr 或自定义 RAII 类)通过封装资源来确保自动释放。但有时需要访问原始资源(例如与旧 C API 交互,或调用不支持智能指针的库函数)。此时,RAII 类必须提供一个安全的方式来获取其管理的原始资源。

为什么需要提供原始资源访问?

  • 兼容旧代码:许多 C++ 库(尤其是系统 API)仍使用原始指针或句柄,不接受智能指针。
  • 性能要求:某些场景下,直接操作原始资源可能比通过 RAII 类再封装更高效(尽管这种情况较少)。
  • 灵活性:有时需要调用资源特有的成员函数,而 RAII 类没有提供相应的接口。

两种访问方式

  1. 显式转换 :提供一个 get() 成员函数,返回原始资源(如 shared_ptr::get())。这是安全且推荐的方式,因为用户明确知道在获取原始资源。
  2. 隐式转换 :提供 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:成对使用 newdelete 时要采取相同形式

说明与介绍

核心思想 :当你使用 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::vectorstd::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::arraystd::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. 最佳实践总结
  • 永远配对newdeletenew[]delete[]
  • 避免手动管理 :使用 vectorstring、智能指针。
  • 警惕 typedef :如果必须定义数组类型,使用 using + std::array
  • 代码审查 :当看到 newdelete 时,立即检查配对形式。

总结

  • 成对使用 newdelete 时要采取相同形式 :单个对象用 delete,数组用 delete[]
  • 混用导致未定义行为:可能崩溃、泄漏、数据损坏。
  • 优先使用现代 C++ 特性std::vectorstd::unique_ptr<T[]> 可以完全避免手动配对。
  • 记住 :C++ 不帮你检查配对,你必须自己保证。使用 RAII 容器和智能指针,让编译器替你管理资源。

条款17:以独立语句将 new 出的对象置入智能指针

说明与介绍

核心思想 :当你使用智能指针(如 std::shared_ptrstd::unique_ptr)管理动态资源时,应确保在单独语句中完成对象的 new 和智能指针的构造。如果合并在一起,可能因为编译器重排语句顺序而导致资源泄漏。

为什么需要独立语句?

考虑以下调用:

cpp 复制代码
processWidget(std::shared_ptr<Widget>(new Widget), priority());

C++ 函数参数的求值顺序是不确定的。编译器可能按以下顺序执行:

  1. new Widget
  2. priority()
  3. 构造 std::shared_ptr<Widget>

如果 priority() 抛出异常,则 new Widget 返回的指针还没有被智能指针接管,导致资源泄漏。使用独立语句可以保证在调用 priority() 之前,new 出来的对象已经被智能指针管理。

最佳实践

  • 永远不要将 new 与智能指针构造放在同一个表达式中。
  • 使用独立语句:std::shared_ptr<Widget> pw(new Widget); 然后再调用函数。
  • 优先使用 std::make_sharedstd::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_sharedstd::make_unique,它们从根本上消除了这个问题。
  • 如果必须使用 new ,请写独立语句:shared_ptr<T> ptr(new T(...)); 然后再使用。
  • 记住 :资源管理需要异常安全。让每一个 new 的结果立即被 RAII 对象接管,不留间隙。
相关推荐
vb2008114 分钟前
FastAPI APIRouter
开发语言·python
Benszen6 分钟前
KVM虚拟化解决方案
开发语言·perl
会编程的土豆8 分钟前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木10 分钟前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
我叫袁小陌19 分钟前
算法解题思路指南
算法
MC皮蛋侠客21 分钟前
C++17 多线程系列(五):C++17 并行算法——从串行到并行的零成本迁移
c++·多线程
地平线开发者24 分钟前
Conv+BN+Add+ReLU 融合机制简介
算法·自动驾驶
yuanyuan2o234 分钟前
模型预训练:Hugging Face Transformers 基础
算法·ai·语言模型·自然语言处理·nlp·深度优先
杨充42 分钟前
1.3 浮点型数据设计灵魂
开发语言·python·算法