一、智能指针的使用场景分析
C++ 作为公认的高效编程语言,其手动内存管理(new/delete)避免了高级语言(如 Java、Python)自动垃圾回收带来的额外开销。但这把双刃剑对程序员提出了更高要求,手动管理极易引发内存泄漏。
在某些复杂场景中,异常处理会导致资源释放逻辑遗漏。例如,在函数中连续分配内存后执行可能抛出异常的除法操作,若第二个 new 抛出异常,第一个分配的内存将因后续 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() {
// 如果发生除 0 错误抛出异常,下面的 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; // 异常重新抛出
}
cout << "delete []" << array1 << endl;
delete[] array1;
cout << "delete []" << array2 << endl;
delete[] array2;
}
int main() {
try {
func();
} catch (const char* errmsg) { cout << errmsg << endl; }
catch (const exception& e) { cout << e.what() << endl; }
catch (...) { cout << "未知异常" << endl; }
return 0;
}
二、RAII 和智能指针的设计思路
RAII(Resource Acquisition Is Initialization,资源获取即初始化)是一种利用对象生命周期来管理动态资源的设计思想。资源可以是内存、文件句柄、网络连接或互斥锁等。在 RAII 模式下,资源被委托给一个对象管理,只要对象存在,资源就有效;当对象离开作用域(无论是正常退出还是异常退出),析构函数都会自动调用以释放资源。
智能指针类除了满足 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 的智能指针类管理 new 出来的数组以后,程序简单多了
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;
}
int len, time; cin >> len >> time;
cout << divide(len, time) << endl;
// 即使这里抛出异常,sp1 和 sp2 的析构函数也会自动清理内存
}
三、C++ 标准库智能指针的使用
C++ 标准库中的智能指针都在 <memory> 头文件中。主要有以下几种:
- auto_ptr:C++98 时期的产物。由于拷贝会导致所有权转移(原指针置空),容易造成悬空指针访问,C++11 后强烈建议弃用。
- unique_ptr:C++11 引入,独占资源所有权。不支持拷贝,仅支持移动语义。适用于不需要共享资源的场景,性能最优。
- shared_ptr:共享资源所有权,底层采用引用计数。支持拷贝和移动。当引用计数降为 0 时自动释放资源,避免双重释放和泄漏。
- weak_ptr:不管理资源所有权,不增加引用计数。主要用于解决 shared_ptr 的循环引用问题。
struct Date {
int _year, _month, _day;
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day) {}
~Date() { cout << "~Date()" << endl; }
};
int main() {
// auto_ptr 已废弃,仅作了解
// auto_ptr<Date> ap1(new Date);
// auto_ptr<Date> ap2(ap1); // 拷贝后 ap1 变为 nullptr
// unique_ptr 不支持拷贝,支持移动
unique_ptr<Date> up1(new Date);
unique_ptr<Date> up3(move(up1)); // 移动后 up1 悬空
// shared_ptr 支持拷贝,引用计数增加
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1);
shared_ptr<Date> sp3(sp2);
cout << sp1.use_count() << endl; // 输出 3
// 支持移动
shared_ptr<Date> sp4(move(sp1));
return 0;
}
其他使用说明
-
make_shared:相比直接
new,make_shared会将计数器与资源分配在同一块内存中,减少内存碎片,效率更高。它不是成员函数,而是独立模板函数。shared_ptr<Date> sp1(new Date(2024, 9, 11)); shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11); -
operator bool:支持隐式转换为 bool 类型,可用于判断是否为空。
shared_ptr<Date> sp1(new Date); if (sp1) cout << "not nullptr" << endl; if (!sp1) cout << "is nullptr" << endl; -
构造函数 explicit:防止普通指针隐式转换为智能指针对象,强制显式管理所有权。
// 报错:无法隐式转换 // shared_ptr<Date> sp5 = new Date; -
定制删除器:默认析构使用
delete。对于new[]或malloc等资源,需指定匹配的删除器(如delete[]或free)。unique_ptr特化了数组版本,shared_ptr可通过构造函数传入自定义删除器。// 管理 new[] 资源 unique_ptr<Date[]> up1(new Date[5]); shared_ptr<Date[]> sp1(new Date[5]); // 自定义删除器示例(lambda) auto delArrOBJ = [](Date* ptr){ delete[] ptr; }; unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
四、智能指针的原理
核心实现模拟
以下模拟实现了 auto_ptr、unique_ptr 和 shared_ptr 的核心功能,帮助理解底层逻辑。
namespace wusaqi {
template<class T>
class auto_ptr {
public:
auto_ptr(T* ptr) : _ptr(ptr) {}
// 破坏性拷贝,转移所有权
auto_ptr(auto_ptr<T>& sp) : _ptr(sp._ptr) { sp._ptr = nullptr; }
auto_ptr<T>& operator=(auto_ptr<T>& ap) {
if (this != &ap) {
if (_ptr) delete _ptr;
_ptr = ap._ptr; ap._ptr = NULL;
}
return *this;
}
~auto_ptr() { if (_ptr) { cout << "delete:" << _ptr << endl; delete _ptr; } }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
template<class T>
class unique_ptr {
public:
explicit unique_ptr(T* ptr) : _ptr(ptr) {}
~unique_ptr() { if (_ptr) { cout << "delete:" << _ptr << endl; delete _ptr; } }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
// 禁用拷贝
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = 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;
};
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() {
if (--(*_pcount) == 0) {
delete _ptr; delete _pcount;
_ptr = nullptr; _pcount = nullptr;
}
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
int use_count() const { return *_pcount; }
private:
T* _ptr;
int* _pcount;
};
}
shared_ptr 源码深度解析
- 基本结构:包含指向资源的指针
_ptr和指向引用计数器的指针_pcount。 - 引用计数:每次拷贝构造或赋值,计数器加一;析构时减一。计数归零时同时释放资源和计数器。
- 自赋值处理:赋值前检查是否指向同一资源,避免重复释放。
- 定制删除器:通过模板参数或构造函数传入可调用对象(如 lambda),替代默认的
delete操作,确保资源释放方式正确。
namespace wusaqi {
template<class T>
class shared_ptr {
public:
shared_ptr(T* ptr = nullptr) : _ptr(ptr), _pcount(new int(1)) {}
template<class D>
shared_ptr(T* ptr, D del) : _ptr(ptr), _pcount(new int(1)), _del(del) {}
shared_ptr(const shared_ptr<T>& sp) : _ptr(sp._ptr), _pcount(sp._pcount), _del(sp._del) {
++(*_pcount);
}
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;
}
~shared_ptr() { release(); }
T* get() const { return _ptr; }
int use_count() const { return *_pcount; }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr = nullptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr){ delete ptr; };
};
}
五、shared_ptr 和 weak_ptr
shared_ptr 循环引用问题
当两个 shared_ptr 互相持有对方时,引用计数永远不会归零,导致内存泄漏。例如双向链表节点互相持有对方的 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->_prev = n1;
// 此时 n1 和 n2 的引用计数均为 2,main 结束后不会析构
return 0;
}
weak_ptr 解决方案
weak_ptr 不管理资源所有权,不增加引用计数,专门用于绑定 shared_ptr。它将强引用改为弱引用,打破循环依赖。
struct ListNode {
int _data;
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; // 绑定到 weak_ptr 不增加 n2 的计数
n2->_prev = n1;
// 引用计数恢复正常,main 结束后可正确释放
return 0;
}
weak_ptr 不能直接访问资源,需调用 lock() 方法尝试获取临时的 shared_ptr。若资源已销毁,lock() 返回空指针,从而避免悬垂指针风险。
std::shared_ptr<string> sp1(new string("111111"));
std::weak_ptr<string> wp = sp1;
if (!wp.expired()) {
auto sp3 = wp.lock(); // 安全访问
*sp3 += "###";
}
六、内存泄漏
什么是内存泄漏及危害
内存泄漏指程序未能释放不再使用的内存。短期运行的程序影响较小,但长期运行的服务(如操作系统、后台进程)若持续泄漏,会导致可用内存耗尽,系统响应变慢甚至崩溃。
如何避免内存泄漏
- 事前预防:优先使用智能指针管理动态资源,遵循 RAII 原则。特殊场景下自行封装资源管理类。
- 事后检测:定期使用内存泄漏检测工具(如 Valgrind、AddressSanitizer)扫描代码,尤其在上线前。
- 规范编码:确保
new与delete、new[]与delete[]严格匹配,注意异常安全。


