一、智能指针概念
智能指针是存储指向动态分配(堆)对象指针的类。除了能够在适当的时间自动删除指向的对象外,他们的工作机制很像C++的内置指针。智能指针在面对异常的时候格外有用,因为他们能够确保正确的销毁动态分配的对象。他们也可以用于跟踪被多用户共享的动态分配对象。
二、为什么需要智能指针
下面我们先分析一下下面这段程序有没有什么内存方面的问题?
int div() { int a, b; cin >> a >> b; if (b == 0) throw invalid_argument("除0错误"); return a / b; } void func() { int* p1 = new int[10]; // 这里可能会抛异常,此时抛异常内存会分配失败,程序结束后不会造成内存泄漏。 int* p2 = new int[10]; // 这里可能会抛异常,此时抛异常会导致p2以及后面的内存分配失败,退出程序后, //由于p1已经成功申请内存,但是C++没有内存回收机制,因此会造成内存泄漏。 int* p3 = new int[10]; // 这里可能会抛异常 try { div(); } catch (...) { delete[] p1; delete[] p2; delete[] p3; throw; } delete[] p1; delete[] p2; delete[] p3; } int main() { try { func(); } catch (const exception& e) { cout << e.what() << endl; // ... } return 0; }
关于内存泄漏见文章【C++ 内存管理】
三、智能指针设计原理剖析
RAII思想
RAII(Resource Acquisition Is Initialization),也称为 “资源获取就是初始化” ,是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。RAII是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效
利用RAII思想设计的SmartPtr类
template<class T> class SmartPtr { public: SmartPtr(T* ptr = nullptr) : _ptr(ptr) {} ~SmartPtr() { if (_ptr) 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() { SmartPtr<int> sp1(new int); SmartPtr<int> sp2(new int); cout << div() << endl; } int main() { try { Func(); } catch (const exception& e) { cout << e.what() << endl; } return 0; }
利用RAII的思想,将变量的资源交给一个对象管理,在对象的生命周期结束之时自动释放资源,巧妙地解决了内存泄漏的问题。
但是上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将 * 、-> 重载下,才可让其像指针一样去使用。
template <class T> class SmartPtr { public: // RAII思想 SmartPtr(T* ptr) :_ptr(ptr) {} ~SmartPtr() { //delete[] _ptr; delete _ptr; _ptr = nullptr; } // 像指针一样 T& operator*() { return *_ptr; } T* operator->() { return _ptr; } T* Get() { return _ptr; } private: T* _ptr; }; struct Date { int _year; int _month; int _day; Date(int year = 1, int month = 1, int day = 1) { } }; int main() { SmartPtr<int> sp1(new int); *sp1 = 10; cout << *sp1 << endl; SmartPtr<Date> sparray(new Date); // 需要注意的是这里应该是sparray.operator->()->_year = 2018; // 本来应该是sparray->->_year这里语法上为了可读性,省略了一个-> sparray->_year = 2018; sparray->_month = 1; sparray->_day = 1; }
总结一下智能指针的原理:
- RAII特性
- 重载operator*和opertaor->,具有像指针一样的行为。
四、C++标准库中的智能指针
1. std::auto_ptr
C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。
int main() { std::auto_ptr<int> sp1(new int); std::auto_ptr<int> sp2(sp1); // 管理权转移 // sp1悬空 *sp2 = 10; cout << *sp2 << endl; cout << *sp1 << endl; return 0; }
当sp2利用sp1进行拷贝构造时,sp1便被置空了,auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr。
auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份bit::auto_ptr来了解它的原理。
namespace lhf { template<class T> class auto_ptr { public: auto_ptr(T* ptr) :_ptr(ptr) {} auto_ptr(auto_ptr<T>& p) :_ptr(p._ptr) { p._ptr = nullptr; //管理权转移 } auto_ptr<T>& operator=(auto_ptr<T>& p) { if (this != &p) { if (_ptr) { delete _ptr; } //资源转移到当前对象 _ptr = p._ptr; p._ptr = nullptr; } return *this; } ~auto_ptr() { if (_ptr) { //cout << "delete" << endl; delete _ptr; } } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; }; }
2. std::unique_ptr
unique_ptr是C++11提供的。C++11库才更新了智能指针的实现,在C++11出来之前,第三方库boost已经搞好了更好用的scoped_ptr、shared_ptr、weak_ptr等智能指针,C++11将boost库中的智能指针的精华部分吸收了过来,实现了官方库的unique_ptr、shared_ptr、weak_ptr等智能指针。
unique_ptr的实现原理:简单粗暴的防拷贝。下面简化模拟实现了一份unique_ptr来了解它的原理。
namespace lhf { template<class T> class unique_ptr { public: unique_ptr(T* ptr) :_ptr(ptr) {} ~unique_ptr() { if (_ptr) { delete _ptr; } } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } unique_ptr(const unique_ptr<T>& p) = delete; unique_ptr<T>& operator=(const unique_ptr<T>& p) = delete; private: T* _ptr; }; }
int main() { lhf::unique_ptr<int> sp1(new int); lhf::unique_ptr<int> sp2(sp1); std::unique_ptr<int> sp1(new int); std::unique_ptr<int> sp2(sp1); return 0; }
3. std::shared_ptr
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源。
- shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
模拟实现shared_ptr的代码
namespace lhf { template<class T> class shared_ptr { private: void release() { if (--(*_pCount) == 0 && _ptr) { cout << "delete" << _ptr << endl; delete _ptr; _ptr = nullptr; delete _pCount; _pCount = nullptr; } } public: shared_ptr(T* ptr) :_ptr(ptr) ,_pCount(new int(1)) {} shared_ptr(const shared_ptr<T>& p) :_ptr(p._ptr) , _pCount(p._pCount) { ++(* _pCount); } shared_ptr<T>& operator=(const shared_ptr<T>& p) { if (_ptr != p._ptr) { this->release(); _ptr = p._ptr; _pCount = p._pCount; ++(*_pCount); } return *this; } ~shared_ptr() { this->release(); } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; int* _pCount; }; }
int main() { lhf::shared_ptr<int> sp1(new int); lhf::shared_ptr<int> sp2(sp1); lhf::shared_ptr<int> sp3(sp1); lhf::shared_ptr<int> sp4(new int); lhf::shared_ptr<int> sp5(sp4); //sp1 = sp1; //sp1 = sp2; sp1 = sp4; sp2 = sp4; sp3 = sp4; *sp1 = 2; *sp2 = 3; return 0; }
4. std::shared_ptr的循环引用
案例如下:
struct ListNode { int _data; shared_ptr<ListNode> _prev; shared_ptr<ListNode> _next; ~ListNode() { cout << "~ListNode()" << endl; } }; int main() { shared_ptr<ListNode> node1(new ListNode); shared_ptr<ListNode> node2(new ListNode); cout << node1.use_count() << endl; cout << node2.use_count() << endl; node1->_next = node2; node2->_prev = node1; cout << node1.use_count() << endl; cout << node2.use_count() << endl; return 0; }
循环引用分析:
- node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
- node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
- node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
- 也就是说_next析构了,node2就释放了。
- 也就是说_prev析构了,node1就释放了。
- 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。
解决方法:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr。原理就是,node1->_next = node2;和node2->_prev = node1,时weak_ptr的_next和 _prev不会增加node1和node2的引用计数。
5. std::weak_ptr
std::weak_ptr 要与 std::shared_ptr 一起使用。 一个 std::weak_ptr 对象看做是 std::shared_ptr 对象管理的资源的观察者,它不影响共享资源的生命周期。
- 如果需要使用 weak_ptr 正在观察的资源,可以将 weak_ptr 提升为 shared_ptr。
- 当 shared_ptr 管理的资源被释放时,weak_ptr 会自动变成 nullptr。
weak_ptr的模拟实现
namespace lhf { // 不参与指向资源的释放管理 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 lhf::shared_ptr<T>& p) { _ptr = p._ptr; return *this; } // 像指针一样 T& operator*() { return *_ptr; } T* operator->() { return _ptr; } public: T* _ptr; }; }
struct ListNode { /*ListNode* _next = nullptr; ListNode* _prev = nullptr;*/ std::weak_ptr<ListNode> _next; std::weak_ptr<ListNode> _prev; int _val = 0; ~ListNode() { cout << "~ListNode()" << endl; } }; int main() { std::shared_ptr<ListNode> node1(new ListNode); std::shared_ptr<ListNode> node2(new ListNode); cout << node1.use_count() << endl; cout << node2.use_count() << endl; node1->_next = node2; node2->_prev = node1; cout << node1.use_count() << endl; cout << node2.use_count() << endl; return 0; }
删除器:
如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题。
// 仿函数的删除器 template<class T> struct FreeFunc { void operator()(T* ptr) { cout << "free:" << ptr << endl; free(ptr); } }; template<class T> struct DeleteArrayFunc { void operator()(T* ptr) { cout << "delete[]" << ptr << endl; delete[] ptr; } }; int main() { FreeFunc<int> freeFunc; std::shared_ptr<int> sp1((int*)malloc(4), freeFunc); DeleteArrayFunc<int> deleteArrayFunc; std::shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc); std::shared_ptr<A> sp4(new A[10], [](A* p){delete[] p; }); std::shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p){fclose(p); }); return 0; }
由此可见,利用删除器就可以使不是new出来的对象或者是不同的类型的对象都可以由智能指针进行管理。