C++智能指针的发展
智能指针
GC–garbage collection垃圾回收,Java里的机制。在头文件<memory>
中
内存泄漏
堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
采用RAII思想或者智能指针来管理资源。
RAII资源获得即初始化
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
相当于把资源生命周期和对象生命周期绑定在一起了==>在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:1.不需要显式地释放资源;2.采用这种方式,对象所需的资源在其生命期内始终保持有效。
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public://保存资源SmartPtr(T* ptr = nullptr): _ptr(ptr){}//释放资源~SmartPtr(){if(_ptr) delete _ptr;}
private:T* _ptr;
};
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。
C98 auto_ptr
原理如下:
- RAII特性
- 重载operator*和opertaor->,具有像指针一样的行为。
AutoPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。
template<class T>
class auto_ptr {
public://保存资源auto_ptr(T* ptr = nullptr): _ptr(ptr){}//释放资源~auto_ptr(){if(_ptr) delete _ptr;}//重载operator*T& operator*(){return *_ptr;}//重载operator->T* operator->(){return _ptr;}
private:T* _ptr;
};
存在的问题-对象悬空
auto_ptr类中没有写拷贝构造,构造函数就是浅拷贝,auto_ptr<int> auto_p1(new int); auto_ptr<int> autop2(auto_p2);
这样的代码,就会造成资源重复析构。C98中对于auto_ptr
资源重复析构提出的解决方法是资源管理器转移,即只有一个对象管一份资源,auto_p1
就不管了,让auto_p2
管==>会导致对象悬空,即auto_p1悬空。
auto_ptr(SmartPtr<T>& sp) :_ptr(sp.ptr) {// 管理权转移sp._ptr = nullptr;
}
auto_ptr<T>& operator=(SmartPtr<T>& sp) {// 检测是否为自己给自己赋值if (this != &sp) {// 释放当前对象中资源if (_ptr) delete _ptr;// 转移sp中资源到当前对象中_ptr = sp._ptr;sp._ptr = NULL;}return *this;
}
注意:解决办法绝对不能写深拷贝!!因为要模拟的是原生指针,且这份资源归属是用户的,咱不能偷偷地copy一份。
C++11 unique_ptr
借鉴的是boost的scoped_ptr(防拷贝的智能指针),只是在auto_ptr的基础上加了两句。详细的模拟实现请参考我的gitee库
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
C++11 shared_ptr
shared_ptr本身是线程安全的,用锁保护了引用计数的++/–,但它管理的资源不是线程安全的,需要用户手动控制。共享指针的mutex保护的是自己的计数器的线程安全,不保护资源的线程安全。
借鉴的是boost的shared_ptr(可以拷贝的智能指针),原理是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
- shared_ptr在其内部,给每个资源都维护了着一份计数器,用于记录该份资源被几个对象共享。
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
计数器的实现
可以采用 static map<T* ptr, static int count>;
,但是后续加锁是个大问题。
简单的方法就是申请新资源的时候再开一块计数器空间,让对象中的一个指针指向资源,另个指针指向计数器空间。
成员变量的计数器要写成int* count;
,可以实现对单份资源的管理,让一份资源拥有一个计数器。
不能写int count;
这个变成了单个对象的计数;
也不能写static int count;
因为静态成员变量是属于整个类的,所有对象均可以访问,相当于公共计数器,不是独一份资源的计数器。
锁的实现
一份资源对应一把锁,故写成mutex* _mutex;
在用shared_ptr的时候,如何保证资源的线程安全?重新用一把锁保护资源!注意此处不能去用计数器的锁,因为两块资源不是一样的空间。
存在的问题-循环引用
在如下所示struct ListNode
对象创建的双向链表处会出问题,因为其成员变量是std::shared_ptr<ListNode>
类型的,node1
本身和node2._prev
会同时指向node1的引用计数,node2
本身和node1._next
会同时指向node2的引用计数。
node1和node2都是局部对象,出了作用域就会销毁,但是他们的_prev
和_next
没法对引用计数器–。类对象的成员什么时候销毁?对象销毁的时候,其成员变量才会销毁,但是node2要等node1._next
销毁了才会销毁,node1要等node2._prev
销毁了才会销毁,这就造成了死循环,谁都没有销毁。
拓展来说,假设有两个类,类A中有个成员管理着类B的另一个成员,类B中有个成员管理着类A的另一个成员,这也会造成循环引用。
struct ListNode
{std::shared_ptr<ListNode> _prev;std::shared_ptr<ListNode> _next;~ListNode() {std::cout << "~ListNode()" << std::endl; }//仅为观察实验现象,无其他作用
};
int main()
{std::shared_ptr<ListNode> node1(new ListNode);std::shared_ptr<ListNode> node2(new ListNode);node1->_next = node2;//赋值构造 会对node1的引用计数做++node2->_prev = node1;//赋值构造 会对node2的引用计数做++return 0;
}
只要屏蔽node1->_next = node2;
或者node2->_prev = node1;
里的任何一句,都不会有循环引用的问题。
C++11 weak_ptr
没有使用RAII思想。其功能是可以指向资源/访问资源,但是不参与资源的管理,不增加引用计数。与shared_ptr搭配使用。
struct ListNode
{std::weak_ptr<ListNode> _prev;//使用weak_ptr来解决std::weak_ptr<ListNode> _next;//使用weak_ptr来解决~ListNode() {std::cout << "~ListNode()" << std::endl; }//仅为观察实验现象,无其他作用
};
int main()
{std::shared_ptr<ListNode> node1(new ListNode);std::shared_ptr<ListNode> node2(new ListNode);node1->_next = node2;//赋值构造 会对node1的引用计数做++node2->_prev = node1;//赋值构造 会对node2的引用计数做++return 0;
}
C++11 default_delete
以上4种指针只能管理单个new空间,因为delete在释放资源的时候,需要考虑到是数组(要用delete[])还是单个数(delete即可),new[]
和delete[]
要匹配,否则可能会导致问题。
于是C++11引入定制删除器,默认的定制删除器用的是delete。constexpr shared_ptr() noexcept;
用到定制删除器可以看这个构造函数template <class U, class D> shared_ptr (U* p, D del);
,其中del可以用函数指针、仿函数、lambda表达式。
template<class T>
struct delete_array
{operator()(const T* ptr){delete[] ptr;cout << "delete[] :" << ptr << endl;}
};int main()
{std::shared_ptr<int> sp1(new int[10], delete_array<int>());//仿函数std::shared_ptr<string> sp2(new string[10], delete_array<string>());//lambda表达式std::shared_ptr<string> sp3(new string[10], [](string* ptr) {delete[] ptr; });std::shared_ptr<FILE> sp4(fopen("text.txt", "r"), [](FILE* ptr) {fclose(ptr); });return 0;
}
结合定制删除器改造shared_ptr
template<class T>
struct defaule_delete
{void operator()(T* ptr){delete ptr;}
};template<class T, class D = defaule_delete<T>>
class shared_ptr
{
public:shared_ptr(T* ptr = nullptr) :_ptr(ptr), _pRefCount(new int(1)), _mutex(new mutex){}void Release(){int flag = 0;//标记是否需要释放锁,这是局部变量(独立栈空间)_mutex->lock();if (--(*_pRefCount) == 0){flag = 1;//delete _ptr;_del(_ptr);delete _pRefCount;}_mutex->unlock();if (flag) delete _mutex;}~shared_ptr(){Release();}shared_ptr(const shared_ptr<T>& sp) :_ptr(sp._ptr), _pRefCount(sp._pRefCount), _mutex(sp._mutex){_mutex->lock();++(*_pRefCount);_mutex->unlock();}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (sp._ptr != _ptr){Release();_ptr = sp._ptr;_pRefCount = sp._pRefCount;_mutex = sp._mutex;_mutex->lock();++(*_pRefCount);_mutex->unlock();}return *this;}int use_count() { return *_pRefCount; }T* get(){return _ptr;}// 像指针一样使用T& operator*() { return *_ptr; }T* operator->() { return _ptr; }T* get() const { return _ptr; }
private:T* _ptr;int* _pRefCount;mutex* _mutex;D _del;
};template<class T>
struct delete_array
{void operator()(const T* ptr){delete[] ptr;//cout << "delete[]" << endl;}
};struct close_file
{void operator()(FILE* ptr){fclose(ptr);}
};
int main()
{shared_ptr<int, delete_array<int>> sp1(new int[5]);shared_ptr<string, delete_array<string>> node1(new string[5]);shared_ptr<FILE, close_file> file1(fopen("test.txt", "r"));//shared_ptr<int> sp2(new int);return 0;
}