一、使用
1. 抛异常的安全性
cpp
int calc(int a, int b) {
if (b == 0) {
throw("by 0");
} else {
return a / b;
}
}
void func() {
int* p1 = new int[10];
calc(1, 0);
}
-
在这个函数中,
calc抛出异常,int数组就不会被析构,造成内存泄露。 -
除非在每一个
new后面都跟着一个delete[] p1;。
2. RAII 和智能指针设计思路
-
利用对象生命周期管理资源的获取和释放,将资源托管给类的析构函数。
-
并且重载了
*、->等运算符进行调用,使其表现得像指针。
cpp
int calc(int a, int b) {
if (b == 0) {
throw("by 0");
} else {
return a / b;
}
}
void func() {
SmartPtr<int> p1 = new int[10];
calc(1, 0);
}
- 异常时,智能指针对象一定会析构,就能带着其管理的
int*资源一起析构。
3. C++ 标准库的智能指针
- 拷贝问题:由于智能指针是仿指针,因此拷贝也需要仿照指针进行浅拷贝,但这会带来析构两次的问题。
-
auto_ptr: C++98 的智能指针,极其不推荐使用,因为它会让被拷贝的源指针悬空。
https://media/image1.png -
unique_ptr: 不能拷贝,除非被move。 -
shared_ptr: 用引用计数实现拷贝。cppshared_ptr<int> p1(new int[10]); shared_ptr<int> p2 = p1; cout << p1.use_count();- 还可以通过
use_count()查看引用计数。
- 还可以通过
二、shared_ptr 实现
1. 成员变量
cpp
T* _ptr;
int* _count;
-
由于静态变量会统计所有智能指针的个数,而智能指针可能会指向不同的资源,因此不能用静态变量来计数。
-
需要使用动态开辟的指针,当构造时就开辟新的空间来存储计数。
2. 构造函数
cpp
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_count(sp._count) {
(*_count)++;
}
shared_ptr(T* ptr)
:_ptr(ptr) {
_count = new int(1);
}
-
拷贝构造时,引用计数加一。
-
用原始指针构造时,新开一块空间对
_count进行初始化计数。
3. 析构函数
cpp
~shared_ptr() {
if (--(*_count) == 0) {
delete _ptr;
delete _count;
}
}
- 只有引用计数到达 0 时才释放管理的资源。
4. 解引用操作
cpp
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
5. 获取引用计数
cpp
int use_count() {
return *_count;
}
6. 赋值运算符重载(重点)
-
需要先处理当前智能指针管理的资源(如果引用计数到 0 则析构)。
-
然后接收新的资源,并对新的引用计数加一。
cpp
shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
if (sp._ptr != _ptr) {
if (--(*_count) == 0) {
delete _ptr;
delete _count;
}
_count = sp._count;
_ptr = sp._ptr;
++(*_count);
}
return *this;
}
三、定制删除器
-
由于智能指针指向的资源不一定是
new出来的单个T类型对象,还可能是T数组,甚至是打开的文件句柄。 -
直接用
delete或delete[]释放可能会出错。
-
模板特化
-
对于数组,可以使用
shared_ptr<Date[]>,库会进行模板特化,调用delete[]。shared_ptr<Date[]> d1(new Date[10]);
-
-
仿函数
-
对于文件等资源,
delete无法释放。shared_ptr<FILE> p3(fopen("源.cpp", "r")); // 错误,会造成内存泄露 -
需要定义删除器。
cppstruct del { void operator()(FILE* fp) const { if (fp) { fclose(fp); std::cout << "文件已关闭" << std::endl; } } }; shared_ptr<FILE> p3(fopen("源.cpp", "r"), del());
-
-
unique_ptr的删除器-
unique_ptr的删除器类型需要在模板参数中指定,而不是在构造函数中传入。unique_ptr<FILE, del> p4(fopen("源.cpp", "r")); -
也可以使用
lambda表达式,但需要用decltype推导其类型。auto fclosefunc = [](FILE* ptr) { fclose(ptr); }; unique_ptr<FILE, decltype(fclosefunc)> p5(fopen("源.cpp", "r"), fclosefunc);
-
-
模拟实现定制删除器
-
由于需要在构造函数中接收仿函数,可以用
function包装器来存储。function<void(T*)> _del = [](T* t) { delete t; }; -
提供两个构造函数重载,区分是否传入自定义删除器。
cppshared_ptr(T* ptr) :_ptr(ptr) { _count = new int(1); } template <class D> shared_ptr(T* ptr, D del) : _ptr(ptr) , _del(del) { _count = new int(1); } -
析构时调用包装器存储的函数。
cpp~shared_ptr() { if (--(*_count) == 0) { _del(_ptr); delete _count; } }
-
-
make_shared-
由于
shared_ptr需要为对象和控制块(包含引用计数等)分别开辟空间,可能会产生内存碎片。 -
make_shared可以将这两个部分一次性连续开辟,减少碎片。cpp
shared_ptr<Date> p6 = make_shared<Date>();
-
四、循环引用
-
考虑在双向链表中使用
shared_ptr。cppstruct ListNode { int _data; ListNode* _next; ListNode* _prev; ~ListNode() { cout << "~ListNode()" << endl; } }; shared_ptr<bit::ListNode> p1(new bit::ListNode); shared_ptr<bit::ListNode> p2(new bit::ListNode); p1->_next = p2; // 错误,类型不匹配 -
在链表的节点里,如果要用智能指针管理,由于
_next为普通指针,p2为智能指针,因此无法直接赋值。 -
需要将节点内的指针也改为智能指针类型。
cppstruct ListNode { int _data; std::shared_ptr<ListNode> _next; std::shared_ptr<ListNode> _prev; ~ListNode() { cout << "~ListNode()" << endl; } }; -
但是,这样做会导致内存泄露!
-
原因分析:
-
第一个节点的资源由
p1和p2的_prev指向,引用计数为 2。 -
第二个节点的资源由
p2和p1的_next指向,引用计数也为 2。 -
函数结束时,
p1和p2先析构,两个节点的引用计数都变为 1。 -
接着,第一个节点要析构,需要等待第二个节点的
_prev析构。 -
第二个节点要析构,需要等待第一个节点的
_next析构。 -
这就陷入了循环等待,导致两个节点都无法被释放。
-
-
解决方案:
weak_ptr-
将节点内的指针改为
weak_ptr。cppstruct ListNode { int _data; std::weak_ptr<ListNode> _next; std::weak_ptr<ListNode> _prev; ~ListNode() { cout << "~ListNode()" << endl; } }; -
weak_ptr指向资源但不会增加引用计数,从而打破了循环引用,资源可以正常析构。
-