头文件
RAII 是 C++ 核心编程思想,智能指针则是基于 RAII 实现的内存自动管理工具,用来解决 C++ 原生裸指针内存泄漏、野指针、重复释放等经典问题,也是 C++11 及现代 C++ 必备知识点。


一、什么是内存泄漏 & 裸指针痛点

1. 裸指针的常见问题

C++ 原生 new / delete 手动管理堆内存,全程需要开发者自己把控,极易出错:

  1. 内存泄漏new 开辟内存后,忘记调用 delete,程序退出前内存无法回收。
  2. 重复释放:同一块内存多次 delete,直接导致程序崩溃。
  3. 野指针:内存释放后,指针指向随机值但是继续被使用,访问非法内存。
  4. 悬空指针:某内存空间的支配权还给操作系统后,依然访问该空间内存,这是一种无权访问
  5. 分支/异常导致漏释放:代码分支多、抛出异常时,delete 无法执行。

示例:典型内存泄漏场景

#include <iostream>
using namespace std;

void test()
{
    // 手动在堆上开辟内存
    int* p = new int(100);

    // 模拟业务逻辑,中途 return,跳过 delete
    if (true)
    {
        return; 
    }

    // 永远执行不到,内存泄漏
    delete p;
}

int main()
{
    test();
    return 0;
}

问题:函数提前返回,delete 未执行,堆内存无人回收。


二、RAII 核心思想

1. 全称

RAII:Resource Acquisition Is Initialization
中文直译:资源获取即初始化

2. 核心原理

利用 C++ 类的构造函数、析构函数的生命周期特性 管理资源:

  1. 构造函数:对象创建时,自动获取/申请资源(堆内存、文件、锁、网络句柄等)。
  2. 析构函数:对象生命周期结束(出作用域)时,自动释放资源

核心精髓:资源的生命周期 和 栈对象的生命周期绑定,不用手动释放。
栈对象由编译器自动管理,出作用域必然调用析构,资源一定被释放

3. 手写简易 RAII 内存管理类(看懂本质)

我们自己封装一个类,模拟 RAII 管理堆内存:

#include <iostream>
using namespace std;

// 基于 RAII 封装内存管理
class MemGuard
{
private:
    int* ptr; // 托管的裸指针
public:
    // 构造函数:创建对象 → 申请资源
    MemGuard(int* p) : ptr(p)
    {
        cout << "构造:接管堆内存" << endl;
    }

    // 析构函数:对象销毁 → 自动释放资源
    ~MemGuard()
    {
        cout << "析构:自动释放堆内存" << endl;
        delete ptr;
        ptr = nullptr;
    }

    // 重载 * 运算符,像普通指针一样使用
    int& operator*()
    {
        return *ptr;
    }
};

void test()
{
    int* raw_p = new int(999);
    // 栈对象 guard,生命周期在当前函数内
    MemGuard guard(raw_p); 

    if (true)
    {
        return; // 函数提前退出
    }
    // 无需手动 delete
}

int main()
{
    test(); 
    // guard 出作用域,析构函数自动执行,释放内存
    return 0;
}

运行逻辑:

  1. guard栈对象,函数结束/提前 return 都会被销毁。
  2. 销毁时自动执行析构函数,执行 delete彻底避免内存泄漏

智能指针,本质就是 C++ 标准库提前写好的、通用 RAII 资源管理类


三、智能指针总览

C++11 标准库提供三种主流智能指针,全部定义在 <memory> 头文件:

智能指针 作用 所有权规则 C++ 版本
std::unique_ptr 独占式智能指针(推荐首选) 独占所有权,不能拷贝,只可移动 C++11
std::shared_ptr 共享式智能指针 引用计数,多指针共享同一块内存 C++11
std::weak_ptr 弱引用指针 配合 shared_ptr,不增加引用计数,解决循环引用 C++11

通用特性:

  • 重载了 *->,用法和裸指针几乎一致。
  • 基于 RAII,出作用域自动释放内存。

四、std::unique_ptr 独占智能指针(最常用)

1. 核心特点

  1. 独占所有权:同一堆内存,只能被一个 unique_ptr 托管
  2. 禁止拷贝构造、拷贝赋值(防止多个指针共管同一块内存)。
  3. 支持移动语义(std::move 转移所有权)。
  4. 开销极小,性能接近裸指针,日常开发首选

2. 基本用法

2.1 创建与初始化
#include <iostream>
#include <memory>
using namespace std;

int main()
{
    // 方式1:直接接收 new 出来的裸指针
    unique_ptr<int> p1(new int(100));

    // 方式2(C++14 推荐):make_unique,更安全、简洁
    auto p2 = make_unique<int>(200);

    // 像裸指针一样解引用
    cout << *p1 << endl; // 100
    cout << *p2 << endl; // 200

    return 0;
}
2.2 禁止拷贝,支持移动
int main()
{
    unique_ptr<int> p1 = make_unique<int>(10);
    
    // unique_ptr<int> p2 = p1;  // 编译报错!禁止拷贝

    // 使用 std::move 转移所有权(移动语义)
    unique_ptr<int> p2 = move(p1); 

    // 转移后,原指针 p1 变为空(不再托管内存)
    if (p1 == nullptr)
    {
        cout << "p1 已空" << endl;
    }
    cout << *p2 << endl; // 10

    return 0;
}
2.3 常用成员函数
unique_ptr<int> p = make_unique<int>(666);

p.get();        // 获取内部托管的裸指针
p.release();    // 释放管理权(返回裸指针,智能指针置空,**不会 delete**)
p.reset();      // 释放当前内存,指针置空
p.reset(new int(888)); // 释放旧内存,托管新内存

3. 使用场景

  1. 绝大多数单个对象、数组内存管理。
  2. 函数局部变量、类成员指针(优先使用)。
  3. 容器中存储指针。

五、std::shared_ptr 共享智能指针

1. 核心特点

  1. 共享所有权:允许多个 shared_ptr 同时托管同一块堆内存
  2. 引用计数机制:内部维护一个计数器:
    • 新增一个 shared_ptr → 计数 +1
    • 一个 shared_ptr 销毁 → 计数 -1
    • 计数变为 0 时,自动释放堆内存
  3. 支持拷贝、赋值、移动。
  4. 相比 unique_ptr 有少量计数开销,适合多指针共享资源场景。

2. 基本用法

2.1 创建与引用计数变化
#include <iostream>
#include <memory>
using namespace std;

int main()
{
    // 推荐写法:make_shared(效率更高)
    shared_ptr<int> p1 = make_shared<int>(100);
    cout << "计数:" << p1.use_count() << endl; // 计数 = 1

    // 拷贝,共享内存,计数 +1
    shared_ptr<int> p2 = p1;
    cout << "计数:" << p1.use_count() << endl; // 计数 = 2

    {
        // 局部 shared_ptr,出作用域自动销毁
        shared_ptr<int> p3 = p1;
        cout << "计数:" << p1.use_count() << endl; // 计数 = 3
    }
    // p3 销毁,计数 -1
    cout << "计数:" << p1.use_count() << endl; // 计数 = 2

    return 0;
}

执行流程:函数结束,p1p2 依次销毁,计数减为 0,内存自动释放。

2.2 常用成员函数
shared_ptr<int> p = make_shared<int>(10);

p.use_count();  // 获取当前引用计数
p.reset();      // 放弃托管,计数-1,指针置空
p.get();        // 获取内部裸指针

3. 致命问题:循环引用(内存泄漏)

shared_ptr 最大坑点:两个对象互相使用 shared_ptr 指向对方,造成循环引用,计数永远不为0,内存泄漏

循环引用示例
#include <iostream>
#include <memory>
using namespace std;

class B;
class A
{
public:
    shared_ptr<B> pb;
    ~A() { cout << "A 析构" << endl; }
};

class B
{
public:
    shared_ptr<A> pa;
    ~B() { cout << "B 析构" << endl; }
};

int main()
{
    shared_ptr<A> a = make_shared<A>();
    shared_ptr<B> b = make_shared<B>();

    // 互相指向对方:循环引用
    a->pb = b;
    b->pa = a;

    // 函数结束,a、b 销毁,但内部互相引用,计数无法归0
    // A、B 的析构函数永远不执行 → 内存泄漏
    return 0;
}

解决方案:使用 std::weak_ptr 弱指针打破循环。


六、std::weak_ptr 弱引用指针

1. 核心特点

  1. 弱引用:专门配合 shared_ptr 使用。
  2. 不增加引用计数:只观察资源,不拥有资源所有权。
  3. 可以检测 shared_ptr 托管的内存是否已经释放。
  4. 不能直接解引用使用,必须先提升为 shared_ptr

2. 作用

  1. 解决 shared_ptr 循环引用问题。
  2. 做“旁观者”,判断资源是否有效。

3. 修复上面的循环引用

将其中一个 shared_ptr 改为 weak_ptr

#include <iostream>
#include <memory>
using namespace std;

class B;
class A
{
public:
    shared_ptr<B> pb;
    ~A() { cout << "A 析构" << endl; }
};

class B
{
public:
    // 改为弱指针,不增加引用计数
    weak_ptr<A> pa; 
    ~B() { cout << "B 析构" << endl; }
};

int main()
{
    shared_ptr<A> a = make_shared<A>();
    shared_ptr<B> b = make_shared<B>();

    a->pb = b;
    b->pa = a;

    // 正常执行析构,内存释放
    return 0;
}

4. 常用用法

shared_ptr<int> sp = make_shared<int>(10);
weak_ptr<int> wp = sp;

// 1. 判断资源是否存在
if (!wp.expired()) 
{
    // 2. 弱指针提升为 shared_ptr 再使用
    shared_ptr<int> temp = wp.lock();
    cout << *temp << endl;
}
  • expired():判断指向的内存是否已释放(true=已释放)。
  • lock():尝试提升为 shared_ptr,资源失效则返回空指针。

七、智能指针与裸指针对比 & 选择建议

1. 优缺点对比

类型 优点 缺点
裸指针 无开销、灵活 手动管理内存,易泄漏、野指针、重复释放
unique_ptr 零额外开销、安全、语法简单 不能拷贝,仅独占使用
shared_ptr 支持共享所有权 引用计数有性能开销,易出现循环引用
weak_ptr 打破循环引用、安全观察 不能单独使用,依赖 shared_ptr

2. 开发选择原则

  1. 优先使用 unique_ptr:绝大多数场景首选,性能最优,安全。
  2. 必须多指针共享同一块内存时,使用 shared_ptr
  3. 使用 shared_ptr 且存在互相引用时,搭配 weak_ptr 打破循环。
  4. 尽量少用裸指针;迫不得已使用裸指针时,保证 newdelete 成对出现。
  5. 推荐使用 make_unique / make_shared 创建智能指针,比直接 new 更安全。

八、新手常见误区总结

  1. RAII 理解误区
    RAII 靠栈对象生命周期自动释放资源,智能指针必须定义在栈上,不要用 new 创建智能指针。

  2. unique_ptr 拷贝误区
    unique_ptr 禁止拷贝,只能用 std::move 转移所有权。

  3. shared_ptr 循环引用
    两个类互相持有 shared_ptr 必然内存泄漏,务必用 weak_ptr 解决。

  4. 不要混用裸指针和智能指针
    同一块内存不要同时被裸指针和多个智能指针共管,极易重复释放。

  5. weak_ptr 不能直接解引用
    弱指针只是观察者,必须通过 lock() 转为 shared_ptr 才能使用。


九、全文精简总结

  1. RAII 思想:资源获取即初始化,利用栈对象构造/析构自动管理资源,是智能指针的底层原理。
  2. 智能指针本质:基于 RAII 封装的指针类,自动释放堆内存,规避裸指针缺陷。
  3. unique_ptr:独占模式,不可拷贝、可移动,性能最高,日常首选。
  4. shared_ptr:共享模式,引用计数管理,多指针共用资源。
  5. weak_ptr:弱引用,不增加计数,专门解决 shared_ptr 循环引用问题。
  6. 现代 C++ 规范:能不用裸指针就不用裸指针,优先全套智能指针。

更多推荐