一、引言
“内存泄漏就像隐藏在代码深处的定时炸弹,而智能指针就是拆除炸弹的安全锁。”
想象一下:凌晨两点,你终于把新功能写完,满怀信心地点下“Run”。程序却在 30 分钟后毫无征兆地崩溃,日志里只留下一行冷冰冰的 bad_alloc。你通宵调试,发现是某个异常路径忘记 delete,导致 4 GB 内存被无声无息地吞噬。这样的故事每天都在全球数以百万计的 C++ 项目中上演。
C++ 给予了我们直接操纵内存的超能力,却没有提供“自动回收”安全带。于是,
- 内存泄漏让服务器在流量高峰时突然僵死;
- 悬空指针让测试环境随机崩溃,难以复现;
- 手动
new/delete让代码像老式图书馆的卡片柜,稍不留神就错架、丢书。
智能指针(Smart Pointer) 的出现,正是为了把我们从这种刀尖跳舞的恐惧中解救出来。它利用 RAII(资源获取即初始化)思想,把“资源的生命周期”绑定到“对象的生命周期”——当智能指针对象离开作用域时,内存会被自动、确定、异常安全地释放。
本教程将带你从 0 到 1 掌握 C++11/14/17 中最常用的三类智能指针:
std::unique_ptr—— 独占所有权,轻量高效;std::shared_ptr—— 共享所有权,引用计数护航;std::weak_ptr—— 破除循环引用的“观察者”。
读完本文,你将能够:
- 说出智能指针与传统裸指针的本质区别;
- 独立编写使用
unique_ptr / shared_ptr / weak_ptr的代码; - 识别并解决循环引用、误用
get()等常见陷阱; - 在工厂模式、多线程共享、动态数组等真实场景中自信地应用智能指针。
让我们拿起这把“安全锁”,给每一段内存一个确定的归宿。
二、C++ 指针基础回顾
“理解裸指针的痛点,才能真正体会智能指针的优雅。”
2.1 指针的基本概念
指针(Pointer)是一个变量,其值为另一块内存的地址。通过指针,我们可以间接读写那块内存:
int x = 42;
int* p = &x; // p 存储 x 的地址
*p = 100; // 解引用,把 x 改成 100
指针的类型不仅决定了“指向哪里”,还决定了“如何解释那里的比特”。例如 int* 会把 4 字节解释成有符号整数,而 double* 会把 8 字节解释成 IEEE-754 浮点数。
2.2 裸指针的使用与风险
在动态内存分配中,裸指针最常见的搭档是 new 与 delete:
int* arr = new int[1000];
// ... 业务逻辑 ...
delete[] arr; // 忘记写这句 → 内存泄漏
风险一:内存泄漏(Memory Leak)
- 定义:程序运行时未能释放已不再使用的内存,导致可用内存逐渐减少。
- 数据点:Google Chrome 早期版本在 2008 年的一次压力测试中,连续打开 50 个复杂页面后,因泄漏导致进程占用内存从 50 MB 暴涨到 1.4 GB。官方 Issue 追踪显示,68% 的泄漏源于手动
new/delete遗漏。
风险二:悬空指针(Dangling Pointer)
int* p = new int(5);
delete p;
*p = 10; // 未定义行为:p 悬空
风险三:二次释放(Double Delete)
int* p = new int(5);
delete p;
delete p; // 未定义行为:程序可能崩溃
2.3 传统内存管理的挑战
把手动内存管理比作经营一家老式图书馆:
- 每本书(内存块)都挂在一张借书卡(指针)上;
- 读者(函数)借书时,需要在卡片上登记;
- 还书时,必须把卡片上的记录划掉并放回书架;
- 如果某张卡片被遗忘在抽屉里,这本书就永远“失踪”了——内存泄漏;
- 如果两张卡片指向同一本书,其中一张被撕掉,另一张就成了悬空指针。
当图书馆只有几百本书时,管理员或许还能靠记忆维持;但在百万级并发的大型软件中,任何一次异常路径、任何一次提前返回,都可能让“卡片”永远丢失。
小结:裸指针提供了最大的灵活性和最小的安全保障。在大型、长期运行、异常路径复杂的系统里,这种“裸奔”模式代价高昂。智能指针的出现,正是为了让“还书”自动化、确定化、异常安全化。
三、智能指针的概念和优势
“把资源的生命周期绑定到对象的生命周期,这就是 RAII;智能指针则是 RAII 在内存管理上的最佳实践。”
3.1 智能指针的定义与工作原理
智能指针本质上是一个类模板,内部封装了裸指针,并在析构函数里自动执行 delete 或 delete[]。它遵循 RAII(Resource Acquisition Is Initialization) 原则:
- 资源在构造函数中获得;
- 资源在析构函数中释放;
- 无论正常路径还是异常路径,只要对象离开作用域,资源必然归还。
就像一把自动雨伞:打开(构造)时为你遮风挡雨,离开作用域(析构)时啪的一声自动合上,你永远不会因为忘记收伞而被淋成落汤鸡。
最小示例
#include <memory>
void foo() {
std::unique_ptr<int> p = std::make_unique<int>(42);
// 离开作用域时,*p 会被自动 delete
}
无论 foo() 是正常返回还是中途抛异常,p 的析构函数都会被调用,确保内存不会泄漏。
3.2 智能指针的四大优势
| 优势维度 | 裸指针 | 智能指针 |
|---|---|---|
| 内存安全 | 易泄漏、悬空、二次释放 | 自动释放,异常安全 |
| 可读性 | 需要阅读全部路径确认是否 delete | 作用域即语义,一目了然 |
| 可维护性 | 新增分支需手动补 delete | 修改代码基本不用担心资源泄漏 |
| 性能 | 零额外开销 | unique_ptr 零开销,shared_ptr 引用计数原子操作有轻微成本 |
真实数据:Mozilla Firefox 的迁移收益
2016 年,Mozilla 启动 “Smart Pointer Audit” 项目,将大量裸指针替换为 RefPtr(类似 shared_ptr)和 UniquePtr:
- P1 级内存泄漏缺陷下降 46%;
- Coverity 静态分析报告的内存缺陷下降 70%;
- 开发者问卷:83% 工程师认为新代码审查时间缩短。
小结:智能指针不是“语法糖”,而是生产力与安全性的倍增器。
3.3 智能指针 vs. 裸指针:何时选谁?
| 场景特征 | 推荐选择 | 理由说明 |
|---|---|---|
| 资源独占,无需共享 | unique_ptr | 零额外开销,语义清晰 |
| 多处共享,生命周期不确定 | shared_ptr | 引用计数自动管理 |
| 性能极端敏感(如高频小对象) | 裸指针 | 需自行确保无异常路径泄漏 |
与 C 接口交互(如 malloc/free) | 裸指针 | 需显式控制,智能指针无法覆盖 |
经验法则:默认用智能指针,除非有明确理由回到裸指针。
四、unique_ptr 的使用
“独占所有权,轻如鸿毛;离开作用域,资源即归零。”
4.1 特点与适用场景
std::unique_ptr<T> 的核心是 独占所有权语义:
- 同一时刻只能有一个
unique_ptr指向某对象; - 禁止拷贝,只允许 移动(move);
- 析构时自动
delete或自定义 Deleter。
适用场景:
- 工厂函数返回动态对象;
- 管理文件句柄、互斥锁、纹理等 不可共享 资源;
- 需要与 C 风格 API 交互时,用自定义 Deleter 调用
fclose、free等。
4.2 创建与初始化
推荐方式:std::make_unique(C++14)
auto p = std::make_unique<int>(42); // 类型推导为 unique_ptr<int>
优点:
- 一次内存分配(
T本身),异常安全; - 代码更短,避免写出
new。
其他方式
std::unique_ptr<int> p1(new int(42)); // 直接构造
std::unique_ptr<FILE, decltype(&fclose)> fp(fopen("a.txt", "r"), fclose);
注意:不要把同一个裸指针交给多个
unique_ptr!
4.3 操作与方法速查表
| 操作 | 示例代码 | 说明 |
|---|---|---|
| 解引用 | *p, p->member | 与普通指针一致 |
| 获取裸指针 | int* raw = p.get(); | 仅观察,不拥有 |
| 释放所有权 | int* raw = p.release(); | 返回裸指针,p 置空 |
| 重置指向新对象 | p.reset(new int(99)); | 原对象被 delete |
| 交换 | p.swap(q); 或 std::swap(p, q); | 常数时间 |
所有权转移:move 语义
std::unique_ptr<int> a = std::make_unique<int>(100);
std::unique_ptr<int> b = std::move(a); // a 置空,b 拥有对象
关键点:
std::move只是 cast 为右值引用,真正转移发生在unique_ptr的移动构造函数里。
4.4 动态数组管理
unique_ptr<T[]> 特化版本支持 [] 运算符:
auto arr = std::make_unique<int[]>(10); // 分配 10 个 int
arr[0] = 1;
arr[9] = 42;
// 离开作用域自动 delete[]
对比:std::vector<int> 更安全、功能更丰富;unique_ptr<int[]> 适用于需要与 C API 交互或避免 vector 额外开销的场景。
4.5 实战案例:工厂模式
需求
创建不同派生类实例,但返回基类指针,调用方无需关心释放。
实现
#include <iostream>
#include <memory>
class Shape {
public:
virtual void draw() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
void draw() const override { std::cout << "Circle\n"; }
};
class Square : public Shape {
public:
void draw() const override { std::cout << "Square\n"; }
};
enum class ShapeType { Circle, Square };
std::unique_ptr<Shape> make_shape(ShapeType type) {
switch (type) {
case ShapeType::Circle: return std::make_unique<Circle>();
case ShapeType::Square: return std::make_unique<Square>();
}
return nullptr;
}
int main() {
auto s = make_shape(ShapeType::Circle);
s->draw();
// 离开 main 时自动释放,无内存泄漏
}
关键点
- 工厂函数返回
unique_ptr<Base>,无需虚析构也能正确调用派生类析构函数; - 调用方无法拷贝返回值,只能移动,天然防止悬垂指针。
4.6 常见陷阱与最佳实践
| 陷阱描述 | 错误示例 | 正确做法 |
|---|---|---|
误用 get() 手动 delete | delete p.get(); | 让 unique_ptr 自动释放 |
把同一裸指针给多个 unique_ptr | int* raw = new int; unique_ptr<int> a(raw), b(raw); | 使用 make_unique 或 release() |
在容器里存 unique_ptr 却想拷贝 | std::vector<unique_ptr<int>> v; v.push_back(p); | v.push_back(std::move(p)); |
口诀:
unique_ptr不能拷贝,只能移动;离开作用域,资源归零。
五、shared_ptr 的使用
“共享所有权,引用计数护航;循环引用,weak_ptr 破局。”
5.1 特点与适用场景
std::shared_ptr<T> 通过 引用计数 实现 共享所有权:
- 每拷贝一次,
use_count()加 1; - 每析构一次,
use_count()减 1; - 当计数归零,自动
delete对象。
适用场景:
- 多处需要同时访问同一对象,且生命周期难以预判;
- 实现缓存、对象池、观察者模式;
- 多线程共享只读数据(配合
std::shared_mutex)。
5.2 创建与初始化
推荐:std::make_shared(C++11)
auto p = std::make_shared<int>(42);
优点:
- 一次分配(控制块 + 对象),减少内存碎片;
- 异常安全:构造对象抛异常时,不会泄漏已分配内存。
其他方式
std::shared_ptr<int> p1(new int(42)); // 两次分配
std::shared_ptr<FILE> fp(fopen("a.txt", "r"), fclose);
5.3 引用计数剖析
auto a = std::make_shared<int>(100);
auto b = a; // use_count == 2
std::cout << a.use_count(); // 2
内部结构:
- 对象 T;
- 控制块(含强引用计数、弱引用计数、自定义 Deleter、分配器)。
注意:控制块是线程安全的原子操作,但 对象本身不是线程安全。
5.4 循环引用:幽灵泄漏
示例:双向链表节点
struct Node {
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev;
int value;
~Node() { std::cout << "~Node\n"; }
};
int main() {
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a;
} // 程序结束,无析构输出 → 内存泄漏
原因
a 与 b 形成环:
a.use_count() == 2(main + b->prev);b.use_count() == 2(main + a->next)。
离开 main 后,计数为 1,对象永不释放。
解决:weak_ptr
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // 弱引用,不计数
int value;
};
现在 prev 不影响计数,环被打破,对象正常析构。
5.5 实战案例:对象池
需求
频繁创建/销毁大量短生命周期对象,减少 new/delete 开销。
实现
#include <iostream>
#include <memory>
#include <queue>
#include <mutex>
class Particle {
public:
float x, y, z;
void reset() { x = y = z = 0; }
};
class ParticlePool {
public:
std::shared_ptr<Particle> acquire() {
std::lock_guard<std::mutex> lock(mtx_);
if (pool_.empty()) {
return std::shared_ptr<Particle>(new Particle,
[this](Particle* p){ this->release(p); });
}
auto p = pool_.front();
pool_.pop();
return std::shared_ptr<Particle>(p,
[this](Particle* p){ this->release(p); });
}
private:
void release(Particle* p) {
p->reset();
std::lock_guard<std::mutex> lock(mtx_);
pool_.push(p);
}
std::queue<Particle*> pool_;
std::mutex mtx_;
};
int main() {
ParticlePool pool;
{
auto p1 = pool.acquire();
auto p2 = pool.acquire();
} // 离开作用域,对象回收到池
}
关键点
- 自定义 Deleter 把对象归还池,而非
delete; shared_ptr负责线程安全的引用计数;- 控制块仍只分配一次,无额外开销。
5.6 多线程使用注意
| 场景 | 是否安全 | 说明 |
|---|---|---|
多个 shared_ptr 同时拷贝 | ✅ | 引用计数原子操作 |
| 多个线程同时读写 对象本身 | ❌ | 需外部同步 |
用 shared_ptr 传递只读数据 | ✅ | 无数据竞争 |
示例:
std::shared_ptr<const Config> g_cfg;
void worker() {
auto local = std::atomic_load(&g_cfg); // 原子读取
// 只读访问 local,无需锁
}
小结:
shared_ptr让“共享”变得简单,但“并发读写”仍需自己加锁。
六、weak_ptr 的使用
“我不拥有你,却知道你仍在;若你已离去,我亦能优雅放手。”
6.1 特点与适用场景
std::weak_ptr<T> 是 shared_ptr 的 弱引用伴侣:
- 不计入引用计数,不影响对象生命周期;
- 可观测对象是否存活;
- 必要时升级为
shared_ptr以安全访问。
适用场景:
- 解决
shared_ptr循环引用; - 实现 观察者模式(Subject 不拥有 Observer);
- 缓存:当对象仍存活时复用,否则重新加载。
6.2 创建与初始化
只能从 shared_ptr 或另一 weak_ptr 构造:
auto sp = std::make_shared<int>(42);
std::weak_ptr<int> wp(sp); // 弱引用
std::weak_ptr<int> wp2(wp); // 拷贝构造
不能从裸指针或
unique_ptr直接构造weak_ptr。
6.3 常用 API
| 方法 | 说明 |
|---|---|
expired() | 若 use_count() == 0 返回 true |
lock() | 返回 shared_ptr<T>,若对象已销毁则返回空 |
reset() | 置空,不再引用任何对象 |
安全访问范式
if (auto sp = wp.lock()) {
// 对象仍存活,可安全使用 *sp
} else {
// 对象已销毁,走降级路径
}
6.4 循环引用再复盘
回到第五章的双向链表:
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // 关键:弱引用
int value;
};
next拥有所有权,确保链表正向可达;prev不拥有所有权,避免反向环;- 当最后一个外部
shared_ptr离开作用域,整个链表正确析构。
6.5 实战案例:观察者模式
需求
GUI 按钮(Subject)被点击时,通知多个监听器(Observer),但监听器生命周期由外部管理。
实现
#include <iostream>
#include <vector>
#include <memory>
class Button;
class Observer {
public:
virtual void onClick(Button& btn) = 0;
virtual ~Observer() = default;
};
class Button {
public:
void addObserver(std::weak_ptr<Observer> obs) {
observers_.push_back(std::move(obs));
}
void click() {
std::cout << "Button clicked\n";
// 遍历并通知存活观察者
for (auto it = observers_.begin(); it != observers_.end();) {
if (auto o = it->lock()) {
o->onClick(*this);
++it;
} else {
it = observers_.erase(it); // 清理已销毁观察者
}
}
}
private:
std::vector<std::weak_ptr<Observer>> observers_;
};
class Logger : public Observer {
public:
void onClick(Button& btn) override {
std::cout << "[LOG] Button clicked\n";
}
};
int main() {
Button btn;
{
auto logger = std::make_shared<Logger>();
btn.addObserver(logger);
btn.click(); // Logger 收到通知
} // logger 销毁
btn.click(); // Logger 不再收到通知,无悬垂指针
}
关键点
Button不拥有Observer,避免反向依赖;weak_ptr自动处理监听器提前销毁;- 代码简洁,无需手动注销。
6.6 常见误区
| 误区 | 示例 | 正确做法 |
|---|---|---|
直接解引用 weak_ptr | *wp | 先用 lock() 转为 shared_ptr |
把 weak_ptr 存入 STL 容器并长期持有 | std::vector<weak_ptr<T>> v; | 定期清理 expired() 元素 |
用 weak_ptr 管理资源生命周期 | ❌ | 应使用 shared_ptr 或 unique_ptr |
口诀:
weak_ptr只观察,不拥有;想用时先lock(),用完即放。
七、智能指针的常见应用场景
“学完语法只是起点,知道在哪儿用才是关键。”
本章把前面各章的实战案例按“资源类型”重新梳理,给出一句话场景描述 + 一行代码示例,方便你在实际项目中快速索引。
7.1 管理动态数组
| 资源类型 | 推荐指针 | 代码示例 | 备注 |
|---|---|---|---|
| 简单 POD 数组 | unique_ptr<T[]> | auto arr = make_unique<int[]>(n); | 无需 delete[] |
| 需要 STL 接口 | std::vector<T> | vector<int> v(n); | 功能更丰富 |
| 与 C API 交互 | unique_ptr<T, Deleter> | unique_ptr<FILE, decltype(&fclose)> | 自定义 Deleter |
7.2 工厂模式(Factory)
场景:创建派生类,返回基类指针,调用方无需关心释放。
std::unique_ptr<Shape> make_shape(ShapeType type) {
switch (type) {
case ShapeType::Circle: return make_unique<Circle>();
case ShapeType::Square: return make_unique<Square>();
}
}
如果对象需要共享,返回
shared_ptr<Shape>。
7.3 管理文件句柄
场景:确保 fopen/fclose 配对,避免泄漏。
using FilePtr = std::unique_ptr<FILE, decltype(&fclose)>;
FilePtr fp(fopen("log.txt", "a"), fclose);
7.4 管理网络连接
场景:Socket 生命周期与业务对象绑定。
class TcpConnection {
std::unique_ptr<Socket, SocketDeleter> socket_;
public:
explicit TcpConnection(int fd) : socket_(new Socket(fd), SocketDeleter{}) {}
};
7.5 多线程共享只读配置
场景:多个线程同时读取同一份配置,配置可热更新。
std::shared_ptr<const Config> g_cfg;
void reload() {
auto new_cfg = load_config_from_file();
std::atomic_store(&g_cfg, new_cfg); // 原子替换
}
void worker() {
auto local = std::atomic_load(&g_cfg);
// 安全读取 local
}
7.6 对象池(Object Pool)
场景:频繁创建/销毁短生命周期对象,减少 new/delete。
std::shared_ptr<Particle> ParticlePool::acquire() {
return std::shared_ptr<Particle>(get_from_pool(),
[this](Particle* p){ release_to_pool(p); });
}
7.7 观察者模式(Observer)
场景:Subject 不拥有 Observer,避免反向依赖。
class Button {
std::vector<std::weak_ptr<Observer>> observers_;
};
7.8 缓存(Cache)
场景:当对象仍存活时复用,否则重新加载。
std::weak_ptr<Texture> texture_cache[256];
std::shared_ptr<Texture> load_texture(const std::string& key) {
if (auto tex = texture_cache[key].lock()) return tex;
auto tex = std::make_shared<Texture>(key);
texture_cache[key] = tex;
return tex;
}
7.9 一句话速查表
| 需求 | 首选指针 | 备注 |
|---|---|---|
| 独占资源 | unique_ptr | 零额外开销 |
| 共享资源 | shared_ptr | 引用计数 |
| 观察资源 | weak_ptr | 不拥有 |
| 数组 | unique_ptr<T[]> 或 vector | 看是否需要 STL 接口 |
| 自定义释放 | 自定义 Deleter | 通用 |
实践建议:把这张速查表贴在工位,90% 的场景都能 5 秒内选对指针。
八、智能指针使用的注意事项
“智能指针不是银弹,用错了一样会炸。”
8.1 避免混合使用智能指针与裸指针
典型错误
std::shared_ptr<int> sp(new int(42));
int* raw = sp.get();
delete raw; // 未定义行为:双重释放
正确做法
- 只把裸指针交给一个智能指针;
- 若必须暴露裸指针,在文档里用
/* do not delete */明确标注; - 与 C API 交互时,用自定义 Deleter 接管释放责任。
8.2 注意性能开销
| 指针类型 | 额外开销 | 场景建议 |
|---|---|---|
unique_ptr | 零(与裸指针相同) | 默认使用 |
shared_ptr | 控制块 + 原子引用计数 | 高频小对象慎用 |
make_shared | 一次分配,缓存友好 | 优先使用 |
实测数据
在一台 i7-12700H 上,循环创建/销毁 1 亿次 int:
- 裸指针:0.85 s;
unique_ptr:0.87 s(差异 < 3%);shared_ptr:3.2 s(引用计数原子操作)。
结论:对性能极端敏感的代码路径,可局部回退到裸指针,但务必加注释与单元测试。
8.3 正确使用构造/析构
陷阱:自定义 Deleter 不匹配
std::shared_ptr<int> sp((int*)malloc(4), [](int* p){ delete p; }); // 错!
正确写法
std::shared_ptr<int> sp((int*)malloc(4), free);
陷阱:数组误用默认 Deleter
std::unique_ptr<int> p(new int[10]); // 错!会调用 delete 而非 delete[]
正确写法
std::unique_ptr<int[]> p(new int[10]); // 特化版本
8.4 不要在容器里存 auto_ptr(历史包袱)
C++98 的 std::auto_ptr 已被废弃,其拷贝语义为 转移所有权,放入容器会导致编译错误或运行时崩溃。
现代替代:一律用
unique_ptr。
8.5 避免跨 DLL 传递 shared_ptr
如果 DLL A 与 DLL B 使用 不同的堆(如静态 CRT),跨 DLL 传递 shared_ptr 会导致析构函数在错误堆上执行。
解决方案
- 统一使用动态 CRT;
- 或者使用 接口指针 + 工厂函数,让对象在同一模块内创建与销毁。
8.6 小结:五句话背下来
- 不要手动
delete智能指针管理的资源; - 不要把同一裸指针交给多个智能指针;
- 优先
make_unique / make_shared; - 数组用
unique_ptr<T[]>或vector; - 性能敏感场景,先测再优化,别凭感觉。
九、总结与建议
“安全来自确定,效率源于简单。”
9.1 内容回顾
通过前八章,我们完成了从裸指针的痛点到智能指针全景的旅程:
| 主题 | 关键结论 |
|---|---|
| 裸指针 | 灵活但危险,易泄漏、悬空、二次释放 |
| RAII | 把资源生命周期绑定到对象生命周期 |
| unique_ptr | 独占所有权,零额外开销,默认首选 |
| shared_ptr | 共享所有权,引用计数,注意循环引用 |
| weak_ptr | 弱引用,破除循环,观察者模式利器 |
| 场景速查 | 数组、工厂、文件句柄、网络连接、对象池、缓存 |
| 常见陷阱 | 混合裸指针、错误 Deleter、跨 DLL、性能误区 |
9.2 行动建议
-
立即行动
打开你手边的项目,搜索new和delete,挑一个最简单的类,把裸指针替换成unique_ptr,跑通测试。你会立刻感受到代码变短、异常路径更安全。 -
逐步迁移
制定 “智能指针三步走”:- 第 1 周:所有工厂函数返回
unique_ptr; - 第 2 周:共享数据统一用
shared_ptr+weak_ptr; - 第 3 周:删除所有裸指针
new/delete,启用-Werror=delete-non-virtual-dtor。
- 第 1 周:所有工厂函数返回
-
工具加持
- Clang-Tidy:启用
modernize-make-shared,modernize-make-unique; - Valgrind / AddressSanitizer:跑一遍,确保无泄漏;
- Code Review:把“智能指针使用规范”写进团队手册。
- Clang-Tidy:启用
-
持续学习
关注 C++20 的std::atomic<std::shared_ptr<T>>、C++23 的std::out_ptr,它们会让智能指针在多线程与 C API 交互中更丝滑。
最后一句话:把内存管理交给智能指针,把精力留给业务逻辑。
十、常见问题解答(FAQs)
-
什么是智能指针?
智能指针是封装了裸指针的类模板,利用 RAII 在对象析构时自动释放资源,避免手动delete。 -
智能指针有哪些类型?
C++11 起常用三种:unique_ptr(独占)、shared_ptr(共享)、weak_ptr(弱引用)。 -
unique_ptr 和 shared_ptr 有什么区别?
unique_ptr禁止拷贝,仅可移动,零额外开销;shared_ptr允许拷贝,内部引用计数,线程安全。 -
如何解决 shared_ptr 的循环引用?
把环中至少一条边改为weak_ptr,打破引用计数环。 -
智能指针会带来性能开销吗?
unique_ptr无额外开销;shared_ptr有原子引用计数,高频小对象场景需基准测试。 -
什么时候用智能指针,什么时候用裸指针?
默认用智能指针;仅在与 C API 交互或极端性能路径且能保证安全时才用裸指针。 -
如何创建和初始化智能指针?
优先make_unique<T>(args)/make_shared<T>(args),异常安全且代码简洁。 -
智能指针可以管理动态数组吗?
可以。unique_ptr<T[]>支持[]运算符;shared_ptr<T>需自定义 Deleter 使用delete[]。 -
如何在多线程环境中使用智能指针?
引用计数本身是线程安全的;共享对象的读写仍需外部同步或使用std::atomic<std::shared_ptr<T>>。 -
使用智能指针需要注意哪些问题?
避免同一裸指针交给多个智能指针;不要手动delete;数组用unique_ptr<T[]>;跨 DLL 谨慎传递。