目录
智能指针出现的原因
在上一篇异常的介绍中就提到了,如果申请了一块空间,因为抛异常导致资源没有释放,这就会导致内存泄漏。
内存泄漏就是因为疏忽或错误造成程序没有释放不再使用的内存。这并不是说内存真的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
上一段简单的说明就是,内存泄漏就是指针丢失了,但是空间是丢失不了的,造成空间浪费。
对于短期运行的程序出现内存泄漏,没有什么影响,进程结束,内存就释放了;对于长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
避免内存泄漏
- 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。
- 采用RAII思想或者智能指针来管理资源。这就是接下来要说的。
- 出问题了使用内存泄漏工具检测,检测工具的原理就是申请空间就用一个容器记录下来,释空间的时候就从容器中删除,这样就可以检查哪些是内存泄漏的资源。
智能指针的使用及原理
RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:一是不需要显示地释放资源;二是对象所需的资源在其生命周期内始终有效。
template<class T> class SmartPtr { SmartPtr(T* ptr = nullptr) // 使用指针构造一个对象 :_ptr(ptr) {} ~SmartPtr() { if (_ptr) delete _ptr; // 在对象生命周期结束后自动调用析构函数释放资源 } private: T* _ptr; };
智能指针的原理
上面的代码还不能算是智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过 -> 去访问所指空间中的内容,因此类中还得需要重载 * 和 -> ,这样才可以像指针一样去使用。
template<class T> class SmartPtr { public: SmartPtr(T* ptr = nullptr) // 使用指针构造一个对象 :_ptr(ptr) {} ~SmartPtr() { if (_ptr) delete _ptr; // 在对象生命周期结束后自动调用析构函数释放资源 } T& operator&() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; };
auto_ptr
C++98版本的库中就提供了auto_ptr的智能指针。
auto_ptr的实现原理:管理权转移的思想。
假如这里有一个A类,把它托管给auto_ptr管理,当我要赋值的时候,就像迭代器一样,我不需要让他深拷贝,他是一个指针,负责指向就好了,如果是浅拷贝,赋值之后两个对象指向同一块空间,析构就会析构两次,为了解决这个问题,auto_ptr会把原来对象的权限转移给新的对象,但是要是使用原来的对象就会出错,所以它使用的是管理权转移,说白了也就是把旧的指针赋值过去,再置为空,但是这种做法很不负责,平常是不用的。
unique_ptr
因为auto_ptr不是那么好用,C++11中就有了unique_ptr。
unique_ptr的实现原理:简单粗暴的防拷贝。下面就是简单实现一下。
template<class T> class unique_ptr { public: unique_ptr(T* ptr = nullptr) :_ptr(ptr) {} // 防止拷贝 C++11的方法 unique_ptr(unique_ptr<T>& up) = delete; unique_ptr<T>& operator=(unique_ptr<T>& up) = delete; // C++98使用的是私有拷贝构造、赋值,只声明不实现 ~unique_ptr() { if (_ptr) delete _ptr; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; };
但是这样简单的防止拷贝也不是办法,它只适用于不需要拷贝的。
shared_ptr
如果就是想要拷贝呢?C++11中开始提供更靠谱的并且支持拷贝的shared_ptr。
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。有几个资源指向这块空间就是几,释放的时候,如果不是最后一个,引用计数就 - - ,如果是最后一个再释放 。
template<class T> class shared_ptr { public: shared_ptr(T* ptr = nullptr) :_ptr(ptr) , _pCount(new int(1)) // 初始化为1 {} ~shared_ptr() { if (--(*_pCount) == 0) { delete _ptr; delete _pCount; } } // 拷贝构造 shared_ptr(const shared_ptr<T>& sp) :_ptr(sp._ptr) , _pCount(sp._pCount) { (*_pCount)++; } // 赋值 shared_ptr& operator=(const shared_ptr<T>& sp) { if (sp._ptr != _ptr) // 不需要自己给自己赋值,也不需要给指向同一块的赋值 { if (--(*_pCount) == 0) // 原来的是不是最后一个,是的话就释放 { delete _ptr; delete _pCount; } _ptr = sp._ptr; _pCount = sp._pCount; (*_pCount)++; } return *this; } T* get() { return _ptr; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; int* _pCount; // 引用计数 };
shared_ptr看似很完美,但是它还有一个问题,那就是循环引用,接下来我们就来看看什么是循环引用。
struct Node { int _data; std::shared_ptr<Node> _prev; std::shared_ptr<Node> _next; ~Node() { cout << "~Node()" << endl; } }; int main() { // 这里不能使用=赋值,因为shared_ptr构造使用的explicit,不可以有隐式类型的转换 std::shared_ptr<Node> n1(new Node); std::shared_ptr<Node> n2(new Node); // 如果只把next修改,最后会析构两次,如果把next和prev都修改,变成循环链表最后就没有析构 n1->_next = n2; // n2->_prev = n1; return 0; }
这就是循环引用,谁也释放不了,这样就出现了weak_ptr。
weak_ptr
weak_ptr不是常规的智能指针,它没有RAII,不支持直接管理资源,weak_ptr主要用shared_ptr构造,用来解决shared_ptr循环引用问题。
next和prev是weak_ptr时,他不参与资源释放管理,也不增加引用计数,但是可以访问和修改资源,所以就不存在循环引用。
struct Node { int _data; std::weak_ptr<Node> _prev; std::weak_ptr<Node> _next; ~Node() { cout << "~Node()" << endl; } };
下面就简单实现一下weak_ptr。
// 为了解决shared_ptr循环引用问题 template<class T> class weak_ptr { public: weak_ptr() :_ptr(nullptr) {} weak_ptr(const shared_ptr<T>& sp) :_ptr(sp.get()) {} weak_ptr(const weak_ptr<T>& wp) :_ptr(wp.get()) {} weak_ptr<T>& operator=(const shared_ptr<T>& sp) { _ptr = sp.get(); return *this; } T* get() { return _ptr; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; };
shared_ptr线程安全
如果是多线程的情况下,shared_ptr是线程安全的吗,如果不处理那一定不是线程安全的。
第一,在shared_ptr拷贝的时候,引用计数是要改变的,不管是++还是--,如果不加锁是一定会出问题的。
#include <iostream> #include <vector> #include <thread> #include <mutex> using namespace std; namespace dsh { template<class T> class shared_ptr { public: shared_ptr(T* ptr = nullptr) :_ptr(ptr) , _pCount(new int(1)) // 初始化为1 , _pMutex(new mutex) // 初始化锁 {} ~shared_ptr() { Release(); } void AddPCount() { _pMutex->lock(); (*_pCount)++; _pMutex->unlock(); } void Release() { bool flag = false; // 标识是不是最后一个,是的话也要释放锁 _pMutex->lock(); if (--(*_pCount) == 0) // 原来的是不是最后一个,是的话就释放 { delete _ptr; delete _pCount; flag = true; } _pMutex->unlock(); if (false) delete _pMutex; } // 拷贝构造 shared_ptr(const shared_ptr<T>& sp) :_ptr(sp._ptr) , _pCount(sp._pCount) , _pMutex(sp._pMutex) { AddPCount(); } // 赋值 shared_ptr& operator=(const shared_ptr<T>& sp) { if (sp._ptr != _ptr) // 不需要自己给自己赋值,也不需要给指向同一块的赋值 { Release(); _ptr = sp._ptr; _pCount = sp._pCount; _pMutex = sp._pMutex; AddPCount(); } return *this; } int use_count() { return *_pCount; } T* get() { return _ptr; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; int* _pCount; // 引用计数 mutex* _pMutex; // 锁 }; } int main() { dsh::shared_ptr<int> sp1(new int(1)); dsh::shared_ptr<int> sp2(sp1); vector<thread> v(2); int n = 1000000; mutex mtx; for (auto& t : v) { t = thread([&]() { for (size_t i = 0; i < n; i++) { dsh::shared_ptr<int> sp(sp1); } }); } v[0].join(); v[1].join(); cout << sp1.use_count() << endl; cout << sp1.get() << endl; return 0; }
第二,那shared_ptr维护的指针是线程安全的吗,那肯定也不是,所以想要在多线程下访问也是要加锁的。
int main() { dsh::shared_ptr<int> sp1(new int(1)); dsh::shared_ptr<int> sp2(sp1); vector<thread> v(2); int n = 1000000; mutex mtx; for (auto& t : v) { t = thread([&]() { for (size_t i = 0; i < n; i++) { // 拷贝是线程安全的 dsh::shared_ptr<int> sp(sp1); mtx.lock(); (*sp)++; mtx.unlock(); } }); } v[0].join(); v[1].join(); cout << sp1.use_count() << endl; cout << sp1.get() << endl; cout << *sp1 << endl; return 0; }
所以换言之,STL都不是线程安全的,使用的时候也是要按需求加锁的。
定制删除器
如果一个对象是通过new[]出来的,那么用delete就会出问题,一定要使用delete[]。对于自定义类型,new[]实际是malloc和n次构造函数,它会在开空间的时候会在头的位置多开一个指针,使用这个指针存放调用了几次构造函数,等到delete[]的时候,先向前偏移到该位置,再释放资源,如果继续使用delete,指针不会偏移,释放的位置就会出错。这在vs下是这样实现的,所以使用new和delete的时候一定要匹配。
智能指针中提供了一个定值删除器,unique_ptr和shared_ptr也使用了default_ptr。
当然,它也可以是一个仿函数,使用的时候定义匿名对象就可以了。
template <class T> struct DeleteArr { void operator()(T* ptr) { delete[] ptr; } }; template <class T> struct FreeArr { void operator()(T* ptr) { free(ptr); } }; int main() { std::shared_ptr<Node> n1(new Node[5], DeleteArr<Node>()); std::shared_ptr<int> n2(new int[5], DeleteArr<int>()); std::shared_ptr<int> n3((int*)malloc(sizeof(4)), FreeArr<int>()); return 0; }
既然可以使用仿函数,那么自然可以使用lambda表达式。
int main() { std::shared_ptr<Node> n1(new Node[5], [](Node* ptr) { delete[] ptr; }); std::shared_ptr<int> n2(new int[5], [](int* ptr) { delete[] ptr; }); std::shared_ptr<int> n3((int*)malloc(sizeof(4)), [](int* ptr) { free(ptr); }); return 0; }
库中的模板参数还有一个类型,这就是删除器的类型,在创建对象的时候就可以指定是哪个删除器, 可以写一个默认的,也可以自己实现一个。这里就是简单说一下,库里的实现方式还是很麻烦的。
template <class T> // 默认的定值删除器 struct Delete { void operator()(T* ptr) { delete ptr; } }; template<class T, class D = Delete<T>> class shared_ptr { public: shared_ptr(T* ptr = nullptr) :_ptr(ptr) , _pCount(new int(1)) // 初始化为1 {} void Release() { if (--(*_pCount) == 0) { // delete _ptr; D del; del(_ptr); delete _pCount; } } ~shared_ptr() { Release(); } // 拷贝构造 shared_ptr(const shared_ptr<T>& sp) :_ptr(sp._ptr) , _pCount(sp._pCount) { (*_pCount)++; } // 赋值 shared_ptr& operator=(const shared_ptr<T>& sp) { if (sp._ptr != _ptr) // 不需要自己给自己赋值,也不需要给指向同一块的赋值 { Release(); _ptr = sp._ptr; _pCount = sp._pCount; (*_pCount)++; } return *this; } //... }
修改了一下代码,可以使用默认的删除器也可以自己写一个删除器,假如DeleteArr就是自己写的,因为它释放数组,默认的就让他直接释放指针。
int main() { shared_ptr<Node, DeleteArr<Node>> n1(new Node[5]); shared_ptr<int, DeleteArr<int>> n2(new int[5]); return 0; }