一直以来, C++一直最令人诟病的问题之一是什么?
没错, 就是内存泄漏
C语言还好一些, 但在C++中, 异常的相关处理出现之后, 内存泄漏的问题简直是防不胜防
而且, C++也不像Java那样带有垃圾自动回收机制, 所以在C++11之前, C++的内存管理一直是一件非常麻烦的事情
虽然现在也麻烦, 不过已经好了很多
博主 C++异常相关文章:
智能指针
C++在引入了异常的概念之后, 内存泄漏的问题就非常有可能出现
比如这段代码:
#include <iostream>#include <exception>
using std::cin;using std::cout;using std::endl;using std::exception;using std::invalid_argument;
int div() { int a, b; cin >> a >> b; if (b == 0) throw invalid_argument("除0错误");
return a / b;}
void Func() { // 1、如果p1这里new 抛异常会如何? // 2、如果p2这里new 抛异常会如何? // 3、如果div调用这里又会抛异常会如何? int* p1 = new int; int* p2 = new int;
cout << div() << endl;
delete p1; delete p2;}
int main() { try { Func(); } catch (exception& e) { cout << e.what() << endl; }
return 0;}注释的三个问题, 各自都会产生什么结果?
-
如果
int* p1 = new int;时抛异常, 其实是空间开辟失败了, 所以不会发生内存泄漏 -
如果
int* p2 = new int;时抛异常, 那就说明p1空间是开辟成功了的, 所以如果这里抛异常, 会导致p1指向的空间无法被delete, 会发生内存泄漏 -
而, 如果
div()执行时抛异常, 那么 就会导致p1和p2所指向的空间泄露
不过, 如果是第3种情况, 可以直接这样解决:
void Func() { int* p1 = new int; int* p2 = new int;
try { cout << div() << endl; } catch (...) { delete p1; delete p2; throw; }
delete p1; delete p2;}但是, 如果类似第2种情况, 就无法相对方便的解决
虽然看似可以模仿上面的解决方法尝试解决问题, 但实际上是无法解决的
为什么呢?
int* p2 = new int;时抛异常, 我们捕捉到异常之后, 就需要将p1的空间释放掉
但如果是这种情况呢?
void Func() { int* p1 = new int; int* p2 = new int; int* p3 = new int; int* p4 = new int; int* p5 = new int;
try { cout << div() << endl; } catch (...) { delete p1; delete p2; delete p3; delete p4; delete p5; throw; }
delete p1; delete p2; delete p3; delete p4; delete p5;}如果是在p3、p4或p5任意一个位置抛异常了呢?
如果是p3, 那就需要释放 p1 p2; 如果是p4, 那就需要释放 p1 p2 p3; 如果是p5, 那就需要释放 p1 p2 p3 p4
如果 开辟的操作更多, 那么不同位置抛异常, 要处理的情况就不同
这种情况, 就会非常的麻烦.
所以, C++11 正式引入了智能指针
RAII 思想
RAII (Resource Acquisition Is Initialization) 资源获取即初始化 是一种 利用对象生命周期来控制程序资源 的技术
什么意思呢?
C++中, 一个对象在生命周期即将结束时 是会自动调用析构函数释放资源的
进而可以利用这一点, 将我们所需要获取的资源 来托管给对象, 通过对象来进行管理、访问、使用资源
即, 实现一个类, 其成员变量包括所需资源, 在实例化对象时 通过构造函数开辟并初始化资源, 在对象生命周期快要结束时 对象会自动调用析构函数, 将开辟出来的空间释放
这样做有什么好处呢?
-
不需要显式地释放资源
-
所需的资源在对象生命周期内始终保持有效
比如, 这段代码:
#include <iostream>#include <exception>
using std::cin;using std::cout;using std::endl;using std::exception;using std::invalid_argument;
template <class T>class SmartPtr {public: SmartPtr(T* ptr = nullptr) : _ptr(ptr) {}
~SmartPtr() { if (_ptr) { cout << "析构 释放资源" << _ptr << endl; delete _ptr; } }
private: T* _ptr;};
int div() { int a, b; cin >> a >> b; if (b == 0) throw invalid_argument("除0错误");
return a / b;}
void Func() { // int* p1 = new int; // int* p2 = new int;
SmartPtr<int> p1(new int); SmartPtr<int> p2(new int);
cout << div() << endl;}
int main() { try { Func(); } catch (exception& e) { cout << e.what() << endl; }
return 0;}这段代码, 如果发生异常 会是什么结果?

通过传入new int调用构造函数, 实例化一个 SmartPtr对象
SmartPtr对象的成员函数是一个指针, 用来指向开辟资源的空间
构造函数将_ptr指向new int出来的空间, 析构函数delete掉_ptr指向的空间
这就实现了, 不用手动的delete new出来的资源, 对象生命周期结束, 析构函数会自动释放.
这就是 智能指针的实现思想, 也是RAII思想
智能指针原理
对于上面的SmartPtr, 还不能将其称之为智能指针
因为, 它还不具有指针的行为: *解引用、->等
所以, 智能指针至少还需要 重载*和->, 不过这两个操作符的重载很简单:
template <class T>class SmartPtr {public: SmartPtr(T* ptr = nullptr) : _ptr(ptr) {}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
~SmartPtr() { if (_ptr) { cout << "析构 释放资源" << _ptr << endl; delete _ptr; } }
private: T* _ptr;};这样就可以实现, 直接通过对象进行*解引用和->相关操作:
struct Date { int _year; int _month; int _day;};
int main() { SmartPtr<int> sp1(new int); *sp1 = 10; cout << *sp1 << endl;
SmartPtr<Date> sparray(new Date); sparray->_year = 2018; sparray->_month = 1; sparray->_day = 1;
cout << sparray->_year << "-" << sparray->_month << "-" << sparray->_day << endl;
return 0;}这段代码执行结果为:

可以直接通过对象, 来实现指针相关的操作
智能指针的问题
从上面来看, 智能指针的原理是不是非常的简单?
但智能指针的实现, 其实还存在着另外的问题: 对象的拷贝构造和赋值怎么实现?
在普通指针的使用中, 经常会有这样的操作:

在指针定义时, 直接用其他指针初始化 或 用指针给指针赋值
转换为SmartPtr, 就大概是这样:

对于类来说 SmartPtr<int> sp2 = sp1就是拷贝构造
所以 智能指针是需要实现拷贝构造和赋值重载的
但问题是, 该如何实现呢? 需要使用 深拷贝还是浅拷贝?
**一定是需要实现浅拷贝 **
因为无论是 普通指针给普通指针初始化 还是普通指针给普通指针赋值
这两个操作完成之后, 这两个普通指针一定是指向同一块空间的, 即两个指针维护同一块空间
那么, 智能指针也需要实现相同的逻辑, 所以 无论是拷贝构造还是赋值重载都不能实现深拷贝
浅拷贝的实现旧很简单了:
template <class T>class SmartPtr {public: SmartPtr(T* ptr = nullptr) : _ptr(ptr) {}
// 拷贝构造 SmartPtr(const SmartPtr<T>& sp) : _ptr(sp._ptr) {}
// 赋值重载 SmartPtr<T>& operator=(const SmartPtr<T>& sp){ // 防止自我赋值 if(this != &sp) { // 先清除被赋值对象中的资源 if(_ptr) delete _ptr;
_ptr = sp._ptr; } }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
~SmartPtr() { if (_ptr) { cout << "析构 释放资源" << _ptr << endl; delete _ptr; } }
private: T* _ptr;};都浅拷贝实现, 只需要将传入的SmartPtr对象维护的资源 赋值给当前对象需要维护的资源变量就可以了
但是, 如果只是这样处理会出大问题
int main() { SmartPtr<int> sp1(new int); // 用 sp1 拷贝构造 sp2 SmartPtr<int> sp2 = sp1;
return 0;}这样的代码执行之后:

会发现, 同一块空间被释放了两次, 直接报错
出现这种情况的原因是, 浅拷贝让两个对象的成员变量指向了同一块空间, 在两个对象生命周期结束时, 都会调用析构函数 但会尝试delete同一块空间
这就是 智能指针的实现存在的最大的问题, 需要实现浅拷贝, 但是浅拷贝就会出现重复delete
为了解决这个问题, C++诞生了许多版本的智能指针
auto_ptr
为了解决 同一块空间会被delete两次的问题, C++98 实现的智能指针auto_ptr用了一个”神”操作
auto_ptr在执行拷贝构造或者赋值重载时, 会转移资源的管理权
什么意思呢?
就是, 会 先把传入的对象的资源给新对象, 然后再把传入对象的指向资源的指针置空
auto_ptr的模拟代码就是这样的:
namespace July { template <class T> class auto_ptr { public: auto_ptr(T* ptr = nullptr) : _ptr(ptr) {}
auto_ptr(auto_ptr<T>& sp) : _ptr(sp._ptr) { sp._ptr = nullptr; }
auto_ptr<T>& operator=(auto_ptr<T>& sp) { // 防止自我赋值 if (this != &sp) { // 先清除被赋值对象中的资源 if (_ptr) delete _ptr;
_ptr = sp._ptr; sp._ptr = nullptr; } }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
~auto_ptr() { if (_ptr) { cout << "析构 释放资源" << _ptr << endl; delete _ptr; } }
private: T* _ptr; };} // namespace July这种方法 确实解决了 同一块空间会被delete两次的问题(因为其中一个对象的资源变成了 nullptr, delete nullptr 什么都不做)
但是, 却又出现了一个更离谱的问题:
auto_ptr执行过拷贝构造或赋值重载之后, 等号右边的对象, 就不能再使用了
int main() { July::auto_ptr<int> ap1(new int); *ap1 = 10; cout << "ap1:: " << *ap1 << endl;
July::auto_ptr<int> ap2(ap1); cout << "ap1:: " << *ap1 << endl; cout << "ap2:: " << *ap2 << endl;
return 0;}使用上面的类, 执行这段代码:

会报出段错误. 原因是: 通过ap1拷贝构造ap2, ap1的资源会被置空, 所以无法再被访问
使用库中的auto_ptr也是相同的效果:
#include <memory>
int main() { std::auto_ptr<int> ap1(new int); *ap1 = 10; cout << "ap1:: " << *ap1 << endl;
std::auto_ptr<int> ap2(ap1); cout << "ap1:: " << *ap1 << endl; cout << "ap2:: " << *ap2 << endl;
return 0;}
使用库中的auto_ptr, 编译时 编译器甚至会报出警告, 提示此类已弃用
把后面的 cout << "ap1:: " << *ap1 << endl; 注释掉, 就可以执行了:

成功的用ap1拷贝构造了ap2, 然后成功的解决了同一块空间释放两次的问题, 但是 也成功的添加了一个更离谱的问题
由于 auto_ptr 执行拷贝构造或赋值重载之后, 旧对象就无法正常使用了, 所以这是一个失败的设计
auto_ptr解决 同一块空间释放两次 的方法是 转移对资源的管理权这会导致, 旧对象无法再访问资源, 所以挺没用的
这个智能指针是 C++98 就存在的, 但是太难用
所以 C++11又设计了三个相对好用一些的智能指针
unique_ptr
unique_ptr 是 C++11 设计的智能指针之一
基本的用法不必多说
unique_ptr解决 同一块空间会被delete两次的问题 的方法, 更加简单粗暴: 禁止拷贝构造和赋值
没错, unique_ptr直接禁止了拷贝构造和赋值的行为
namespace July { template <class T> class unique_ptr { public: // RAII思想 unique_ptr(T* ptr = nullptr) : _ptr(ptr) {}
~unique_ptr() { if (_ptr) { cout << "delete" << _ptr << endl; delete _ptr; _ptr = nullptr; } }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
// C++11 之后 unique_ptr(const unique_ptr<T>& up) = delete; unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
private: // C++98 可以这样实现 // 1. 只声明不实现, 不过如果是public, 这种方法可以在其他地方实现功能 // 2. 声明成私有 // unique_ptr(const unique_ptr<T>& sp); // unique_ptr<T>& operator=(const unique_ptr<T>& sp); T* _ptr; };} // namespace July使用此类, 如果尝试拷贝构造或者赋值, 是无法编译通过的:
int main(){ July::unique_ptr<int> up1(new int); *up1 = 20; cout << "up1:: " << *up1 << endl;
// 尝试拷贝构造 July::unique_ptr<int> up2(up1);
return 0;}编译时, 编译器会直接报错:

使用库中的, 也会有相同的效果:

不过 库中的实现要复杂的多
shared_ptr **
上面介绍的两个智能指针, 都没有办法相对完美的解决 同一块资源被delete两次的问题(一个转移资源、一个禁止拷贝)
不过, C++11 还有一个智能指针 可以很好的解决这个问题
那就是 shared_ptr
这个智能指针的解决方法是:
在类中添加一个成员变量 用来引用计数, 每有一个对象指向同一个资源空间, 这个计数就增加 1, 否则则减少 1; 只有当计数为 0 是, 才会释放资源空间
但是, 这个用以引用计数的成员变量, 不能直接用static修饰的变量
如果使用 static修饰的变量作为引用计数变量, 那么这个变量会被所有shared_ptr对象共享, 无论是否维护同一块资源空间
但是, 我们要实现的是 维护同一块资源空间的对象之间共享一个引用计数
这要怎么实现呢? 其实很简单, 如何实现多个对象维护同一块资源空间 就如何实现多个对象共享引用计数
shared_ptr模拟:
namespace July { template <class T> class shared_ptr { public: // RAII思想 shared_ptr(T* ptr) : _ptr(ptr) , _pCount(new int(1)) {}
~shared_ptr() { release(); }
shared_ptr(const shared_ptr<T>& sp) : _ptr(sp._ptr) , _pCount(sp._pCount) { (*_pCount)++; }
shared_ptr<T>& operator=(const shared_ptr<T>& sp) { // 这里需要用 维护资源是否相同 来判断 是否需要执行赋值操作 // 不能只判断对象是否相同, 因为不同对象维护的资源也可能相同 if (_ptr != sp._ptr) { release();
_ptr = sp._ptr; _pCount = sp._pCount; ++(*_pCount); }
return *this; }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
// 释放资源函数 // 当前对象的引用计数为1(再--为0)时, 才真正进行资源释放操作 void release() { if (--(*_pCount) == 0 && _ptr) { cout << "delete" << _ptr << endl; delete _ptr; _ptr = nullptr;
delete _pCount; _pCount = nullptr; } } int getCount() { return *_pCount; }
private: T* _ptr; int* _pCount; };} // namespace July其中, 最重要的四个部分:
-
成员变量
int* _pCount要实现多个对象之间共享一个引用计数, 就需要让多个维护同一资源的对象能够同时访问这个变量
那其实 资源共享是怎么是实现的, 引用计数一样的方法实现 就可以了
所以, 使用一个
int*指针变量, 在实例化对象时初始化为new int(1), 就可以让 此指针指向一块堆空间这样, 就可以让其他对象的
int*指针变量指向同一块空间 -
析构函数
由于可能存在多个对象正在共同维护同一块资源的情况
所以 需要在当前引用计数为1(再减就为0)时 调用析构函数, 才进行资源的释放
否则就只需要将引用计数
--, 就可以了所以实现了一个,
release()函数:// 释放资源函数// 当前对象的引用计数为1(再--为0)时, 才真正进行资源释放操作void release() {// 先 --(*_pCount), 然后判断结果是否为0// 如果为0, 且维护资源不为空, 则进行资源释放// 需要释放 资源空间 和 引用计数空间if (--(*_pCount) == 0 && _ptr) {cout << "delete" << _ptr << endl;delete _ptr;_ptr = nullptr;delete _pCount;_pCount = nullptr;}}在析构函数内, 直接调用此函数就可以了
-
拷贝构造
此类的拷贝构造的实现 非常简单
只需要根据传入的对象所 维护的资源和引用计数, 初始化新对象
然后 引用计数
++就可以了shared_ptr(const shared_ptr<T>& sp): _ptr(sp._ptr), _pCount(sp._pCount) {(*_pCount)++;} -
赋值重载
赋值重载的实现 是最复杂的
首先, 需要判断 两个对象维护的资源是否为同一资源:
如果是, 则不执行赋值操作; 如果不是, 再执行
然后要执行赋值操作, 就需要知道两个对象相互赋值会发生什么: 比如
sp1 = sp2可能会发生什么呢?-
sp1将会失去原先维护的资源如果
sp1原来的引用计数为 1, 那么就需要将sp1维护的资源空间释放掉所以, 需要先执行
release(), 判断是否需要释放资源 -
sp2维护的资源和引用计数, 将会多一个维护者sp1所以 需要将
sp2维护的资源 和 引用计数, 赋值给sp1
shared_ptr<T>& operator=(const shared_ptr<T>& sp) {// 这里需要用 维护资源是否相同来判断 是否需要执行赋值操作// 不能只判断对象是否相同, 因为不同对象维护的资源也可能相同if (_ptr != sp._ptr) {release();_ptr = sp._ptr;_pCount = sp._pCount;++(*_pCount);}return *this;} -
此时模拟的shared_ptr, 就是一个好用的智能指针:
int main() { July::shared_ptr<int> sp1(new int(10)); cout << "构造 sp1, sp1:: " << *sp1 << ", pCount:: " << sp1.getCount() << endl; July::shared_ptr<int> sp2(sp1); cout << "由sp1拷贝构造 sp2, sp2:: " << *sp2 << ", pCount:: " << sp2.getCount() << endl; July::shared_ptr<int> sp3(sp1); cout << "由sp1拷贝构造 sp3, sp3:: " << *sp3 << ", pCount:: " << sp3.getCount() << endl << endl;
cout << "更改 *sp2 = 20" << endl; *sp2 = 20; cout << "sp1:: " << *sp1 << ", pCount:: " << sp1.getCount() << endl; cout << "sp2:: " << *sp2 << ", pCount:: " << sp2.getCount() << endl; cout << "sp3:: " << *sp3 << ", pCount:: " << sp3.getCount() << endl << endl;
July::shared_ptr<int> sp4(new int(44)); cout << "构造 sp4, sp4:: " << *sp4 << ", pCount:: " << sp4.getCount() << endl << endl;
sp2 = sp4; cout << "sp4赋值给sp2, sp2:: " << *sp2 << ", pCount:: " << sp2.getCount() << endl << endl; cout << "sp1:: " << *sp1 << ", pCount:: " << sp1.getCount() << endl << endl;
sp4 = sp2; cout << "sp2赋值给sp4, sp4:: " << *sp4 << ", pCount:: " << sp4.getCount() << endl << endl;
return 0;}这段代码的执行结果是:

将 shared_ptr换成标准库中的(还需要将 调用的getCount()改为use_count()), 依旧是这样:

shared_ptr的循环引用 与 weak_ptr
虽然shared_ptr已经很好用了
但是, 使用不当的话还会出现一种后果很严重的错误:
struct ListNode { int _data; July::shared_ptr<ListNode> _prev = nullptr; July::shared_ptr<ListNode> _next = nullptr;
~ListNode() { cout << "~ListNode()" << endl; }};
int main() { July::shared_ptr<ListNode> node1(new ListNode); July::shared_ptr<ListNode> node2(new ListNode);
cout << node1.getCount() << endl; cout << node2.getCount() << endl;
node1->_next = node2; node2->_prev = node1;
cout << node1.getCount() << endl; cout << node2.getCount() << endl;
return 0;}使用shared_ptr管理ListNode结构体对象
而ListNode内部 指向前节点和后节点的成员变量, 同样是shared_ptr<ListNode>
那么, 上面的这段代码 执行结果是什么?

首先创建了两个节点. 然后分别指向对方, 就像这样:

node1维护自己的空间资源, 然后node2->_prev也维护同一空间, 所以node1.getCount()变为2
node2维护自己的空间资源, 然后node1->_next也维护同一空间, 所以node2.getCount()变为2
看起来很正常, 但是 空间释放正常吗?
是实现的shared_ptr的析构函数中, 如果释放了资源 是会输出delete+地址的
再加一句话演示一下:
struct ListNode { int _data; July::shared_ptr<ListNode> _prev = nullptr; July::shared_ptr<ListNode> _next = nullptr;
~ListNode() { cout << "~ListNode()" << endl; }};
int main() { July::shared_ptr<ListNode> node1(new ListNode); July::shared_ptr<ListNode> node2(new ListNode);
cout << node1.getCount() << endl; cout << node2.getCount() << endl;
node1->_next = node2; node2->_prev = node1;
cout << node1.getCount() << endl; cout << node2.getCount() << endl;
July::shared_ptr<int> sp1(new int(10)); cout << "构造 sp1, sp1:: " << *sp1 << ", pCount:: " << sp1.getCount() << endl;
return 0;}
sp1的空间释放了, 但是**node1和node2的空间并没有释放!**
用 VS 调试看看:

可以看到, main 函数执行完return 0;之后, sp1的引用计数从1->0, 资源被销毁
但是, node1和node2的引用计数 却是从2->1, 而且没有销毁资源
这是为什么?
来分析一下:
-
首先 用
shared_ptr构造维护了两个节点:node1和node2此时, 两块资源空间, 只有各自维护,
_pCount为 1 -
在
node1和node2被构造出来的时候, 其各自有两个成员变量_next和_prev也被构造了出来 -
然后, 将
node2赋值给了node1->_next此时,
node2的资源空间, 就有两个shared_ptr对象在维护:node2和node1->_next_pCount变为2 -
将
node1赋值给了node2->_prev此时,
node1的资源空间, 也就有两个shared_ptr对象在维护:node1和node2->_next_pCount变为2 -
然后, 直到执行
return 0;按期望来说,
main函数结束node1和node2对象生命周期结束, 应该调用析构函数 将维护的资源释放掉很明显
node1和node2析构函数是调用了, 但是资源并没有释放掉因为 在
node1和node2调用析构函数时, 两块资源空间的引用计数都是2, 是不会释放资源的, 只会将对应的引用计数--, 然后将对象摧毁掉也就是说,
node1和node2对象被摧毁, 但由于原node2的_prev还在维护原node1的资源空间,原node1的_next还在维护原node2的资源空间, 导致两块资源空间没有释放变化如下:

如果将
原node1标号为1号空间,原node2标号为2号空间node1和node2对象被销毁之后, 资源的维护状态就变成了: 1号空间内有对象在维护 2号空间, 2号空间内有对象在维护 1号空间那么 这两块空间就没办法释放掉了
因为, 如果要释放1号空间资源, 就需要调用维护着它的2号空间里的
_prev对象 的析构函数析构函数是在对象生命周期结束时自动调用的, 想要2号空间里的
_prev对象生命周期结束 就需要 释放2号空间而 如果要 释放2号空间资源, 就需要调用维护着它的1号空间里的
_next对象 的析构函数这就成了一个 循环, 然后 释放空间的条件永远无法满足, 空间就无法释放, 也无法访问, 这又是一种内存泄漏
这种情况被称为,
shared_ptr的循环引用, 即 两块空间内都存在shared_ptr对象 维护着对方空间, 导致释放空间的条件无法满足, 出现内存泄漏
上面的这种情况, 单使用 shared_ptr是无法解决的
shared_ptr的循环引用问题, 需要通过weak_ptr这个智能指针来解决
weak_ptr
weak_ptr是一个特殊的智能指针, 它是**辅助shared_ptr**使用的
它可以指向shared_ptr指向的对象. 但是, 它只能访问、管理数据, 不能管理这块资源空间
即 不参与资源的释放, 也不会使资源空间的引用计数增加
它就像一个指向shared_ptr指向的对象的普通指针
并且, weak_ptr只能访问已经存在的对象, 实例化新对象需要使用shared_ptr
所以, 在上述例子中, 我们只需要将 ListNode 节点结构体中的_prev和_next改用weak_ptr管理数据就可以了:
namespace July { template <class T> class shared_ptr { public: // RAII思想 shared_ptr(T* ptr) : _ptr(ptr) , _pCount(new int(1)) {}
~shared_ptr() { release(); }
shared_ptr(const shared_ptr<T>& sp) : _ptr(sp._ptr) , _pCount(sp._pCount) { (*_pCount)++; }
shared_ptr<T>& operator=(const shared_ptr<T>& sp) { // 这里需要用 维护资源是否相同来判断 是否需要执行赋值操作 // 不能只判断对象是否相同, 因为不同对象维护的资源也可能相同 if (_ptr != sp._ptr) { release();
_ptr = sp._ptr; _pCount = sp._pCount; ++(*_pCount); } else { // 为了演示 cout << "自我赋值, 跳过执行" << endl; }
return *this; }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
// 释放资源函数 // 当前对象的引用计数为1时, 进行资源释放操作 void release() { if (--(*_pCount) == 0 && _ptr) { cout << "delete" << _ptr << endl; delete _ptr; _ptr = nullptr;
delete _pCount; _pCount = nullptr; } }
T* get() const { return _ptr; }
int getCount() { return *_pCount; }
private: T* _ptr; int* _pCount; };
// 简化版本的weak_ptr实现 template <class T> class weak_ptr { public: weak_ptr() : _ptr(nullptr) {}
weak_ptr(const shared_ptr<T>& sp) : _ptr(sp.get()) {}
weak_ptr<T>& operator=(const shared_ptr<T>& sp) { _ptr = sp.get(); return *this; }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private: T* _ptr; };} // namespace July
struct ListNode { int _data; July::weak_ptr<ListNode> _prev; July::weak_ptr<ListNode> _next;
~ListNode() { cout << "~ListNode()" << endl; }};
int main() { July::shared_ptr<ListNode> node1(new ListNode); July::shared_ptr<ListNode> node2(new ListNode);
cout << node1.getCount() << endl; cout << node2.getCount() << endl;
node1->_next = node2; node2->_prev = node1;
cout << node1.getCount() << endl; cout << node2.getCount() << endl;
return 0;}修改了, ListNode 之后, 这段代码的运行结果是:

即使 执行了node1->_next = node2; node2->_prev = node1; node1 和 node2的引用计数也不会发生变化
也就不会影响资源的释放
但, 使用weak_ptr需要注意, 资源的声明周期, 因为weak_ptr并不管理资源, 所以在使用上更贴近原始指针一些, 因为不能保证所指向资源是否有效
智能指针的定制删除器
本文前面介绍智能指针时, 智能指针维护的资源 全部都是new出来的
但是, C++除了new之外 还有new[]
而new[]开辟的资源 需要使用delete[]释放
C++是兼容C语言的, 还可以malloc空间
这就导致了, 使用智能指针释放资源的方式并不是固定的, 并且开辟空间存储的资源也是不固定的
智能指针 针对标准内置类型数据, 有一个很好的释放支持
但如果是自定义类型呢?
class Date {public: ~Date() { cout << "~Date" << endl; }
private: int _year = 1; int _month = 1; int _day = 1;};
int main() { std::shared_ptr<Date> sp1(new Date); std::shared_ptr<Date> sp2(new Date[10]);
return 0;}
执行后, 输出的第一行是 ~Date 说明 new 出来的正常释放了
但是, new[]出来的数据, 没法释放 进程直接崩溃了
那么, 怎么才能让智能指针 支持对各种类型的数据, 各种开辟的方法实现正常的释放呢?
这就需要用到 定制删除器的概念了:


查看文档中 unique_ptr的模板 和 shared_ptr的构造函数
unique_ptr的模板中, 实际有两个模板参数, 一个是资源类型, 另一个 则是仿函数-定制删除器 用来指定资源的释放方式
shared_ptr的构造函数中, 也有一个可以传入del的重载, 这也是需要传入定制删除器的
定制删除器实际 就是释放资源方式的仿函数.
还是以上面的代码为例, 我们在 构造智能指针时, 传入一个lambda表达式:
int main() { std::shared_ptr<Date> sp1(new Date); std::shared_ptr<Date> sp2(new Date[10], [](Date* d){ delete [] d; });
return 0;}这段代码的执行结果:

很顺利的释放了所有资源
C++11 智能指针 和 boost 智能指针的关系
其实这两个东西之间的关系非常容易解释
我们每个人肯定或多或少都玩过网络游戏
他们之间的关系 就好像 游戏的体验服 和 正式服之间的关系
Boost库是一个非常强大且广泛使用的C++库集合, 它包含了一系列为C++开发提供便利的库, 旨在扩展 C++标准库的功能
Boost 库的开发遵循严格的编程规范和高质量标准, 因此其代码质量和性能得到了广泛认可
许多Boost库的作者本身就是C++标准委员会成员, 因此, Boost”天然”成了标准库的后备, 负责向新标准输送组件, 这也使得 Boost获得了”准”标准库的美誉
而 C++11 中的三个智能指针 unique_ptr shared_ptr weak_ptr 就是从 Boost 库中优化过来的
-
C++ 98 中产生了第一个智能指针
auto_ptr, 这个智能指针太离谱 -
C++ Boost 给出了更实用的
scoped_ptr和shared_ptr和weak_ptr -
C++ TR1, 引入了
shared_ptr等, 不过注意的是 TR1 并不是标准版 -
C++ 11, 引入了
unique_ptr和shared_ptr和weak_ptr
需要注意的是unique_ptr对应的就是 Boost 库中的scoped_ptr, 改了个名字
并且这些智能指针的实现原理是参考 Boost 中的实现的