目录
一、智能指针的出现
如果在类似这样的一个场景中
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;//如果p2抛异常,p1内存泄露
//如果div函数抛异常,p1和p2内存泄露
cout << div() << endl;
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
也就是在堆空间上手动申请空间后,在释放该空间之前,如果抛异常,可能导致程序直接终止,这样一来,这部分空间得不到释放,出现了内存泄露。
于是,设计出智能指针这样的解决方案来解决抛异常导致内存泄露的问题。
简单了解一下内存泄露。
- 什么是内存泄露
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
- 内存泄露的分类
堆内存泄漏 (Heap leak)堆内存 指的是程序执行中依据须要分配通过 malloc / calloc / realloc / new 等从堆中分配的一 块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak 。 系统资源泄漏指程序使用系统分配的资源,比如 套接字 、 文件描述符 、 管道 等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
智能指针的核心技术思想就是RAII(Resource Acquisition Is Initialization),直译为利用对象生命周期来控制程序资源(如内存、文件句柄、网络链接、互斥量等)。
大致理解为,利用类的构造函数和析构函数自动调用,对这一层程序资源进行封装。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显示的释放资源。
- 采用这种方式,对象所需要的资源在其生命周期内始终保持有效。
template <class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _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 (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
智能指针不是指针,是像指针一样的一个类。
智能指针的原理就两点
- RAII特性
- 重载operator* 和operator->
二、STL中的智能指针
智能指针是十分优秀的一种解决方案,在使用各种已经设计好的智能指针时,我们重点关注拷贝构造函数的设计。因为智能指针是代替管理程序中的资源,对于该拷贝构造函数的要求就不再是深拷贝的层次了,而是浅拷贝。
vector、list、map等等 | 拷贝构造为深拷贝,利用这种类来管理数据时,数据和类是一一对应的,一个对象有一份数据,属于自己本身 |
智能指针、迭代器 | 拷贝构造为浅拷贝,这种类对于数据只是暂时代管,利用类的特性进行的一层封装,本质这些数据不属于它们,在拷贝构造时,只需要浅拷贝即可 |
auto_ptr
和智能指针相关的类都在头文件
auto_ptr实现于C++98中,我们不在实践中使用它,是因为这种解决方案脱离需求。
它的大致实现如下
template <class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& ap)
{
_ptr = ap._ptr;
ap._ptr = nullptr;
}
~auto_ptr()
{
cout << "delete" << _ptr << endl;
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
它的拷贝构造函数将被拷贝对象ap代管的资源转移后,将被拷贝对象ap置空,这种实现会导致在完成拷贝构造后,任何调用ap所在的对象都会出现错误。这无疑是巨大的漏洞,因此,有许多公司也禁止员工使用该类。
unique_ptr
在介绍该智能指针之前,先了解一些题外话。
C++标准委员会在吸取很多教训后,不再直接将新内容加入标准库中,而是打算用一个新的库作为试验品,先将新内容、新语法加入到这个库中,不断试验,其中,优秀的设计再加入到下一个标准中,这个库就是boost库。
百度百科:Boost社区建立的初衷之一就是为C++的标准化工作提供可供参考的实现,Boost社区的发起人Dawes本人就是C++标准委员会的成员之一。在Boost库的开发中,Boost社区也在这个方向上取得了丰硕的成果。在送审的C++标准库TR1中,有十个Boost库成为标准库的候选方案。在更新的TR2中,有更多的Boost库被加入到其中。从某种意义上来讲,Boost库成为具有实践意义的准标准库。
在C++11之前,boost库中,有这样两组设计
boost::scoped_ptr、scoped_array boost::shared_ptr、shared_array
其中C++11标准中
c++标准库中std::unique_ptr 跟boost::scoped_ptr类似 c++标准库中std::shared_ptr 跟boost::shared_ptr类似
unique_ptr认为拷贝是极具风险的,因此,该类直接禁止生成拷贝构造函数。
template<class T>
class unique_ptr
{
public:
// RAII
unique_ptr(T* ptr)
:_ptr(ptr)
{}
//
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
~unique_ptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
shared_ptr
C++11提供了更加靠谱且可以拷贝的shared_ptr。
shared_ptr是通过引用计数来实现多个对象之间共享资源。如果引用计数为0,就说明当前对象是最后一个使用该资源的,程序结束时调用析构必须要将该资源释放。如果引用计数不为0,就说明还有其他对象在使用该资源,不能释放该资源。
那怎么实现引用计数呢?使用静态成员变量可行吗?
template <class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
{
_count = 1;
}
shared_ptr(const shared_ptr<T>& sp)
{
_ptr = sp._ptr;
++_count;
}
~shared_ptr()
{
if (--_count == 0)
{
cout << "delete" << _ptr << endl;
delete _ptr;
}
}
int use_count()
{
return _count;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
static int _count;
T* _ptr;
};
答案是不行,因为静态成员变量是计数所有的对象,只要类型相同就会计数,_count是所有对象共享,而我们所期望是一个资源空间共享一个_count。
正确的写法是让资源和计数绑定,在堆上开空间存放_count。
template <class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
{
_count = new int(1);
}
shared_ptr(const shared_ptr<T>& sp)
{
_ptr = sp._ptr;
_count = sp._count;
++(*_count);
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_count = sp._count;
++(*_count);
}
return *this;
}
void release()
{
if (--(*_count) == 0)
{
cout << "delete" << _ptr << endl;
delete _ptr;
delete _count;
}
}
~shared_ptr()
{
release();
}
int use_count()
{
return *_count;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
private:
int* _count;
T* _ptr;
};
这里需要解释一下operator=的写法
int main()
{
shared_ptr<int> sp1(new int(1));
cout << sp1.use_count() << endl;
shared_ptr<int> sp2(sp1);
cout << sp2.use_count() << endl;
shared_ptr<int> sp4(new int(5));
shared_ptr<int> sp3(sp2);
cout << sp3.use_count() << endl;
sp1 = sp4;
sp1 = sp2;
sp4 = sp4;
return 0;
}
- 如果是不同资源间的赋值,比如
sp1 = sp4
赋值结束后,sp1所在资源的计数减一,然后sp1指向sp4所在资源,并且计数加一。要注意减一后,sp1所在资源的计数不能为0,否则该资源没法释放,会出现内存泄露,因此减一的逻辑要复用析构的逻辑。
- 如果是同一资源的不同对象,比如
sp1 = sp2;
赋值结束后,减一加一都是对同一资源进行的操作,因此没有必要做这块的消耗,所以,条件比较时,是用对象的内部指针作比较,而对象本身。
if (_ptr != sp._ptr)
//不是这样if (*this != sp)
- 如果是自己给自己赋值,比如
sp4 = sp4
这句代码和第二种情况的结果没有区别,上述写法已经可以规避这样无意义的消耗了。
shared_ptr的缺陷
shared_ptr的缺陷就是可能会出现循环引用。比如当资源空间是ListNode这样的自定义类型,内部有两个指针,双向循环链表的结点,两个结点的指针可能互相引用。
struct ListNode
{
int _val;
uto::shared_ptr<ListNode> _next;
uto::shared_ptr<ListNode> _prev;
ListNode(int val = 0)
:_val(val)
{}
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
uto::shared_ptr<ListNode> n1(new ListNode(10));
uto::shared_ptr<ListNode> n2(new ListNode(20));
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
return 0;
}
- node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动 delete。
- node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
- 程序运行结束,node1和node2析构,引用计数减到1,但是_next还指向_node2,同时_prev还指向_node1。
- 也就是说_next析构了,node2就释放了。
- 也就是说_prev析构了,node1就释放了。
- 但是_next属于node1的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev又属于node2成员。这就叫循环引用,谁也不会释放。
解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和
_prev不会增加node1和node2的引用计数。
// weak_ptr不支持RAII,不参与资源管理
template<class T>
class weak_ptr
{
public:
//从构造函数可以看出,weak_ptr不代管资源
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& wp)
{
_ptr = sp.get();
}
weak_ptr<T>& operator=(const shared_ptr<T>& wp)
{
_ptr = sp.get();
return *this;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
改写ListNode的next和prev后,解决了循环引用。
struct ListNode
{
int _val;
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
ListNode(int val = 0)
:_val(val)
{}
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
shared_ptr定制删除器
我们之前介绍的shared_ptr都是用来封装一些简单类型
int main()
{
shared_ptr<ListNode> sp1(new ListNode(10));
return 0;
}
如果封装一些比较复杂的类
shared_ptr<ListNode> sp2(new ListNode[10]);
shared_ptr<FILE> sp3(fopen("Test.cpp", 'r'));
仅仅是我们这样的析构实现,是不能有效地释放资源的
void release()
{
if (--(*_count) == 0)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _count;
}
}
因此,shared_ptr支持定制删除器的模式,也实现了对应的构造函数
我们根据自己的理解实现定制删除器,先在构造函数加入删除器模板
template<class D>
shared_ptr(T* ptr = nullptr,D del)
:_ptr(ptr)
, _count(new int(1))
,_del(del)
{
}
而我们想要在析构函数调用D类型,有哪些方法呢。一种是改变类模板参数,在类中定义D类型的成员变量。还有一种,利用包装器,可以不用传模板参数,只要符合包装器的类型即可。
关于包装器中的类型,一般包装器包装三种类型,回调函数指针、lambda、仿函数,这里是用来析构,返回值设置为void,参数和类模板参数相同,可以同时给出缺省值,表示当没有特殊的删除器,即只是析构简单类型的情况。
private:
int* _count;
T* _ptr;
function<void(T*)> _del = [](T* ptr) {delete ptr; };
于是,析构函数就可以用包装器来写
void release()
{
if (--(*_count) == 0)
{
cout << "delete:" << _ptr << endl;
_del(_ptr);
//delete _ptr;
delete _count;
}
}
~shared_ptr()
{
release();
}
回到刚开始的问题,如果要析构下面这行代码
shared_ptr<FILE> sp3(fopen("Test.cpp", 'r'));
可以直接传lambda表达式作为删除器
shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* ptr) {fclose(ptr); });
如果析构一个数组
shared_ptr<ListNode> sp2(new ListNode[10]);
可以写一个仿函数,作为删除器
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
shared_ptr<ListNode> sp2(new ListNode[10],DeleteArray<ListNode>());
完整的加入删除器的代码
template <class T>
class shared_ptr
{
public:
template<class D>
shared_ptr(T* ptr = nullptr,D del)
:_ptr(ptr)
, _count(new int(1))
,_del(del)
{
}
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
{
_count = new int(1);
}
shared_ptr(const shared_ptr<T>& sp)
{
_ptr = sp._ptr;
_count = sp._count;
++(*_count);
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_count = sp._count;
++(*_count);
}
return *this;
}
void release()
{
if (--(*_count) == 0)
{
cout << "delete:" << _ptr << endl;
_del(_ptr);
//delete _ptr;
delete _count;
}
}
~shared_ptr()
{
release();
}
int use_count()
{
return *_count;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
int* _count;
T* _ptr;
function<void(T*)> _del = [](T* ptr) {delete ptr; };
};