【C++】 智能指针


C++ 智能指针完整详解

前言

**在 C++ 裸指针开发中,内存泄漏、野指针、重复释放、异常安全等问题长期困扰开发者。C++11 引入智能指针(Smart Pointer),基于****RAII(资源获取即初始化)**思想自动管理堆内存生命周期,程序退出作用域时自动释放内存,彻底规避手动 new/delete 带来的各类内存隐患。

标准库智能指针全部定义于头文件**<memory>****,核心三类:std::unique_ptrstd::shared_ptrstd::weak_ptr。**

本文从原理、API、场景、坑点、性能对比完整拆解。


一、核心基础:RAII 机制

智能指针本质是栈上封装裸指针的模板类,核心逻辑 RAII:

  1. 构造函数:接管堆内存(new 出来的对象);
  2. 析构函数:自动调用 delete 释放内存;
  3. 离开作用域(函数返回、代码块结束、异常抛出)时,栈对象自动销毁,析构触发内存释放。

对比裸指针痛点:

cpp 复制代码
// 裸指针问题:异常会导致内存泄漏
void bad_func() {
    int* p = new int(10);
    throw runtime_error("error"); // 抛出异常,delete 永远不会执行,内存泄漏
    delete p;
}

// unique_ptr 自动兜底,异常也会释放内存
void good_func() {
    unique_ptr<int> p(new int(10));
    throw runtime_error("error"); // 栈对象销毁,自动释放内存
}

二、std::unique_ptr 独占智能指针(推荐优先使用)

1. 核心特性

独占所有权:同一时刻只能有一个 unique_ptr 持有对象,禁止拷贝,仅支持移动语义。

  • 开销极低:底层仅封装一个裸指针,无额外引用计数;
  • 性能几乎等同于裸指针,日常开发首选;
  • 不支持拷贝构造 / 赋值,仅支持 std::move 转移所有权。

2. 基础用法

(1)创建方式
cpp 复制代码
#include <memory>
#include <iostream>
using namespace std;

// 方式1:new 初始化(不推荐)
unique_ptr<int> p1(new int(100));

// 方式2:make_unique(C++14 推荐,异常安全、简洁)
auto p2 = make_unique<string>("hello unique");

// 管理数组
auto arr = make_unique<int[]>(5); // 长度5的int数组
arr[0] = 1;
(2)移动所有权
cpp 复制代码
unique_ptr<int> a = make_unique<int>(10);
// unique_ptr<int> b = a; // 编译报错,禁止拷贝

unique_ptr<int> b = move(a); // 转移所有权
cout << (a == nullptr) << endl; // true,a 已置空
cout << *b << endl; // 10
(3)常用成员函数
cpp 复制代码
auto p = make_unique<int>(666);

p.get();        // 返回内部裸指针 int*
p.release();    // 释放所有权,返回裸指针,unique_ptr 置空(需手动delete)
p.reset();      // 销毁当前管理对象,置空
p.reset(new int(999)); // 释放旧内存,接管新内存
if (p) {}       // 重载bool,判断是否持有有效资源
*p; p->func();  // 重载解引用、箭头,和裸指针用法一致

3. 使用场景

  1. 函数局部临时堆对象(默认首选);
  2. 容器存储独占资源(vector<unique_ptr<T>>);
  3. 类成员独占资源,无需共享;
  4. 工厂函数返回动态对象(移动语义自动优化,无拷贝开销)。

4. 自定义删除器

unique_ptr 删除器作为模板参数,支持自定义释放逻辑(管理文件句柄、socket 等):

cpp 复制代码
// 自定义释放函数
void file_del(FILE* f) {
    cout << "关闭文件" << endl;
    fclose(f);
}

// 指定删除器类型
unique_ptr<FILE, decltype(file_del)*> fp(fopen("test.txt", "r"), file_del);

数组版本默认调用 delete[],无需手动处理数组释放。


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

1. 核心特性

共享所有权:多份 shared_ptr 可同时持有同一个对象,内部维护原子引用计数器。

  • 引用计数规则:
    1. 创建 shared_ptr 计数 = 1;
    2. 拷贝 / 赋值给新 shared_ptr,计数 + 1;
    3. 销毁 shared_ptr(离开作用域、reset),计数 - 1;
    4. 计数归 0 时,自动释放堆内存。
  • 原子计数器:多线程安全增减计数,但对象本身访问不保证线程安全;
  • 额外开销:堆上分配控制块(计数 + 删除器),性能弱于 unique_ptr。

2. 基础用法

(1)创建(优先 make_shared)
cpp 复制代码
// make_shared:一次性分配对象+控制块,内存更高效
auto s1 = make_shared<int>(200);
shared_ptr<int> s2 = s1; // 拷贝,计数+1

cout << s1.use_count() << endl; // 2,查看当前引用数
(2)核心接口
cpp 复制代码
auto sp = make_shared<string>("shared test");
sp.get();               // 获取裸指针
sp.reset();             // 计数-1,计数0则释放
sp.reset(new string()); // 接管新对象
sp.use_count();         // 返回当前引用计数
sp.unique();            // 判断计数是否等于1(C++17弃用)

3. 经典坑:循环引用(内存泄漏根源)

当两个对象互相持有对方 shared_ptr,引用计数永远无法归零,内存永久泄漏。

cpp 复制代码
struct Node {
    int val;
    shared_ptr<Node> next;
    Node(int v) : val(v) {}
};

void cycle_leak() {
    auto a = make_shared<Node>(1);
    auto b = make_shared<Node>(2);
    a->next = b;
    b->next = a;
    // 函数结束,a、b销毁,计数各减1,但仍为1,内存泄漏!
}

解决方案:搭配 std::weak_ptr 打破循环引用。

4. 使用场景

  1. 多个模块需要共享同一个资源;
  2. 缓存、全局资源池;
  3. 订阅回调、多持有者对象;
  4. 注意:无共享需求时,一律用 unique_ptr。

四、std::weak_ptr 弱智能指针(配套 shared_ptr)

1. 核心特性

  • 不增加引用计数,仅作为 shared_ptr 的观察者;
  • 解决 shared_ptr 循环引用泄漏;
  • 不能直接解引用访问对象,必须通过 lock() 升级为 shared_ptr
  • 可检测对象是否已销毁(expired())。

2. 完整解决循环引用示例

cpp 复制代码
#include <memory>
struct Node {
    int val;
    weak_ptr<Node> next; // 改用 weak_ptr 打破循环
    Node(int v) : val(v) {}
};

void fix_cycle() {
    auto a = make_shared<Node>(1);
    auto b = make_shared<Node>(2);
    a->next = b;
    b->next = a;
    // 函数退出,计数归零,内存正常释放
}

// weak_ptr 访问对象
auto wp = a->next;
if (!wp.expired()) { // 对象未销毁
    auto sp = wp.lock(); // 升级为shared_ptr,计数临时+1
    cout << sp->val << endl;
}

3. 常用接口

cpp 复制代码
weak_ptr<T> wp(sp);      // 由shared_ptr构造
wp.expired();            // true=对象已释放
wp.lock();               // 返回shared_ptr,失效返回空shared_ptr
wp.reset();              // 清空弱指针
wp.use_count();          // 获取关联shared_ptr的引用数

4. 使用场景

  1. 打破 shared_ptr 循环引用;
  2. 缓存弱引用(不阻止资源释放,资源销毁后自动失效);
  3. 观察者模式,观察者不持有对象生命周期。

五、三种智能指针完整对比表

特性 unique_ptr shared_ptr weak_ptr
所有权模型 独占,不可拷贝 共享,可拷贝 无所有权,仅观察
引用计数 有(原子)
拷贝 / 赋值 禁止,仅移动 允许 允许(基于 shared_ptr)
内存开销 极小(仅裸指针) 较高(控制块堆分配) 极小(存储控制块地址)
线程安全 无计数,移动需手动同步 计数增减原子安全,对象不安全 无计数,线程安全判断失效
循环引用问题 不存在(无法循环持有) 会内存泄漏 专门解决循环引用
适用场景 绝大多数独占资源场景 多模块共享资源 配套 shared_ptr、打破循环
C++ 标准支持 C++11(make_unique C++14) C++11

六、最佳实践规范(博客重点总结)

  1. 优先使用 unique_ptr,无共享需求绝不使用 shared_ptr,性能最优;
  2. 创建智能指针统一用 make_unique / make_shared,避免裸 new:
    • 异常安全:内存分配失败不会造成裸指针泄漏;
    • 内存连续分配,减少堆碎片;
  3. 尽量避免 get() 裸指针长期保存,防止悬垂;
  4. 函数返回动态对象直接返回 unique_ptr,移动语义零开销;
  5. 存在双向 / 环形依赖时,一方使用 weak_ptr 打破循环;
  6. 多线程场景:shared_ptr 计数安全,但对象读写必须加锁;
  7. 不要混用裸指针与智能指针管理同一资源,双重释放崩溃;
  8. 数组资源优先 make_unique<T[]>,自动调用 delete[]

七、常见踩坑汇总

坑 1:裸指针初始化多个 shared_ptr

复制代码
int* p = new int(10);
shared_ptr<int> s1(p);
shared_ptr<int> s2(p); // 两个独立控制块,析构时重复delete,程序崩溃

解决:全程只用 make_shared,不分离裸指针。

坑 2:函数传参裸指针给智能指针

复制代码
void func(shared_ptr<int> sp) {}
int* p = new int(10);
func(p); // 隐式构造shared_ptr,离开函数释放,外部p变成野指针

解决:传参直接传 shared_ptr,禁止裸指针隐式转换。

坑 3:weak_ptr 不判断 expired 直接 lock

对象已销毁时 lock 返回空 shared_ptr,解引用直接崩溃,必须先判断。

坑 4:unique_ptr 拷贝赋值

编译直接报错,改用 std::move 转移所有权。


八、自定义删除器完整示例

智能指针默认释放逻辑:

  • unique_ptr<T>:析构调用 delete ptr;数组特化 unique_ptr<T[]> 自动 delete[] ptr
  • shared_ptr<T>:析构调用 delete ptr

删除器:用户自定义的可调用对象,接管资源释放逻辑,替换默认 delete

一、unique_ptr 删除器核心规则

1. 语法规则

unique_ptr 删除器是模板类型参数,类型固定在编译期,结构:

cpp 复制代码
template< class T, class Deleter = default_delete<T> > class unique_ptr;
  • 第二个模板参数为删除器类型,默认 std::default_delete<T>(封装 delete);
  • 必须在定义时显式指定删除器类型;
  • 无运行时开销,删除器对象存储在 unique_ptr 内部,不额外堆分配。
2. 四种删除器写法
(1)函数指针删除器
cpp 复制代码
template<typename T>
void ArrayDel(T* p) {
    cout << "数组释放 delete[]" << endl;
    delete[] p;
}

// Deleter类型:void(*)(Date*) 无捕获函数指针
unique_ptr<Date, void(*)(Date*)> up(new Date[5], ArrayDel<Date>);
(2)无捕获 lambda

无捕获 lambda 可隐式转为函数指针,也可用decltype推导类型:

cpp 复制代码
auto del = [](Date* p){ delete[] p; };
unique_ptr<Date, decltype(del)> up(new Date[5], del);
(3)有捕获 lambda

不能转函数指针,只能decltype推导:

cpp 复制代码
int logFlag = 1;
auto del = [logFlag](Date* p){
    if(logFlag) cout << "释放数组" << endl;
    delete[] p;
};
unique_ptr<Date, decltype(del)> up(new Date[5], del);
(4)仿函数( 结构体重载 ( ) )
cpp 复制代码
struct FileDel {
    void operator()(FILE* f) const {
        if(f) fclose(f);
    }
};
// 仿函数作删除器
unique_ptr<FILE, FileDel> fp(fopen("a.txt","r"));

3. 短板

删除器属于类型一部分,同 T、不同删除器的unique_ptr是完全不同类型,无法互相赋值 / 移动。

二、shared_ptr 删除器核心规则

1. 语法规则

shared_ptr 模板只有一个类型参数,删除器不写入模板:

cpp 复制代码
template< class T > class shared_ptr;
  • 删除器存放在堆上的控制块中,运行时存储;
  • 任意可调用对象均可传入,声明无需修改模板;
  • 拷贝、赋值不受删除器类型影响,不同删除器的shared_ptr<T>可互相赋值;
  • 代价:控制块堆分配,轻微性能损耗。
2. 四种删除器通用写法
cpp 复制代码
// 1. 函数指针
shared_ptr<Date> sp1(new Date[5], ArrayDel<Date>);

// 2. 内联lambda(最常用)
shared_ptr<Date> sp2(new Date[5], [](Date* p){delete[] p;});

// 3. 仿函数
shared_ptr<FILE> sp3(fopen("a.txt","r"), FileDel{});

// 4. 带捕获lambda
int flag = 1;
shared_ptr<Date> sp4(new Date[5], [flag](Date* p){
    if(flag) cout << "释放数组" << endl;
    delete[] p;
});

关键特性

拷贝shared_ptr时,会同步复制控制块里的删除器;所有共享对象销毁时,统一调用同一个删除器。

三、unique_ptr vs shared_ptr 删除器对比

维度 unique_ptr 删除器 shared_ptr 删除器
是否模板参数 是,影响类型 否,不影响类型
存储位置 unique_ptr 栈内,无堆开销 堆控制块,额外内存开销
类型兼容性 删除器不同则类型不同,无法转移 删除器不同仍为同类型,可赋值共享
写法复杂度 必须显式声明 Deleter 类型 直接传可调用对象,极简
适用场景 独占资源、追求极致性能 共享资源、外部句柄管理