一. 为什么会有智能指针
c++ 引入智能指针(Smart Pointers)的核心目的,是 解决传统 raw pointer(裸指针)的三大核心问题,同时在不丧失 C++ 底层控制能力的前提下,引入更安全、更符合现代编程范式的内存管理机制。
1. 内存泄漏(Memory Leak)
当动态分配的内存(new 出来的)没有被手动 delete 时,这块内存会一直占用,直到程序结束。
- 函数提前返回(比如中间有
if分支跳过delete); - 异常抛出(
try块中new,catch块中未处理delete); - 逻辑疏忽(忘记写
delete)。
示例:
c++
void func() {
int* p = new int(10); // 动态分配内存
if (some_condition) {
return; // 提前返回,p 指向的内存未释放 → 内存泄漏
}
delete p; // 正常路径才释放,异常/提前返回时失效
}
2. 野指针(Dangling Pointer)
当指针指向的内存被释放(delete)后,指针本身没有被置为 nullptr,后续仍可能被访问 / 操作,导致:
- 程序崩溃(访问已释放的内存);
- 数据错乱(内存被重新分配给其他对象,旧指针访问到无效数据)。
c++
int* p = new int(10);
delete p; // 内存释放,但 p 仍指向原地址(野指针)
cout << *p; // 未定义行为(UB),可能崩溃或输出垃圾值
3. 二次释放(Double Free)
同一内存被 delete 两次,会导致程序崩溃(内存管理机制检测到非法释放).
示例 :
c++
int* p = new int(10);
delete p;
delete p; // 二次释放 → 程序崩溃
二. 智能指针的核心设计理念
RAII + 所有权语义
智能指针本质是 "封装裸指针的类模板" ,其核心机制是:
-
RAII(资源获取即初始化) :利用类的构造函数获取资源(如动态内存),析构函数自动释放资源(无需手动
delete)。因为 C++ 保证:对象生命周期结束时(如离开作用域、函数返回),其析构函数一定会被调用。 -
所有权语义(Ownership Semantics) :明确指针指向的内存 "归谁所有",避免多指针混乱操作(比如
std::unique_ptr独占所有权,std::shared_ptr共享所有权)
RAII 理解可以继续深入理解一下,这是一种编程范式
三. 共享智能指帧 std::shared_ptr
核心特点是允许多个 shared_ptr 指向同一块动态内存,通过引用计数(Reference Counting) 管理内存生命周期:当最后一个指向该内存的 shared_ptr 被销毁 / 重置时,才会自动释放内存。
(1)引用计数机制
shared_ptr内部维护两个指针:- 指向实际数据的裸指针(data pointer);
- 指向控制块(control block)的指针(存储引用计数、析构函数、分配器等)
- 每新增一个
shared_ptr指向同一资源,引用计数 +1; - 每销毁 / 重置一个
shared_ptr,引用计数 -1; - 当引用计数降至 0 时,自动调用
delete(或自定义删除器)释放资源,并销毁控制块。
c++
struct MyInt {
int val;
MyInt(int v) : val(v) {
// std::cout << "MyInt 构造:" << val << std::endl;
}
~MyInt() {
std::cout << "MyInt 析构:" << val << std::endl;
}
};
void func(std::shared_ptr<MyInt>& ptr) {
std::cout << "func 内引用计数:" << ptr.use_count() << std::endl;
// 赋值新的 shared_ptr
ptr = std::make_shared<MyInt>(20);
std::shared_ptr<MyInt> ptr2 = ptr;
std::cout << "func 内引用计数:" << ptr.use_count() << std::endl;
}
int main() {
std::shared_ptr<MyInt> sp = std::make_shared<MyInt>(10);
std::cout << "调用前引用计数:" << sp.use_count() << std::endl;
func(sp);
std::cout << "调用后引用计数:" << sp.use_count() << std::endl;
std::cout << "sp 指向的值:" << sp->val << std::endl;
return 0;
}
执行结果:

(2)独占性(相对)与拷贝 / 赋值
- 支持拷贝构造 和赋值运算符:拷贝 / 赋值时仅增加引用计数,不深拷贝数据;
- 不同于
unique_ptr(不可拷贝),shared_ptr可自由拷贝、作为函数参数传递(值传递)、存入容器(如std::vector<std::shared_ptr<T>>)。
//详细解释,结合传参
(3)自定义删除器
shared_ptr 支持自定义删除器(Deletor),用于替代默认的 delete,适配特殊场景(如数组、文件句柄、内存池分配的内存等)。
在计数到0的时候,就会执行删除器,- 如果只是"复制"或"赋值"产生新的 shared_ptr,引用计数仍 >0,删除器不会被执行。
场景 A:数组
c++
std::shared_ptr<int> sp(new int[100], // 裸指针
[](int* p){ delete[] p; } // 自定义删除器
);
场景 B:文件句柄
c++
auto file_deleter = [](FILE* fp){
if(fp) fclose(fp);
};
std::shared_ptr<FILE> sp(fopen("log.txt","w"), file_deleter);
场景 C:内存池
arduino
MemoryPool pool;
std::shared_ptr<Foo> sp(
static_cast<Foo*>(pool.allocate()),
[&pool](Foo* p){ pool.deallocate(p); }
);
场景 D:函数对象当删除器
c++
struct VerboseDeleter{
std::string name;
void operator()(Widget* w) const {
std::cout << "destroying " << name << '\n';
delete w;
}
};
std::shared_ptr<Widget> pw(new Widget, VerboseDeleter{"widget#1"});
(4)空指针安全
- 空的
shared_ptr(未指向任何资源)引用计数为 0,操作(如reset()、use_count())不会触发未定义行为; - 可通过
operator bool()判断是否指向有效资源:
cpp
std::shared_ptr<int> p;
if (p) { // 等价于 p != nullptr
std::cout << *p << std::endl;
} else {
std::cout << "p 为空" << std::endl; // 输出该行
}
(5)大小与性能
shared_ptr大小通常是裸指针的 2 倍(存储数据指针 + 控制块指针);- 引用计数的增减是原子操作 (线程安全),但存在轻微的性能开销(相比裸指针 /
unique_ptr); - 注意:引用计数线程安全 ≠ 数据访问线程安全:多个线程同时修改
shared_ptr指向的数据,仍需加锁。
(6)避免循环引用(核心坑点)
shared_ptr 的最大问题是循环引用 :两个(或多个)shared_ptr 互相指向对方,导致引用计数无法降至 0,内存泄漏。
c++
struct Node {
std::shared_ptr<Node> next; // 共享所有权
~Node() { std::cout << "Node 析构" << std::endl; }
};
int main() {
std::shared_ptr<Node> n1(new Node);
std::shared_ptr<Node> n2(new Node);
n1->next = n2; // n1 引用 n2
n2->next = n1; // n2 引用 n1 → 循环引用
// 函数结束时,n1/n2 销毁,但引用计数仍为1,内存泄漏(析构函数不执行)
return 0;
}
(7)避免从裸指针多次构造 shared_ptr
同一裸指针多次构造 shared_ptr,会导致多个独立的控制块,最终触发二次释放:
c
int* raw = new int(10);
std::shared_ptr<int> p1(raw);
std::shared_ptr<int> p2(raw); // 错误:p1和p2各有独立控制块,析构时两次delete raw
(8)优先使用 std::make_shared
- 更高效:一次性分配数据内存 + 控制块内存(裸指针构造需两次分配:数据 + 控制块);
- 更安全:避免异常安全问题(如
func(shared_ptr<int>(new int), other_func())可能因编译器优化导致内存泄漏); - 语法简洁:无需手动写
new。
限制:
make_shared无法直接指定自定义删除器(需通过shared_ptr构造函数配合)。
(9)不要将 shared_ptr 存储在全局 / 静态变量中(谨慎)
全局 shared_ptr 的析构顺序不确定,可能导致资源释放时依赖的对象已销毁,引发未定义行为。而且这个对象死的可能比main还晚
(10)线程安全说明
- 引用计数操作 :
shared_ptr的引用计数增减是原子操作,多线程拷贝 / 重置shared_ptr本身是线程安全的; - 数据访问 :多线程同时读写
shared_ptr指向的数据,不是线程安全的 ,需通过互斥锁(如std::mutex)保护; - 同一 shared_ptr 对象的修改 :多线程同时修改同一个
shared_ptr对象(如p.reset()),不是线程安全的,需加锁。
(11)补充:shared_ptr 作为函数参数时的引用计数行为
-
值传递:引用计数 +1,函数结束 -1
当
shared_ptr以值传递 方式作为函数参数时,会触发shared_ptr的拷贝构造:- 函数调用时,实参的
shared_ptr拷贝给形参,引用计数 +1; - 函数执行结束后,形参出作用域被销毁,引用计数 -1;
- 最终引用计数回到调用前的状态。
c++#include <iostream> #include <memory> void func(std::shared_ptr<int> ptr) { // 值传递,拷贝构造 std::cout << "func 内引用计数:" << ptr.use_count() << std::endl; // 输出 2 } int main() { std::shared_ptr<int> sp = std::make_shared<int>(10); std::cout << "调用前引用计数:" << sp.use_count() << std::endl; // 输出 1 func(sp); std::cout << "调用后引用计数:" << sp.use_count() << std::endl; // 输出 1 return 0; }优缺点
-
优点 :函数内对形参的修改(如
ptr = nullptr)不会影响实参,逻辑隔离; -
缺点:
- 拷贝
shared_ptr有开销(原子操作 + 指针拷贝),高频调用时性能损耗明显; - 若函数内长期持有形参(如存储到全局容器),可能导致引用计数长期不为 0,内存无法及时释放。
- 拷贝
- 函数调用时,实参的
-
非 const 引用传递:引用计数不变
当
shared_ptr以非 const 引用(shared_ptr<T>&) 传递时,形参是实参的别名,不会触发拷贝构造,因此引用计数始终不变。c++#include <iostream> #include <memory> struct MyInt { int val; MyInt(int v) : val(v) { std::cout << "MyInt 构造:" << val << std::endl; } ~MyInt() { std::cout << "MyInt 析构:" << val << std::endl; } }; void func(std::shared_ptr<MyInt>& ptr) { std::cout << "func 内引用计数:" << ptr.use_count() << std::endl; // 赋值新的 shared_ptr ptr = std::make_shared<MyInt>(20); } int main() { std::shared_ptr<MyInt> sp = std::make_shared<MyInt>(10); std::cout << "调用前引用计数:" << sp.use_count() << std::endl; func(sp); std::cout << "调用后引用计数:" << sp.use_count() << std::endl; std::cout << "sp 指向的值:" << sp->val << std::endl; return 0; }注意这里方法执行,重新创建一个对象的时候,会析构原来的
优缺点
-
优点 :函数内对形参的修改(如
ptr = nullptr)不会影响实参,逻辑隔离; -
缺点:
- 拷贝
shared_ptr有开销(原子操作 + 指针拷贝),高频调用时性能损耗明显; - 若函数内长期持有形参(如存储到全局容器),可能导致引用计数长期不为 0,内存无法及时释放。
- 拷贝
-
-
const 引用传递:引用计数不变
当
shared_ptr以const 引用(const shared_ptr<T>&) 传递时,形参是实参的只读别名,同样不触发拷贝构造,引用计数不变;且函数内无法修改形参(如赋值、置空),避免了非 const 引用的风险。c++#include <iostream> #include <memory> void func(const std::shared_ptr<int>& ptr) { // const 引用传递 std::cout << "func 内引用计数:" << ptr.use_count() << std::endl; // 输出 1 // ptr = std::make_shared<int>(20); // 编译错误:const 引用不可修改 *ptr = 20; // 允许修改指向的资源(仅限制 shared_ptr 本身,不限制资源) } int main() { std::shared_ptr<int> sp = std::make_shared<int>(10); std::cout << "调用前引用计数:" << sp.use_count() << std::endl; // 输出 1 func(sp); std::cout << "调用后引用计数:" << sp.use_count() << std::endl; // 输出 1 std::cout << "sp 指向的值:" << *sp << std::endl; // 输出 20 return 0; }优缺点
- 优点:无拷贝开销,性能优;只读特性避免了意外修改实参的风险,是最安全的传参方式;
- 缺点 :若函数内需要长期持有该
shared_ptr(如存储到容器),const 引用可能悬空(实参销毁后引用失效),需手动拷贝(此时引用计数会 +1)。
-
移动语义(std::move)传递
若通过
std::move将shared_ptr以值传递方式传入函数,会触发移动构造:- 实参的
shared_ptr所有权转移给形参,引用计数 保持不变(而非 +1); - 实参变为 "空" 状态(
use_count() == 0),不再管理资源; - 函数结束后形参析构,引用计数 -1,若此时无其他持有者,资源被释放。
C++#include <iostream> #include <memory> void func(std::shared_ptr<int> ptr) { // 值传递 + std::move std::cout << "func 内引用计数:" << ptr.use_count() << std::endl; // 输出 1 } int main() { std::shared_ptr<int> sp = std::make_shared<int>(10); std::cout << "move 前引用计数:" << sp.use_count() << std::endl; // 输出 1 func(std::move(sp)); std::cout << "move 后实参引用计数:" << sp.use_count() << std::endl; // 输出 0 return 0; }移动构造会 "窃取" 实参的资源指针和引用计数,实参的内部指针被置空,因此引用计数不会增加(仅所有权转移);适用于不再使用实参的场景,避免拷贝开销。
- 实参的
四. 独享智能指帧 std::unique_ptr
std::unique_ptr 是 C++11 引入的独占式智能指针 ,用于管理动态分配的内存,核心特性是所有权唯一 ------ 同一时间只有一个 unique_ptr 指向某个对象,销毁时自动释放内存,彻底避免内存泄漏和手动 delete 的风险。使用相对共享指针要简单,但却是最常用的智能指针。
- 独占所有权:不可拷贝(C++11),仅可移动(move),确保对象只有一个管理者;
- 自动释放 :
unique_ptr析构时(如离开作用域、被重置),自动调用delete释放所管理的内存; - 轻量级:无额外开销(大小等同于原始指针),支持自定义删除器;
- 支持数组 :C++11 原生支持管理动态数组(
unique_ptr<T[]>)。
简单用法,单个对象:
c++
// 方式1:直接构造(C++11 推荐)
unique_ptr<int> ptr1(new int(10));
// 方式2:make_unique(C++14 引入,更安全,避免内存泄漏)
// C++11 无 make_unique,可自行实现
unique_ptr<int> ptr2 = make_unique<int>(20);
// 空 unique_ptr
unique_ptr<int> ptr3; // 初始化为 nullptr
管理动态数组:
c++
// 管理 int 数组,析构时自动调用 delete[]
unique_ptr<int[]> arr_ptr(new int[5]{1,2,3,4,5});
// 访问数组元素
cout << arr_ptr[0] << endl; // 输出 1
通过 * 解引用(单个对象)或 ->(成员访问),或 [](数组):
c++
struct Person {
string name;
int age;
void show() { cout << name << ", " << age << endl; }
};
// 单个对象
unique_ptr<Person> p(new Person{"Alice", 20});
cout << (*p).name << endl; // 解引用访问成员
cout << p->age << endl; // -> 访问成员
p->show(); // 调用成员函数
// 数组
unique_ptr<int[]> arr(new int[3]{10,20,30});
cout << arr[1] << endl; // 输出 20
重置与释放
reset():释放当前管理的对象,可选传入新指针;release():释放所有权(返回原始指针),但不释放内存(需手动管理);nullptr赋值:等价于reset()。
c++
unique_ptr<int> ptr(new int(50));
// 重置(释放原内存,指向新对象)
ptr.reset(new int(60));
cout << *ptr << endl; // 输出 60
// 重置为空(释放原内存)
ptr.reset();
cout << (ptr ? "非空" : "空") << endl; // 输出 空
// release():释放所有权,返回原始指针
unique_ptr<int> ptr2(new int(70));
int* raw_ptr = ptr2.release();
cout << *raw_ptr << endl; // 输出 70
delete raw_ptr; // 必须手动释放,否则内存泄漏
ptr2.reset(); // ptr2 已空,重置无影响
自定义删除器
默认删除器调用 delete/delete[],但可自定义删除器(如管理 FILE*、动态库句柄等)。
c++
// 自定义删除器:关闭文件
void close_file(FILE* fp) {
if (fp) {
fclose(fp);
cout << "文件已关闭" << endl;
}
}
// 创建 unique_ptr,指定删除器类型
unique_ptr<FILE, decltype(&close_file)> file_ptr(fopen("test.txt", "w"), close_file);
if (file_ptr) {
fputs("hello unique_ptr", file_ptr.get());
}
// 析构时自动调用 close_file
Lambda 作为删除器(更简洁)
c++
// 管理动态数组,自定义删除器(示例,实际 delete[] 已足够)
auto deleter = [](int* arr) {
cout << "释放数组内存" << endl;
delete[] arr;
};
unique_ptr<int[], decltype(deleter)> arr_ptr(new int[3], deleter);
其他常用成员函数
get() |
返回原始指针(仅访问,不释放所有权) |
|---|---|
swap() |
交换两个 unique_ptr 的所有权 |
operator bool() |
判断是否指向有效对象(非空) |
场景使用场景
- 替代原始指针,避免内存泄漏
csharp
void func() {
int* ptr = new int(10);
// 若中间抛出异常,delete 不会执行,内存泄漏
delete ptr;
}
// unique_ptr:自动释放
void func_safe() {
unique_ptr<int> ptr(new int(10));
// 即使抛出异常,析构时自动释放
}
- 作为函数返回值(移动语义自动生效)
c++
unique_ptr<Person> create_person(string name, int age) {
// C++11 中,返回值会自动移动(无需显式 std::move)
return unique_ptr<Person>(new Person{name, age});
}
// 调用
auto p = create_person("Bob", 25);
p->show(); // 输出 Bob, 25
- 存储在容器中
arduino
vector<unique_ptr<int>> vec;
// 方式1:emplace_back 直接构造
vec.emplace_back(new int(10));
// 方式2:move 插入
unique_ptr<int> ptr(new int(20));
vec.push_back(move(ptr));
// 遍历容器
for (const auto& p : vec) {
cout << *p << " "; // 输出 10 20
}
其他事项
- 禁止拷贝 :C++11 中
unique_ptr的拷贝构造和拷贝赋值被delete - 避免裸指针与智能指针混用 :不要用
get()返回的指针创建新的unique_ptr,否则会重复释放;
c++
unique_ptr<int> ptr(new int(10));
// 错误:两个 unique_ptr 管理同一内存,析构时双重释放
unique_ptr<int> ptr2(ptr.get());
make_unique更安全 :C++14 引入make_unique,避免 "new 表达式" 和 "智能指针构造" 之间的异常安全问题,C++11 可自行实现:
arduino
template<typename T, typename... Args>
unique_ptr<T> make_unique(Args&&... args) {
return unique_ptr<T>(new T(std::forward<Args>(args)...));
}
-数组管理 :unique_ptr<T[]> 不支持 * 和 ->,仅支持 [] 访问;
- 不要管理栈内存 :
unique_ptr仅用于管理堆内存(new/new[]分配),若指向栈对象,析构时会调用delete,导致未定义行为:
arduino
int a = 10;
unique_ptr<int> ptr(&a); // 错误:栈内存不能被 delete
五. 弱引用智能指正 std::weak_ptr
std::weak_ptr 是 C++11 引入的智能指针,专门用于解决 std::shared_ptr 循环引用导致的内存泄漏问题 ,它本身不管理对象的生命周期,仅作为 shared_ptr 的 "弱引用"------ 既可以访问共享对象,又不会增加引用计数。
核心特性
- 不增加引用计数 :绑定到
shared_ptr时,不会改变其引用计数,因此不会阻止对象被销毁 - 可检测对象是否存活 :通过
expired()或lock()检查所指向的对象是否已被销毁 - 不能直接解引用 :必须先通过
lock()转换为shared_ptr,才能安全访问对象(避免访问已销毁的对象) - 线程安全 :与
shared_ptr类似,读操作线程安全,写操作(如lock())需加锁
lock()是特殊的 "写操作"------ 它会先检查use_count(读),若对象存活则创建shared_ptr,此时会原子性增加use_count(写)
基本用法 std::weak_ptr 不能直接指向裸指针,必须通过 shared_ptr 初始化或赋值:
c++
#include <memory>
#include <iostream>
int main() {
// 1. 通过 shared_ptr 初始化
std::shared_ptr<int> sp = std::make_shared<int>(10);
std::weak_ptr<int> wp(sp); // 引用计数仍为 1
// 2. 空 weak_ptr
std::weak_ptr<int> wp_empty;
// 3. 赋值操作
std::weak_ptr<int> wp2;
wp2 = sp; // 引用计数仍为 1
return 0;
}
核心成员函数
| 函数 | 功能 |
|---|---|
lock() |
转换为 shared_ptr:若对象存活,返回指向该对象的 shared_ptr;否则返回空 shared_ptr |
expired() |
判断对象是否已销毁(引用计数为 0),返回 bool |
reset() |
清空 weak_ptr,不再引用任何对象 |
use_count() |
返回当前 shared_ptr 的引用计数(慎用,仅用于调试) |
swap() |
交换两个 weak_ptr 的引用对象 |
安全访问对象(lock () 核心用法)
weak_ptr 不能直接解引用,必须通过 lock() 获取 shared_ptr 后访问:
c++
// 主函数:程序执行的起点(必须有且仅有一个)
#include <iostream>
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> sp = std::make_shared<int>(100);
std::weak_ptr<int> wp(sp);
// 安全访问:先 lock() 转换为 shared_ptr
if (std::shared_ptr<int> temp = wp.lock()) {
std::cout << "对象值:" << *temp << std::endl; // 输出 100
std::cout << "引用计数:" << temp.use_count() << std::endl; // 2
} else {
std::cout << "对象已销毁" << std::endl;
}
std::cout << "sp 引用计数:" << sp.use_count() << std::endl; // 2
// 销毁 shared_ptr,对象被释放
sp.reset();
// 再次尝试访问
auto temp2 = wp.lock();
if (temp2) {
std::cout << "对象值:" << *temp2 << std::endl; // 输出 100 std::cout << *temp2 << std::endl;
std::cout << "引用计数:" << temp2.use_count() << std::endl; // 2
} else {
std::cout << "对象已销毁" << std::endl; // 输出此行
}
return 0;
}
expired () 检查对象存活
c++
int main() {
std::shared_ptr<int> sp = std::make_shared<int>(200);
std::weak_ptr<int> wp(sp);
if (!wp.expired()) {
std::cout << "对象存活" << std::endl; // 输出
}
sp.reset();
if (wp.expired()) {
std::cout << "对象已销毁" << std::endl; // 输出
}
return 0;
}
解决循环引用问题(核心场景)
shared_ptr 循环引用会导致引用计数无法归 0,对象无法析构,最终内存泄漏。weak_ptr 因不增加引用计数,可打破循环。
c++
struct B; // 前向声明
struct A {
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A 析构" << std::endl; }
};
struct B {
std::shared_ptr<A> a_ptr; // 循环引用
~B() { std::cout << "B 析构" << std::endl; }
};
int main() {
{
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
// 离开作用域时,a 和 b 的引用计数均为 2,无法析构
}
std::cout << "作用域结束" << std::endl; // 输出,但 A/B 未析构
return 0;
}
修改后
c
struct B;
struct A {
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A 析构" << std::endl; }
};
struct B {
std::weak_ptr<A> a_ptr; // 改为 weak_ptr,不增加引用计数
~B() { std::cout << "B 析构" << std::endl; }
};
int main() {
{
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
// a 的引用计数:1(b->a_ptr 是 weak_ptr,不计数)
// b 的引用计数:1(a->b_ptr 是 shared_ptr,计数+1)
}
std::cout << "作用域结束" << std::endl;
return 0;
}