在 C++ 开发中,动态内存管理(new/delete)是最容易引发问题的环节 —— 忘记释放内存会导致内存泄漏,重复释放会导致程序崩溃,异常抛出会跳过 delete 语句。为解决这些痛点,C++ 引入了智能指针,通过 RAII(资源获取即初始化)思想,将内存管理与对象生命周期绑定,实现 “自动释放资源”。本文将从智能指针的设计背景入手,详细讲解 auto_ptr、unique_ptr、shared_ptr、weak_ptr 的原理、使用场景及注意事项,并结合代码示例展示如何在工程中避免内存泄漏。
一、智能指针的诞生:解决动态内存管理痛点
在理解智能指针前,我们先看一个典型的 “内存泄漏” 场景:当代码中抛出异常时,delete 语句会被跳过,导致动态内存无法释放。
1.1 未使用智能指针的风险
#include <iostream>
using namespace std;
// 模拟除法异常:除数为 0 时抛出异常
double Divide(int a, int b) {
if (b == 0) {
throw "Divide by zero!"; // 抛出异常,后续代码不执行
}
return static_cast<double>(a) / b;
}
void Func() {
// 动态申请内存
int* arr1 = new int[10];
int* arr2 = new int[10]; // 若此处抛出内存分配异常,arr1 无法释放
try {
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
} catch (...) {
// 捕获异常后释放内存,再重新抛出
delete[] arr1;
delete[] arr2;
throw;
}
// 正常执行时释放内存
delete[] arr1;
delete[] arr2;
}
int main() {
try {
Func();
} catch (const char* errmsg) {
cout << errmsg << endl;
}
return 0;
}
问题分析:
若 new int[10] 失败(抛出 bad_alloc 异常),arr1 已申请的内存无法释放;
若 Divide 抛出异常,需在 catch 中手动释放内存,代码冗余且容易遗漏。
1.2 智能指针的核心思路:RAII
智能指针的本质是封装动态内存的类,遵循 RAII 设计思想:
- 资源获取:在智能指针对象构造时,获取动态内存(接收
new返回的指针); - 资源持有:智能指针对象生命周期内,管理该内存的访问(重载
*/->模拟指针行为); - 资源释放:在智能指针对象析构时,自动释放内存(调用
delete或自定义删除器)。
简化智能指针示例:
template <class T>
class SmartPtr {
public:
// 构造:获取资源
SmartPtr(T* ptr) : _ptr(ptr) {}
// 析构:释放资源
~SmartPtr() {
delete[] _ptr; // 自动释放,无需手动调用
cout << "Memory released: " << _ptr << endl;
}
// 重载 *:访问资源
T& operator*() { return *_ptr; }
// 重载 ->:访问资源成员
T* operator->() { return _ptr; }
// 重载 []:支持数组访问
T& operator[](size_t i) { return _ptr[i]; }
private:
T* _ptr; // 管理的动态内存指针
};
// 使用智能指针后,无需手动释放内存
void FuncWithSmartPtr() {
SmartPtr<int> sp1(new int[10]); // 构造时获取资源
SmartPtr<int> sp2(new int[10]);
// 正常访问资源
for (size_t i = 0; i < 10; ++i) {
sp1[i] = i;
sp2[i] = i * 2;
}
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
// 析构时自动释放内存,即使抛出异常也会执行
}
二、C++ 标准库智能指针详解
C++ 标准库提供了 4 种智能指针(定义在 <memory> 头文件中),核心差异在于拷贝语义和资源管理方式。
2.1 auto_ptr:被废弃的早期智能指针(不推荐)
auto_ptr 是 C++98 引入的第一个智能指针,核心缺陷是拷贝时转移资源管理权,导致原对象 “悬空”(指针变为 nullptr),访问时会触发未定义行为。
2.1.1 auto_ptr 的问题示例
#include <memory>
using namespace std;
struct Date {
int _year, _month, _day;
Date(int y=1, int m=1, int d=1) : _year(y), _month(m), _day(d) {}
~Date() { cout << "~Date()" << endl; }
};
int main() {
auto_ptr<Date> ap1(new Date(2024, 10, 1));
auto_ptr<Date> ap2(ap1); // 拷贝:ap1 的资源管理权转移给 ap2,ap1 悬空
// ap1->_year = 2025; // 崩溃:ap1 已悬空(指针为 nullptr)
ap2->_year = 2025; // 正常访问
return 0;
}
结论:auto_ptr 设计缺陷严重,C++11 后被 unique_ptr 替代,禁止在工程中使用。
2.2 unique_ptr:独占所有权的智能指针(推荐)
unique_ptr 是 C++11 引入的 “独占型” 智能指针,核心特性是禁止拷贝,仅支持移动(move),确保同一时间只有一个 unique_ptr 管理资源,避免资源竞争。
2.2.1 unique_ptr 的核心特性
- 禁止拷贝:通过
delete拷贝构造和拷贝赋值,防止意外拷贝; - 支持移动:通过移动构造 / 赋值,将资源管理权转移给新对象(原对象悬空);
- 支持数组:提供
unique_ptr<T[]>特化版本,析构时自动调用delete[]; - 自定义删除器:支持传入自定义释放逻辑(如释放文件指针、网络连接)。
2.2.2 unique_ptr 使用示例
#include <memory>
#include <iostream>
using namespace std;
struct Date {
int _year, _month, _day;
Date(int y=1, int m=1, int d=1) : _year(y), _month(m), _day(d) {}
~Date() { cout << "~Date()" << endl; }
};
// 自定义删除器:释放数组(unique_ptr<T[]> 已内置,此处仅示例)
template <class T>
struct DeleteArray {
void operator()(T* ptr) {
cout << "Delete array: " << ptr << endl;
delete[] ptr;
}
};
int main() {
// 1. 管理单个对象
unique_ptr<Date> up1(new Date(2024, 10, 1));
// unique_ptr<Date> up2(up1); // 编译报错:禁止拷贝
unique_ptr<Date> up3(move(up1)); // 移动:up1 悬空,up3 管理资源
// 2. 管理数组(使用特化版本)
unique_ptr<Date[]> up4(new Date[5]); // 析构时调用 delete[]
// 3. 自定义删除器(如释放文件指针)
FILE* fp = fopen("test.txt", "r");
if (fp) {
// lambda 作为删除器:关闭文件
unique_ptr<FILE, void(*)(FILE*)> up5(fp, [](FILE* p) {
fclose(p);
cout << "File closed: " << p << endl;
});
}
return 0;
}
适用场景:不需要拷贝的场景(如局部动态对象、函数返回值),是工程中首选智能指针。
2.3 shared_ptr:共享所有权的智能指针
shared_ptr 是 C++11 引入的 “共享型” 智能指针,支持拷贝和移动,通过引用计数实现多个 shared_ptr 共享同一资源:
每次拷贝 shared_ptr,引用计数 +1;
每次析构 shared_ptr,引用计数 -1;
当引用计数降至 0 时,自动释放资源。
2.3.1 shared_ptr 的核心原理
template <class T>
class shared_ptr {
public:
// 构造:初始化资源和引用计数(引用计数在堆上)
explicit shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _ref_count(new int(1)) {} // 引用计数初始为 1
// 拷贝构造:共享资源,引用计数 +1
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _ref_count(sp._ref_count) {
++(*_ref_count);
}
// 析构:引用计数 -1,为 0 时释放资源
~shared_ptr() {
if (--(*_ref_count) == 0) {
delete _ptr; // 释放资源
delete _ref_count; // 释放引用计数
cout << "Resource released" << endl;
}
}
// 重载 * 和 ->:访问资源
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
// 获取引用计数
int use_count() const { return *_ref_count; }
private:
T* _ptr; // 管理的资源指针
int* _ref_count; // 引用计数(堆上存储,确保共享)
};
2.3.2 shared_ptr 使用示例
#include <memory>
#include <iostream>
using namespace std;
struct Date {
int _year, _month, _day;
Date(int y=1, int m=1, int d=1) : _year(y), _month(m), _day(d) {}
~Date() { cout << "~Date()" << endl; }
};
int main() {
// 1. 共享资源
shared_ptr<Date> sp1(new Date(2024, 10, 1));
shared_ptr<Date> sp2(sp1); // 拷贝:引用计数 = 2
shared_ptr<Date> sp3 = sp2; // 拷贝:引用计数 = 3
cout << "sp1 use_count: " << sp1.use_count() << endl; // 输出 3
cout << "sp2 use_count: " << sp2.use_count() << endl; // 输出 3
// 2. 移动构造:原对象悬空,引用计数不变
shared_ptr<Date> sp4(move(sp1)); // 引用计数仍为 3,sp1 悬空
// 3. 推荐使用 make_shared(更高效,避免内存泄漏)
auto sp5 = make_shared<Date>(2024, 10, 2); // 一次分配资源和引用计数
return 0;
}
注意:make_shared 比直接 new 更高效(仅分配一次内存),且能避免 “资源已分配但引用计数分配失败” 的内存泄漏,优先使用 make_shared 构造 shared_ptr。
2.4 weak_ptr:解决 shared_ptr 循环引用的辅助指针
shared_ptr 存在一个致命问题:循环引用—— 两个 shared_ptr 互相引用,导致引用计数无法降至 0,资源无法释放,引发内存泄漏。
2.4.1 循环引用示例(链表节点)
#include <memory>
#include <iostream>
using namespace std;
struct ListNode {
int _data;
shared_ptr<ListNode> _next; // 指向 next 节点
shared_ptr<ListNode> _prev; // 指向 prev 节点
~ListNode() { cout << "~ListNode()" << endl; } // 不会执行,内存泄漏
};
int main() {
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
n1->_next = n2; // n2 引用计数 +1(变为 2)
n2->_prev = n1; // n1 引用计数 +1(变为 2)
// 析构 n1 和 n2 时,引用计数各减 1(变为 1),无法释放资源
return 0;
}
问题分析:
n1 析构时,n1 的引用计数从 2 减为 1(n2->_prev 仍引用 n1);
n2 析构时,n2 的引用计数从 2 减为 1(n1->_next 仍引用 n2);
最终两个节点的引用计数均为 1,资源无法释放。
2.4.2 weak_ptr 的解决方案
weak_ptr 是专门为解决 shared_ptr 循环引用设计的 “弱指针”,核心特性:
- 不增加引用计数:绑定
shared_ptr时,不改变其引用计数; - 不管理资源:无 RAII 特性,不能直接访问资源(无
*/->重载); - 安全访问:通过
lock()方法获取shared_ptr,若资源已释放则返回空对象。
2.4.3 使用 weak_ptr 解决循环引用
#include <memory>
#include <iostream>
using namespace std;
struct ListNode {
int _data;
weak_ptr<ListNode> _next; // 改为 weak_ptr
weak_ptr<ListNode> _prev; // 改为 weak_ptr
~ListNode() { cout << "~ListNode()" << endl; } // 正常执行
};
int main() {
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
n1->_next = n2; // weak_ptr 绑定,n2 引用计数仍为 1
n2->_prev = n1; // weak_ptr 绑定,n1 引用计数仍为 1
// 析构 n1 和 n2 时,引用计数各减为 0,资源正常释放
return 0;
}
2.4.4 weak_ptr 访问资源示例
int main() {
shared_ptr<string> sp1 = make_shared<string>("hello");
weak_ptr<string> wp = sp1; // 绑定 shared_ptr,不增加引用计数
// 检查资源是否有效
cout << "expired: " << wp.expired() << endl; // 输出 0(有效)
cout << "use_count: " << wp.use_count() << endl; // 输出 1
// 安全访问资源:通过 lock() 获取 shared_ptr
if (shared_ptr<string> sp2 = wp.lock()) {
*sp2 += " world";
cout << *sp1 << endl; // 输出 "hello world"
}
// 释放 sp1,资源失效
sp1.reset();
cout << "expired: " << wp.expired() << endl; // 输出 1(失效)
return 0;
}
三、智能指针的高级特性与注意事项
3.1 自定义删除器
智能指针默认使用 delete 释放资源,若资源不是通过 new 分配(如 new[]、fopen、malloc),需自定义删除器(可调用对象:函数指针、仿函数、lambda)。
3.1.1 shared_ptr 自定义删除器(构造函数参数)
#include <memory>
#include <cstdio>
using namespace std;
// 1. 仿函数删除器:释放数组
template <class T>
struct DeleteArray {
void operator()(T* ptr) {
delete[] ptr;
cout << "Array deleted: " << ptr << endl;
}
};
int main() {
// 方式 1:仿函数删除器
shared_ptr<Date> sp1(new Date[5], DeleteArray<Date>());
// 方式 2:lambda 删除器(释放文件)
FILE* fp = fopen("test.txt", "r");
if (fp) {
shared_ptr<FILE> sp2(fp, [](FILE* p) {
fclose(p);
cout << "File closed: " << p << endl;
});
}
return 0;
}
3.1.2 unique_ptr 自定义删除器(模板参数)
#include <memory>
using namespace std;
// 函数指针删除器:释放数组
template <class T>
void DeleteArrayFunc(T* ptr) {
delete[] ptr;
cout << "Array deleted: " << ptr << endl;
}
int main() {
// 模板参数指定删除器类型,构造函数传入删除器
unique_ptr<Date, void(*)(Date*)> up1(new Date[5], DeleteArrayFunc<Date>());
// lambda 删除器(需用 decltype 获取类型)
auto del = [](Date* ptr) { delete[] ptr; };
unique_ptr<Date, decltype(del)> up2(new Date[5], del);
return 0;
}
3.2 shared_ptr 的线程安全问题
shared_ptr 存在两个线程安全相关点:
- 引用计数线程安全:标准库
shared_ptr的引用计数使用原子操作(atomic<int>),多线程拷贝 / 析构shared_ptr时不会出现竞争; - 资源线程安全:
shared_ptr指向的资源本身不具备线程安全,需外层加锁(如mutex)保护。
3.2.1 线程安全示例
#include <memory>
#include <thread>
#include <mutex>
#include <iostream>
using namespace std;
struct AA {
int _a1 = 0;
int _a2 = 0;
mutex _mtx; // 保护资源的互斥锁
};
int main() {
shared_ptr<AA> p = make_shared<AA>();
const size_t n = 100000;
mutex mtx;
// 线程函数:拷贝 shared_ptr 并修改资源
auto func = [&]() {
for (size_t i = 0; i < n; ++i) {
// 拷贝 shared_ptr:引用计数原子操作,线程安全
shared_ptr<AA> copy(p);
// 修改资源:需加锁保护
lock_guard<mutex> lk(p->_mtx);
copy->_a1++;
copy->_a2++;
}
};
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << "a1: " << p->_a1 << ", a2: " << p->_a2 << endl; // 输出 200000, 200000
return 0;
}
3.3 智能指针的禁用场景
-
不要管理栈上对象:智能指针析构时调用
delete,栈上对象会被重复释放(程序崩溃);int main() { int a = 10; // unique_ptr<int> up(&a); // 崩溃:析构时 delete 栈上对象 return 0; } -
不要重复管理同一资源:多个智能指针管理同一裸指针,会导致重复释放(程序崩溃);
int main() { int* ptr = new int(10); unique_ptr<int> up1(ptr); // unique_ptr<int> up2(ptr); // 崩溃:重复 delete return 0; } -
避免
shared_ptr循环引用:若无法修改类成员为weak_ptr,可通过 “打破引用链”(如在合适时机调用reset())解决。
四、智能指针的发展与工程实践
4.1 智能指针的历史演进
| 标准 / 库 | 智能指针类型 | 核心特性 |
|---|---|---|
| C++98 | auto_ptr | 拷贝转移所有权,设计缺陷,已废弃 |
| Boost | scoped_ptr/shared_ptr | scoped_ptr 禁止拷贝,shared_ptr 引用计数,为 C++11 提供参考 |
| C++11 | unique_ptr/shared_ptr | unique_ptr 替代 scoped_ptr,shared_ptr 支持共享,weak_ptr 解决循环引用 |
| C++14 | make_shared 优化 | 支持完美转发,更高效的内存分配 |
4.2 工程中智能指针的选型建议
| 场景需求 | 推荐智能指针 | 原因 |
|---|---|---|
| 不需要拷贝,独占资源 | unique_ptr | 轻量高效,禁止拷贝避免资源竞争,优先选择 |
| 需要拷贝,共享资源 | shared_ptr | 支持多对象共享资源,需注意循环引用(用 weak_ptr 解决) |
解决 shared_ptr 循环引用 | weak_ptr | 不增加引用计数,辅助访问共享资源 |
| 管理数组 | unique_ptr<T[]> | 内置 delete[] 支持,比 shared_ptr<T[]> 更轻量 |
| 管理非内存资源(文件、锁) | 自定义删除器的智能指针 | 通过删除器实现资源释放逻辑,如 shared_ptr<FILE> 管理文件指针 |
五、总结
智能指针是 C++ 动态内存管理的 “终极解决方案”,通过 RAII 思想实现资源自动释放,避免内存泄漏和重复释放问题。核心要点:
unique_ptr:独占资源,禁止拷贝,工程首选;shared_ptr:共享资源,引用计数,需警惕循环引用;weak_ptr:辅助shared_ptr,解决循环引用,不管理资源;- 自定义删除器:支持非
new资源的释放,如数组、文件、网络连接。

被折叠的 条评论
为什么被折叠?



