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 对象接管,不留间隙。
相关推荐
蚊子码农2 小时前
每日一题--C语言指针与内存泄漏:一道小问题的深度复盘
c语言·开发语言
Fanfanaas2 小时前
Linux 系统编程 进程篇(一)
linux·运维·服务器·c语言·开发语言·网络·学习
星辰徐哥2 小时前
ARP缓存表:作用、查看方法与刷新技巧
开发语言·缓存·php
ego.iblacat2 小时前
lvs 集群部署
开发语言·php·lvs
沐雪轻挽萤2 小时前
6. C++17新特性-编译期 if 语句 (if constexpr)
开发语言·c++
水云桐程序员2 小时前
C语言编程基础,输入与输出
c语言·开发语言·算法
ZPC82102 小时前
MoveIt Servo 与自己编写的 Action Server 通信
人工智能·算法·机器人
爱代码的小黄人2 小时前
MATLAB中for循环实现递减遍历(通用方法)
开发语言·matlab
jllllyuz2 小时前
采用核函数的极限学习机(KELM)MATLAB实现
算法