C++ 智能指针:使用场景、实现原理与内存泄漏防治
一、智能指针的使用场景分析
C++ 以高效著称,手动内存管理(new/delete)避免了高级语言垃圾回收的开销,但这把双刃剑对程序员要求极高。手动管理极易导致内存泄漏,尤其是在异常处理场景中。
例如,在函数中连续分配资源时,若中间发生异常,后续的 delete 可能无法执行,导致资源泄露。如果为了捕获异常而嵌套 try-catch 块,代码会变得极其臃肿且难以维护。智能指针的出现正是为了解决这类问题,通过对象生命周期自动管理资源。
double Divide(int a, int b) {
if (b == 0) throw "Divide by zero condition!";
else return (double)a / (double)b;
}
void Func() {
// 原始方案:若 Divide 抛出异常,array1 和 array2 未释放
int* array1 = new int[10];
int* array2 = new int[10];
try {
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
} catch (...) {
cout << "delete []" << array1 << endl;
cout << "delete []" << array2 << endl;
delete[] array1;
delete[] array2;
throw; // 重新抛出异常
}
// ... 后续逻辑
delete[] array1;
delete[] array2;
}
二、RAII 和智能指针的设计思路
RAII(Resource Acquisition Is Initialization,资源获取即初始化)是一种利用对象生命周期管理资源的类设计思想。资源(内存、文件句柄、锁等)在构造时获取,在析构时释放。无论函数正常返回还是因异常退出栈帧,析构函数都会被调用,从而保证资源释放。
智能指针类除了满足 RAII,还需像迭代器一样重载运算符(operator*, operator->, operator[]),以便透明地访问底层资源。
template<class T>
class SmartPtr {
public:
// RAII: 构造获取资源,析构释放资源
SmartPtr(T* ptr) : _ptr(ptr) {}
~SmartPtr() {
cout << "delete[] " << _ptr << endl;
delete[] _ptr;
}
// 重载运算符,模拟指针行为
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
T& operator[](size_t i) { return _ptr[i]; }
private:
T* _ptr;
};
void Func() {
// 使用 RAII 智能指针后,无需手动 delete,异常也能安全释放
SmartPtr<int> sp1(new int[10]);
SmartPtr<int> sp2(new int[10]);
for (size_t i = 0; i < 10; i++) {
sp1[i] = sp2[i] = i;
}
// 即使 Divide 抛异常,sp1 和 sp2 析构时也会自动释放内存
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
三、C++ 标准库智能指针的使用
C++ 标准库中的智能指针定义在 <memory> 头文件中。主要包括 unique_ptr、shared_ptr 和 weak_ptr。auto_ptr 是 C++98 遗留产物,因拷贝语义混乱已被废弃。
1. unique_ptr(独占所有权)
unique_ptr 翻译为唯一指针,不支持拷贝,仅支持移动(右值)。适用于不需要共享资源的场景,性能最高。
unique_ptr<Date> up1(new Date);
// unique_ptr<Date> up2(up1); // 错误:禁止拷贝
unique_ptr<Date> up3(move(up1)); // 正确:移动后 up1 悬空
2. shared_ptr(共享所有权)
shared_ptr 支持拷贝和移动,底层通过引用计数实现多指针共享同一资源。当引用计数归零时自动释放资源。
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1); // 引用计数 +1
shared_ptr<Date> sp3(sp2); // 引用计数 +1
cout << sp1.use_count() << endl; // 输出 3
优化建议: 推荐使用 make_shared。它会将控制块和资源分配在同一块内存中,减少内存碎片并提高缓存命中率。
shared_ptr<Date> sp1 = make_shared<Date>(2024, 9, 11);
3. weak_ptr(弱引用)
weak_ptr 不管理资源,不增加引用计数,专门用于解决 shared_ptr 的循环引用问题。需配合 lock() 方法使用。
4. 定制删除器
默认删除器使用 delete。对于 new[] 或 malloc 的资源,需指定匹配的删除器(如 delete[] 或 free)。
// 管理数组资源
unique_ptr<Date[]> up1(new Date[5]);
// 自定义删除器示例(lambda)
auto delArrOBJ = [](Date* ptr) { delete[] ptr; };
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
四、智能指针的原理
1. unique_ptr 实现核心
禁用拷贝构造和赋值,仅允许移动。确保同一时刻只有一个指针拥有资源所有权。
template<class T>
class unique_ptr {
public:
explicit unique_ptr(T* ptr) : _ptr(ptr) {}
~unique_ptr() { if (_ptr) { delete _ptr; _ptr = nullptr; } }
// 禁用拷贝
unique_ptr(const unique_ptr<T>&) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;
// 支持移动
unique_ptr(unique_ptr<T>&& sp) : _ptr(sp._ptr) { sp._ptr = nullptr; }
unique_ptr<T>& operator=(unique_ptr<T>&& sp) {
delete _ptr; _ptr = sp._ptr; sp._ptr = nullptr; return *this;
}
private:
T* _ptr;
};
2. shared_ptr 实现核心
需要维护两份数据:资源本身和控制块(含引用计数和删除器)。
- 构造函数:初始化资源指针和控制块计数器(初始为 1)。
- 拷贝构造:复制指针,计数器 +1。
- 析构/赋值:计数器 -1,若为 0 则释放资源和控制块。
template<class T>
class shared_ptr {
public:
shared_ptr(T* ptr = nullptr) : _ptr(ptr), _pcount(new int(1)) {}
shared_ptr(const shared_ptr<T>& sp) : _ptr(sp._ptr), _pcount(sp._pcount) {
++(*_pcount); // 拷贝时计数加一
}
~shared_ptr() {
release();
}
void release() {
if (--(*_pcount) == 0) {
_del(_ptr); // 使用删除器释放资源
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
// 赋值运算符重载
shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
if (_ptr != sp._ptr) {
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
_del = sp._del;
++(*_pcount);
}
return *this;
}
private:
T* _ptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr){ delete ptr; }; // 默认删除器
};
五、shared_ptr 和 weak_ptr
1. 循环引用问题
两个 shared_ptr 互相持有对方时,引用计数永远无法归零,导致内存泄漏。
struct ListNode {
int _data;
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
~ListNode() { cout << "~ListNode()" << endl; }
};
int main() {
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
n1->_next = n2; // n2 引用计数变为 2
n2->_prev = n1; // n1 引用计数变为 2
// 析构时,两者计数均为 2,永远不会释放
return 0;
}
2. 解决方案:weak_ptr
将其中一个方向改为 weak_ptr。weak_ptr 不增加引用计数,仅观察资源。当最后一个 shared_ptr 销毁时,资源被释放,weak_ptr 失效。
struct ListNode {
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
~ListNode() { cout << "~ListNode()" << endl; }
};
int main() {
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
n1->_next = n2; // n2 引用计数仍为 1
n2->_prev = n1; // n1 引用计数仍为 1
// 析构时,计数归零,资源正常释放
return 0;
}
访问 weak_ptr 绑定的资源前,需调用 lock() 获取临时的 shared_ptr。若资源已释放,lock() 返回空指针。
六、内存泄漏
1. 概念与危害
内存泄漏指程序未能释放不再使用的内存。短期运行影响不大,但长期运行的服务(如操作系统、后台进程)会因可用内存减少而变慢甚至崩溃。
2. 如何避免
- 事前预防:优先使用智能指针(RAII 机制),遵循'谁申请谁释放'原则。
- 特殊场景:若必须手动管理,确保异常路径下也能释放资源。
- 事后检测:定期使用工具(如 Valgrind)扫描内存泄漏。
总结来说,现代 C++ 开发应尽量避免裸指针,利用智能指针体系保障内存安全。


