C++11智能指针
1、为什么需要智能指针?
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void f()
{
pair<string, string>* p1 = new pair<string, string>;
div();
delete p1;
cout << "delete:" << p1 << endl;
}
int main()
{
try
{
f();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
来看这段代码,我们new了之后进行了delete,看似没什么问题,但是如果div除0错误抛异常就会直接跳转到main函数内捕获异常处,然后继续往下执行,导致p1所指向空间没有释放,从而导致了内存泄漏。因此我们需要在div也加上try/catch捕获异常。
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void f()
{
pair<string, string>* p1 = new pair<string, string>;
try
{
div();
}
catch (...)
{
delete p1;
cout << "delete:" << p1 << endl;
throw;
}
delete p1;
cout << "delete:" << p1 << endl;
}
int main()
{
try
{
f();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
上面我们对代码进行了修改,现在如果div抛异常了,我们先释放p1的空间,然后再将异常重新抛出。也解决了问题。
但是如果是下面这种场景呢?
void f()
{
pair<string, string>* p1 = new pair<string, string>;
pair<string, string>* p2 = new pair<string, string>;
pair<string, string>* p3 = new pair<string, string>;
pair<string, string>* p4 = new pair<string, string>;
try
{
div();
}
catch (...)
{
delete p1;
cout << "delete:" << p1 << endl;
throw;
}
delete p1;
cout << "delete:" << p1 << endl;
}
现在p1可能抛异常,p1如果抛异常还好说,因为空间没有创建出来,也不需要释放。但是p2、p3、p4、div都有可能抛异常,如果p4抛异常我就要释放p1、p2、p3,如果div抛异常我就需要把p1->p4都释放掉,代码不好写,写出来也很不优雅。
所以就需要智能指针出场了。
2、内存泄漏
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
C/C++程序中一般我们关心两种方面的内存泄漏:
堆内存泄漏(Heap leak):堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏:指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
3、智能指针的使用及其原理
3.1、智能指针的原理
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
1、不需要显式地释放资源。
2、采用这种方式,对象所需的资源在其生命期内始终保持有效。
针对上面的问题,我们的解决方案如下:
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void f()
{
//pair<string, string>* p1 = new pair<string, string>;
//pair<string, string>* p2 = new pair<string, string>;
//pair<string, string>* p3 = new pair<string, string>;
//pair<string, string>* p4 = new pair<string, string>;
SmartPtr<pair<string, string>> sp1(new pair<string, string>);
SmartPtr<pair<string, string>> sp2(new pair<string, string>);
SmartPtr<pair<string, string>> sp3(new pair<string, string>);
try
{
div();
}
catch (...)
{
throw;
}
}
int main()
{
try
{
f();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}

如上,我们将指针交给一个对象去管理,对象生命周期内资源有效,对象生命周期结束释放资源。
当除0错误抛异常时,sp1->3对象的生命周期就结束了,它会自动去调用析构函数释放资源。
RAII(Resource Acquisition Is Initialization)- 资源获取即初始化。
上面SmartPtr还不能称为智能指针,智能指针还要能像指针一样使用,所以还需要重载以下两个函数:
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
下面就可以像指针一样去使用:
SmartPtr<pair<string, string>> sp1(new pair<string, string>("sort", "排序"));
cout << sp1->first << ":" << sp1->second << endl;
SmartPtr<string> sp2(new string("xxxxxxxxxxx"));
cout << *sp2 << endl;
总结一下智能指针的原理:
1. RAII特性
2. 重载operator*和opertaor->,具有像指针一样的行为。
思考:上面写的智能指针如果赋值会怎么样?
SmartPtr<string> sp1(new string("xxxxx"));
SmartPtr<string> sp2(new string("yyyyy"));
sp1 = sp2;
由于我们没有写赋值运算符重载,所以默认就是按字节序的值拷贝,会把sp2所指向空间的地址赋值给sp1,但是sp2原来那块空间并没有释放,从而导致了内存泄漏,这是其一。其二,最后析构的时候sp1和sp2都会析构,析构两次造成程序崩溃。
思考一下有什么解决方案?
1、直接禁止拷贝。
2、增加一个引用计数,当没有对象指向那块空间时就释放。
上面的解决方案就是我们下面要学的unique_ptr和shared_ptr,这是C++11新增的智能指针模板类。
3.2、auto_ptr
C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。
智能指针都包含于头文件<memory>
下面先实现一个类A,方便我们观察:
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
~A()
{
cout << this;
cout << " ~A()" << endl;
}
int _a;
};
auto_ptr<A> ap1(new A(1));
auto_ptr<A> ap2(new A(2));
auto_ptr<A> ap3(ap1);

运行程序,发现两块空间都释放了,没有问题。
下面再看:

这里我直接用ap1去访问_a,程序直接崩溃了。
这是因为auto_ptr的原理是:管理权转移,拷贝时,会把被拷贝对象的资源管理权转移给拷贝对象。
隐患:导致被拷贝对象悬空,访问就会出问题。
调试看一下:

所以auto_ptr底层实现就是把空间地址拷贝给ap3,然后把自己置空,这样再去访问ap1就会出问题。
auto_ptr是一个失败设计,已被弃用。
知道了原理,下面我们直接手撕一个auto_ptr:
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<T>& operator=(auto_ptr<T>& ap)
{
if (this != &ap)
{
if (_ptr)
delete _ptr;
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
delete _ptr;
_ptr = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
3.3、unique_ptr
unique_ptr的实现原理:简单粗暴的防拷贝。
unique_ptr<A> up1(new A(1));
unique_ptr<A> up2(new A(2));
unique_ptr<A> up3(up1);
up1 = up2;

拷贝构造和赋值直接报错。
那么如何防拷贝呢?可以把函数私有,但是还可以像下面这么写:
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
unique_ptr(const unique_ptr<T>&) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;
~unique_ptr()
{
if (_ptr)
{
delete _ptr;
_ptr = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
3.4、shared_ptr
3.4.1、shared_ptr原理和使用
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
先来看使用:
shared_ptr<A> sp1(new A(1));
shared_ptr<A> sp2(new A(2));
shared_ptr<A> sp3(sp1);
shared_ptr<A> sp4(sp2);
shared_ptr<A> sp5(sp4);

允许拷贝,可以访问对象,不会出现悬空问题,也不会出现多次析构的问题。
思考:如何实现引用计数呢?
思路1:直接在类内声明int _pcount行吗?答案肯定是不行的,这样每个类都有一份,同一块空间智能指针的引用计数是独有的,假设当前sp1和sp2都指向同一块空间,但是由于它们都独有一份_pcount,所以会析构两次。

思路2:静态成员变量可以吗?

静态成员变量也不行,因为同一个类模板实例化出来的所有对象都共享,但是如果我指向两块空间甚至更多的话,它们的引用计数是叠加在一起的,因此不行。
解决方案:直接在堆上开辟空间
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount);
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (this == &sp)
return *this;
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(_pcount);
return *this;
}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
private:
T* _ptr;
int* _pcount;
};
注意:赋值这里需要考虑自己给自己赋值,还需要考虑同一块空间的赋值,所以如果_ptr == sp._ptr,直接返回。
3.4.2、循环引用
但是shared_ptr真的有那么好吗?
其实shared_ptr还有一个问题——循环引用。
看下面的场景:
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
~A()
{
cout << this;
cout << " ~A()" << endl;
}
int _a;
};
struct Node
{
A _val;
Node* _prev;
Node* _next;
};
int main()
{
shared_ptr<Node> sp1(new Node);
shared_ptr<Node> sp2(new Node);
return 0;
}

上面的代码没有什么问题,空间也释放了,不存在内存泄漏。
下面我们在main函数中再加入两行代码:
sp1->_next = sp2;
sp2->_prev = sp1;

这里报错了,这是因为_prev和_next都是Node*,而sp1和sp2是个对象,类型不匹配,因此需要将Node内的成员类型修改为shared_ptr<Node>。
如下:
struct Node
{
A _val;
shared_ptr<Node> _prev;
shared_ptr<Node> _next;
};
这样就能赋值了,然后我们运行查看结果:

咦,这里怎么没有析构呢?怎么就内存泄漏了?
下面进行分析:

首先sp1指向一个节点,然后sp2指向另一个节点,引用计数分别是1,然后对sp1->_next和sp2->_prev赋值,两个节点的引用计数变成2。最后出了作用域sp1和sp2进行析构,两个节点的引用计数都变成1。
1、_prev什么时候析构?右边节点析构时,_prev就析构。
2、右边节点什么时候析构?_next析构时,右边节点就析构。
3、_next什么时候析构?左边节点析构时,_next就析构。
4、左边节点什么时候析构?_prev析构时,左边节点就析构。
然后又回到1,循环往复。
这就是shared_ptr的循环引用问题,在这种情况下会导致内存泄漏。
解决办法,使用weak_ptr:
struct Node
{
A _val;
//shared_ptr<Node> _prev;
//shared_ptr<Node> _next;
weak_ptr<Node> _prev;
weak_ptr<Node> _next;
};

再次运行程序,这时候就不存在内存泄漏了。
1、weak_ptr不是RAII智能指针,而是专门用来解决shared_ptr循环引用问题。
2、weak_ptr不增加引用计数,可以访问资源,不参与资源释放的管理。
3、weak_ptr是弱引用,不增加引用计数。shared_ptr是强引用,增加引用计数。
3.4.3、实现weak_ptr
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 shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
由于这里weak_ptr需要获取sp._ptr,但是在weak_ptr类内是不能直接访问sp._ptr的,因为它是私有的,所以需要在shared_ptr加上get函数,同时我们多实现一个use_count,use_count函数返回引用计数的值,这两个函数都是库里实现的。
int use_count() const
{
return *_pcount;
}
T* get() const
{
return _ptr;
}
将代码改成我们自己写的shared_ptr和weak_ptr进行测试
struct Node
{
A _val;
zzy::weak_ptr<Node> _next;
zzy::weak_ptr<Node> _prev;
};
int main()
{
zzy::shared_ptr<Node> sp1(new Node);
zzy::shared_ptr<Node> sp2(new Node);
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
sp1->_next = sp2;
sp2->_prev = sp1;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
return 0;
}

3.4.4、总结
auto_ptr:管理权转移,会导致被拷贝对象悬空,建议不要使用。
unique_ptr:简单粗暴,直接禁止拷贝。日常使用,不需要拷贝的场景就用它。
shared_ptr:引用计数支持拷贝,需要拷贝的场景就使用它。但是要小心构成循环引用,循环引用会导致内存泄漏。
weak_ptr:不是RAII的智能指针,专门用来处理shared_ptr的循环引用问题。
3.4.5、定制删除器
之前我们指向的对象都是单个的,如果我现在开的是一个数组呢?
zzy::shared_ptr<A> sp1(new A[10]);
zzy::shared_ptr<A> sp2((A*)malloc(sizeof A));
由于我们析构函数都是delete,因此在这种情况下就会出问题。
第一行代码,我们用的是new [],应该搭配delete[]来使用,而我们直接delete,导致程序崩溃,哪怕程序不崩溃也会内存泄漏。
第二行代码,我们用的是malloc,应该搭配free来使用。
对于这个问题,我们就需要使用定制删除器来解决了,先来看看库里的:

库里并不是在类模板中添加模板参数,而是在构造函数添加一个模板参数D,这个D就相当于是一个调用对象,定制化删除。当我们需要delete[]或free,我们就自己传一个给构造函数。
随之而来的问题就是,释放空间是在析构函数中释放的,你现在传给构造函数,那怎么在析构函数中用呢?
我们可以在shared_ptr中声明一个包装器,这个包装器包装了一个返回值为void,参数为T*的指针。
function<void(T*)> _del = [](T* ptr) {delete ptr; };
同时,我们直接给上缺省值,默认就是delete,所以我们直接用lambda表达式。
紧接着我们需要重载一个构造函数,同时修改析构函数:
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(new int(1))
, _del(del)
{}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
_del(_ptr);
delete _pcount;
}
}
这里包装器function<void(T*)>和构造函数中的T是同一个T。*
那么我们在构造的时候就可以传入一个删除器,这里也可以写仿函数,但是我们直接使用lambda,这样更方便:
zzy::shared_ptr<A> sp1(new A[10], [](A* ptr) {delete[] ptr; });
zzy::shared_ptr<A> sp2((A*)malloc(sizeof A), [](A* ptr) {free(ptr); });

可以看到,开的空间都释放了,不存在内存泄漏。
我们还可以这么用,假设我们通过fopen打开文件,然后将FILE*交给智能指针去管理,通过定制删除器调用fclose关闭文件:
zzy::shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* ptr){
cout << "fclose:" << ptr << endl;
fclose(ptr);
});

3.4.6、shared_ptr线程安全问题
shared_ptr本身是线程安全的,但是它所指向的资源不是线程安全的,官方实现的引用计数++/–是线程安全的。
但是我们上面实现的shared_ptr引用计数++/–不是线程安全的,所以这里我们可以使用atomic:
namespace zzy
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new atomic<int>(1))
{}
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(new atomic<int>(1))
, _del(del)
{}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
_del(_ptr);
delete _pcount;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount);
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr == sp._ptr)
return *this;
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
return *this;
}
int use_count() const
{
return *_pcount;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
atomic<int>* _pcount;
function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
}
它指向的资源不是线程安全的,所以多线程访问需要加锁:
int main()
{
zzy::shared_ptr<int> sp(new int(1));
size_t n1 = 10000000;
size_t n2 = 10000000;
mutex mtx;
thread t1([&]() {
for (size_t i = 0; i < n1; i++)
{
zzy::shared_ptr<int> sp1(sp);
{
unique_lock<mutex> lock(mtx);
(*sp1)++;
}
zzy::shared_ptr<int> sp2(sp);
}
});
thread t2([&]() {
for (size_t i = 0; i < n2; i++)
{
zzy::shared_ptr<int> sp1(sp);
{
unique_lock<mutex> lock(mtx);
(*sp1)++;
}
zzy::shared_ptr<int> sp2(sp);
}
});
t1.join();
t2.join();
cout << (*sp) << endl;
return 0;
}

我们只需要在修改资源的时候加锁,其他地方并不需要,这里有一个小技巧——使用局部域将它们包起来。
745

被折叠的 条评论
为什么被折叠?



