C++ 智能指针
手动new的缺陷
我们如果自己new堆内存,需要我们自己在合适的地方释放内存,如果忘记了就会导致内存泄露。这在大型项目里面是很严重的问题,如果是在不常调用的地方内存泄露,那么可能会在服务器启动的后几周几月突然崩溃,如果是在常调用的地方,可能几天几周就奔溃,会带来很大的经济损失。因此在公司不要写出内存泄露的代码。
如果你确实有很强的delete意识,但是在一些复杂的情况下,再很强也会有疏忽的,例如在抛异常的情况下。你先new了n个对象,然后中途还没delete就抛异常了,那么你得在抛异常里面释放内存,如果这个异常提前抛到其他地方了,还得另外算。所以有没有什么方式让内存交给系统管理。
析构函数管理资源释放
因此想到了了析构函数的特点,析构函数在当前类生命周期结束后会自动执行其析构函数,如果我们创建一个类让其管理我们创建的指针,那么在当前作用域结束后就会自动释放内存了
#include<iostream> using namespace std; template<class T> class autoptr { using Ptr = T*; using Ref = T&; public: autoptr(T*ptr) :_ptr(ptr) {} ~autoptr() { if(_ptr){ cout << "指针内存释放" << endl; delete _ptr; } } private: Ptr _ptr; }; int main() { { autoptr<int>ptr(new int); } cout << "作用域结束" << endl; return 0; }效果不错,我们还可以给其加上指针的操作,这样我们可以把类当成指针操作。
template<class T> class autoptr { using Ptr = T*; using Ref = T&; public: autoptr(T*ptr=nullptr) :_ptr(ptr) {} ~autoptr() { if (_ptr) { cout << "指针内存释放" << endl; delete _ptr; } } Ref operator*()const { if (!_ptr)throw std::runtime_error("空指针"); return *_ptr; } Ptr operator->()const noexcept { return _ptr; } operator bool()const{ return _ptr != nullptr; } private: Ptr _ptr=nullptr; };测试一波:

非常优雅
作用域问题
随之而来问题,我如果在当前作用域外还要用咋办?
当前创建的类是没有救了,它无法出作用域,因此指针窜在它手里必定会被析构,因此我们需要让其他对象来传承我们的底层指针。
那么我们顺便带来官方库对智能指针的实现
因此有了第一种方法,也是std的auto_ptr
auto_ptr
它的方式是,当实现拷贝赋值时,会将底层的指针交给对方,自己置空
template<class T> class autoPtr { using Ptr = T*; using Ref = T&; public: explicit autoPtr(T*ptr=nullptr) :_ptr(ptr) {} ~autoPtr() noexcept { if (_ptr) { cout << "指针内存释放" << endl; delete _ptr; } } autoPtr(autoPtr& x) { _ptr = x._ptr; x._ptr = nullptr; } autoPtr& operator=(autoPtr& x) noexcept { if (&x != this) { _ptr = x._ptr; x._ptr = nullptr; } return *this; } Ref operator*()const { if (!_ptr)throw std::runtime_error("空指针"); return *_ptr; } Ptr operator->()const noexcept { return _ptr; } explicit operator bool()const noexcept { return _ptr != nullptr; } private: Ptr _ptr=nullptr; }; 测试一下
int main() { autoPtr<pair<int, string>>p; { autoPtr<pair<int,string>>ptr(new pair<int,string>); *ptr = { 1,"nihao" }; cout << ptr->second << endl; p = ptr; try { *ptr; } catch (const std::runtime_error& x) { cout << x.what() << endl; } } cout << "作用域结束" << endl; cout << p->second << endl; return 0; }
unique_ptr
某些情况我们只希望当前的堆内存只被一个对象所管控,因此有了unique_ptr。
template<class T> class uniquePtr { using Ptr = T*; using Ref = T&; public: explicit uniquePtr(T* ptr = nullptr) :_ptr(ptr) { } ~uniquePtr() noexcept { if (_ptr) { cout << "指针内存释放" << endl; delete _ptr; } } uniquePtr(uniquePtr&& x)noexcept { if (this != &x) { swap(x); } } uniquePtr(const uniquePtr&) noexcept = delete; uniquePtr& operator=(const uniquePtr& ) noexcept = delete; uniquePtr& operator=(uniquePtr&& x) noexcept { if (this != &x) { swap(x); } return *this; } Ref operator*()const { if (!_ptr)throw std::runtime_error("空指针"); return *_ptr; } Ptr operator->()const noexcept { return _ptr; } explicit operator bool()const noexcept { return _ptr != nullptr; } Ptr get()const noexcept { return _ptr; } Ptr release()noexcept {//主动放弃对指针的管理 Ptr ret = _ptr; _ptr = nullptr; return ret; } void reset(Ptr ptr = nullptr)noexcept {//释放旧内存,管理新内存 if (_ptr)delete _ptr; _ptr = ptr; } void swap(uniquePtr& x)noexcept { std::swap(_ptr, x._ptr); } private: Ptr _ptr = nullptr; };shared_ptr
auto_ptr这种剥夺式的赋值不被采用,会导致之前的指针被悬空,很容易出问题,而且同一块地址是可以被很多指针存储的,这种剥夺式的方式并不符合开发的需求,因此auto_ptr在C++11被禁止,在C++17彻底被删除
因此有了后来的shared_ptr,shared_ptr在进行拷贝的时候并不会转移指针,而是拷贝底层的地址。并通过一个计数来判断类析构时是否delete调我们的内存
template<class T> class sharedPtr { using Ptr = T*; using Ref = T&; public: explicit sharedPtr(T* ptr = nullptr) :_ptr(ptr) ,_pcount(ptr?new size_t(1):nullptr) { } ~sharedPtr() noexcept { if (_ptr && !(--*_pcount)) { cout << "指针内存释放" << endl; delete _ptr; delete _pcount; } } sharedPtr(sharedPtr&& x)noexcept { if (this != &x) { swap(x); } } sharedPtr(const sharedPtr& x) noexcept { _ptr = x._ptr; _pcount = x._pcount; if(_ptr)++*_pcount; } sharedPtr& operator=(const sharedPtr& x) noexcept { if (&x != this) { sharedPtr temp(x); swap(temp); } return *this; } sharedPtr& operator=(sharedPtr&& x) noexcept { if (this != &x) { swap(x); } return *this; } Ref operator*()const { if (!_ptr)throw std::runtime_error("空指针"); return *_ptr; } Ptr operator->()const noexcept { return _ptr; } explicit operator bool()const noexcept { return _ptr != nullptr; } Ptr get()const noexcept { return _ptr; } size_t use_count()const noexcept { if (!_ptr)return 0; return *_pcount; } private: void swap(sharedPtr& x)noexcept { std::swap(_ptr, x._ptr); std::swap(_pcount, x._pcount); } Ptr _ptr = nullptr; size_t* _pcount = nullptr; };多线程问题
如果多个线程都在用当前指针,那么count必须是原子操作或者加锁,这里采用原子操作
#include<atomic> template<class T> class sharedPtr { using Ptr = T*; using Ref = T&; public: explicit sharedPtr(T* ptr = nullptr) :_ptr(ptr) ,_pcount(ptr?new std::atomic<size_t>(1):nullptr) { } ~sharedPtr() noexcept { if (_ptr && !(--*_pcount)) { cout << "指针内存释放" << endl; delete _ptr; delete _pcount; } } sharedPtr(sharedPtr&& x)noexcept { if (this != &x) { swap(x); } } sharedPtr(const sharedPtr& x) noexcept { _ptr = x._ptr; _pcount = x._pcount; if(_ptr)++*_pcount; } sharedPtr& operator=(const sharedPtr& x) noexcept { if (&x != this) { sharedPtr temp(x); swap(temp); } return *this; } sharedPtr& operator=(sharedPtr&& x) noexcept { if (this != &x) { swap(x); } return *this; } Ref operator*()const { if (!_ptr)throw std::runtime_error("空指针"); return *_ptr; } Ptr operator->()const noexcept { return _ptr; } explicit operator bool()const noexcept { return _ptr != nullptr; } Ptr get()const noexcept { return _ptr; } size_t use_count()const noexcept { if (!_ptr)return 0; return *_pcount; } private: void swap(sharedPtr& x)noexcept { std::swap(_ptr, x._ptr); std::swap(_pcount, x._pcount); } Ptr _ptr = nullptr; std::atomic<size_t>* _pcount = nullptr; }; weak_ptr
我们看看以下的情况
template<class T> struct listNode { listNode(const T&val) :_val(val) {} sharedPtr<listNode<T>>_l, _r; T _val; }; int main() { sharedPtr<listNode<int>>ln1(new listNode(1)), ln2(new listNode(1)); ln1->_r = ln2; ln2->_l = ln1; return 0; }
最后程序退出的时候
ln1和ln2是在main函数作用域的,因此会释放析构。最后会变成以下情况

产生的原因是listNode是通过new出来的,其内存处在堆区。导致两个内存都不会自己释放,这就导致两个处在堆区的shared_ptr不会count--,最终导致内存泄露。
本质原因是堆内存的shared_ptr的count计数导致内存泄露。
那么我们可以实现一个weak_ptr指针,只存指针不做引用计数
template<class T> class weakPtr { using Ptr = T*; using Ref = T&; public: explicit weakPtr(const sharedPtr<T>&sp=sharedPtr<T>()) :_ptr(sp.get()) { } ~weakPtr() noexcept { } weakPtr(weakPtr&& x)noexcept { if (this != &x) { swap(x); } } weakPtr(const weakPtr& x) noexcept { _ptr = x._ptr; } weakPtr& operator=(const sharedPtr<T>& x) noexcept { weakPtr temp(x); swap(temp); return *this; } weakPtr& operator=(const weakPtr& x) noexcept { if (&x != this) { weakPtr temp(x); swap(temp); } return *this; } weakPtr& operator=(weakPtr&& x) noexcept { if (this != &x) { swap(x); } return *this; } Ref operator*()const { if (!_ptr)throw std::runtime_error("空指针"); return *_ptr; } Ptr operator->()const noexcept { return _ptr; } explicit operator bool()const noexcept { return _ptr != nullptr; } Ptr get()const noexcept { return _ptr; } private: void swap(weakPtr& x)noexcept { std::swap(_ptr, x._ptr); } Ptr _ptr = nullptr; };这样就可以解决循环引用的问题了
删除器
目前我们的智能指针智能对单个对象进行内存管理,如果new的是数组就会出现问题。new []和最后 delete将会导致未定义行为。因此我们需要加一个删除器
删除器使用
//仿函数方式 struct del { void operator()(int* p) { cout << "仿函数删除器 " << endl; delete p; } }; std::shared_ptr<int>s(new int,del()); std::unique_ptr<int, del>u(new int); //lambda表达式方式 auto del_ = [](int* p) {cout << "lamba 删除器" << endl; delete p; }; std::shared_ptr<int>ls(new int, del_); std::unique_ptr<int, decltype(del_)>lu(new int, del_);对于shared_ptr,这两种传参是差不多的,但是对于unique_ptr就不一样,unique_ptr需要传删除器变量类型,因为lambda表达式没有默认构造,因此我们需要显示传删除器对象
强引用 弱引用
但是上述所说的shared_ptr和weak_ptr并不是工程实现的版本,还存在一定的问题。那就是weak_ptr不知道存在的指针是否还在,如果直接访问了释放的内存,那么就会抛异常出错。因此有了以下的实现方式:
我让weak_ptr和shared_ptr都持有引用,但是只有shared_ptr可以修改引用,weak_ptr不能修改只能读。我weak_ptr要读取指针的时候,我先访问引用看是否为零,再去访问底层的ptr。
看似上面可以,但是实际是不行的,因为引用也是一个堆内存,当ptr释放后,引用的堆内存会跟着释放,因此我们需要让这个内存不要和ptr一起释放。因此我们有了弱引用。我们创建一个计数器,里面同时存储弱引用和强引用,通过弱引用记录有多少个weak_ptr还在观察计数器,计数器当且仅当强引用和弱引用都为0的时候才会释放。这样就避免了weak_ptr在观察的时候计数器被释放了的问题。
具体代码我就不实现了。