目录

一、核心概念:RAII 与内存泄漏

为什么要引入智能指针?

传统方式的痛点(代码演示)

二、废弃的设计:auto_ptr

三、独占式管理:unique_ptr

原理与特点

代码演示:所有权转移

四、共享式管理:shared_ptr

原理与特点

代码演示:引用计数

五、辅助型管理:weak_ptr

原理与作用

代码演示:解决循环引用

六、 进阶:定制删除器

原理

解决方案

七、 总结对比表

八、最佳实践建议


在 C++ 中,动态内存管理(new/delete)如果处理不当,极易引发内存泄漏,尤其是在程序抛出异常时。智能指针的核心作用就是利用 RAII(资源获取即初始化) 机制,自动管理内存,防止泄漏。


一、核心概念:RAII 与内存泄漏

为什么要引入智能指针?

在传统的 C++ 开发中,我们使用 new 分配内存,delete 释放内存。但这存在一个巨大的隐患:异常安全

如果在 newdelete 之间代码抛出了异常,函数会立即退出(栈展开),导致 delete 语句无法执行,从而造成内存泄漏

智能指针的核心作用就是利用 RAII(资源获取即初始化) 机制:

  • 构造时获取资源:在智能指针对象的构造函数中申请内存。
  • 析构时释放资源:在智能指针对象的析构函数中释放内存。
  • 自动管理:只要智能指针对象被销毁(例如出了作用域),它管理的内存就会自动被释放,无论是否发生异常。

传统方式的痛点(代码演示)

这段代码展示了如果不使用智能指针,当程序发生异常时,内存是如何泄漏的。

#include <iostream>
#include <stdexcept>

void traditionalWay() {
    int* ptr = new int(10);
    std::cout << "分配内存: " << *ptr << std::endl;

    // 模拟程序运行中抛出了异常
    throw std::runtime_error("发生异常!");

    // ⚠️ 这一行永远不会执行,内存泄漏!
    delete ptr; 
}

int main() {
    try {
        traditionalWay();
    } catch (const std::exception& e) {
        std::cout << "捕获异常: " << e.what() << std::endl;
        std::cout << "注意:上面的内存已经泄漏了。" << std::endl;
    }
    return 0;
}

二、废弃的设计:auto_ptr

  • 原理管理权转移。当发生拷贝或赋值时,资源的所有权会从源对象转移到目标对象,源对象会被置空(nullptr)。
  • 问题:这种机制非常危险,因为如果你在后续代码中继续使用源对象(此时已悬空),程序会崩溃。
  • 结论不要使用,C++11 已将其废弃。

三、独占式管理:unique_ptr

原理与特点

  • 原理简单粗暴。它直接禁用了拷贝构造和赋值操作(或者通过移动语义实现),保证同一时刻只有一个 unique_ptr 对象拥有该资源。
  • 特点:轻量、高效,没有额外的运行时开销。
  • 适用场景:当你不需要共享资源,只需要一个对象独占管理内存时使用。

代码演示:所有权转移

unique_ptr 禁止拷贝,但支持移动。这意味着资源的所有权是可以“转让”的,但不能“复制”。

#include <iostream>
#include <memory>

void uniquePtrDemo() {
    // 1. 创建 (推荐使用 std::make_unique)
    std::unique_ptr<int> ptr1 = std::make_unique<int>(100);
    std::cout << "ptr1 的值: " << *ptr1 << std::endl;

    // 2. 尝试拷贝 -> ❌ 编译报错!
    // std::unique_ptr<int> ptr2 = ptr1; 

    // 3. 移动所有权 -> ✅ 合法
    std::unique_ptr<int> ptr2 = std::move(ptr1);

    // 4. 检查状态
    if (ptr1 == nullptr) {
        std::cout << "ptr1 已空 (所有权已转移)" << std::endl;
    }
    if (ptr2) {
        std::cout << "ptr2 接管了资源,值: " << *ptr2 << std::endl;
    }
    // ptr2 出作用域,自动 delete
}

四、共享式管理:shared_ptr

原理与特点

  • 原理引用计数。内部维护一个计数器,记录有多少个 shared_ptr 对象共享同一个资源。
    • 每增加一个引用,计数器 +1。
    • 每减少一个引用,计数器 -1。
    • 当计数器变为 0 时,自动释放资源。
  • 线程安全shared_ptr 的引用计数是线程安全的(原子操作),但它管理的对象本身不是线程安全的
  • 缺点:存在**循环引用(Circular Reference)**的问题。

代码演示:引用计数

#include <iostream>
#include <memory>

void sharedPtrDemo() {
    std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
    std::cout << "初始引用计数: " << ptr1.use_count() << std::endl; // 输出 1

    {
        std::shared_ptr<int> ptr2 = ptr1; // 拷贝,引用计数 +1
        std::cout << "拷贝后引用计数: " << ptr1.use_count() << std::endl; // 输出 2
        std::cout << "ptr2 的值: " << *ptr2 << std::endl;
        
        // ptr2 在这里析构,引用计数 -1
    }

    std::cout << "ptr2 销毁后引用计数: " << ptr1.use_count() << std::endl; // 输出 1
}

五、辅助型管理:weak_ptr

原理与作用

  • 原理弱引用。它不增加引用计数,只是“观察” shared_ptr 管理的资源。
  • 作用:专门用来解决 shared_ptr循环引用问题。它指向资源但不拥有资源,因此不会阻止资源被释放。
  • 使用:通常用于双向链表、树结构的父/子节点关系中,打破循环。

代码演示:解决循环引用

场景:A 对象持有 B 的指针,B 对象持有 A 的指针。如果都用 shared_ptr,它们互相引用,计数永远不为 0,导致内存泄漏。

#include <iostream>
#include <memory>

class B; // 前向声明

class A {
public:
    // ⚠️ 错误示范:如果这里用 shared_ptr<B>,会导致循环引用
    // std::shared_ptr<B> child; 
    
    // ✅ 正确示范:使用 weak_ptr 打破循环
    std::weak_ptr<B> child; 
    
    ~A() { std::cout << "A 被销毁" << std::endl; }
};

class B {
public:
    std::shared_ptr<A> parent; // B 强引用 A
    ~B() { std::cout << "B 被销毁" << std::endl; }
};

void cycleDemo() {
    {
        auto ptrA = std::make_shared<A>();
        auto ptrB = std::make_shared<B>();

        ptrA->child = ptrB; // A 弱引用 B
        ptrB->parent = ptrA; // B 强引用 A

        std::cout << "A 的引用计数: " << ptrA.use_count() << std::endl; // 2 (ptrA + ptrB->parent)
        std::cout << "B 的引用计数: " << ptrB.use_count() << std::endl; // 1 (ptrB)
    }
    // 出了作用域,ptrA 和 ptrB 计数减 1
    // A 的计数变为 1 (被 B 持有),但因为 B 的计数变为 0,B 被销毁
    // B 销毁时,A 的计数减为 0,A 也被销毁。
    // 完美!
}

六、 进阶:定制删除器

原理

默认情况下,智能指针使用 delete 来释放内存。但如果你使用了 mallocnew[](数组)或者需要关闭文件句柄等特殊资源,标准的 delete 是无法处理的。

解决方案

可以给 shared_ptr 传递一个“删除器”(可以是函数指针、Lambda 表达式或仿函数),告诉它在释放时具体该执行什么操作。

#include <iostream>
#include <memory>

void customDeleterDemo() {
    // 1. 使用数组 new
    // 注意:shared_ptr 默认不知道要用 delete[],必须指定
    std::shared_ptr<int> ptr(new int[5], std::default_delete<int[]>());
    
    // 或者使用 Lambda 表达式作为删除器
    auto deleter = [](int* p) {
        std::cout << "执行自定义删除逻辑..." << std::endl;
        delete[] p;
    };
    
    std::shared_ptr<int> ptr2(new int[10], deleter);
    
    // ptr2 销毁时,会自动调用 deleter(ptr2.get())
}

七、 总结对比表

为了方便记忆,这里整理了这三种主要智能指针的对比:

智能指针 核心机制 是否可拷贝 适用场景 缺点/注意
unique_ptr 独占所有权 不可 (禁止拷贝) 资源不需要共享时 无法共享数据
shared_ptr 引用计数 (共享所有权) 需要多处共享资源时 有循环引用风险,性能稍低
weak_ptr 弱引用 配合 shared_ptr 打破循环 不能直接操作资源,需转换

八、最佳实践建议

  1. 优先使用 unique_ptr:如果你确定资源不需要被共享,这是最安全、最高效的选择。
  2. 需要共享时使用 shared_ptr:但要时刻警惕循环引用的风险。
  3. 解决循环引用使用 weak_ptr:在双向关联(如父子节点、双向链表)中,将一端声明为 weak_ptr
  4. 彻底遗忘 auto_ptr:它是一个失败的设计,不要在代码中使用。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐