C++11智能指针
一、引入智能指针的重要性
关于智能指针,其实是C++11的一种新的特性,记住一句话:它主要是解决内存泄漏问题,因为智能指针是一个类,当超出了类的作用域之后,类就会自动调用析构函数,析构函数就会自动释放资源。那这里可能会问,当我们申请一块内存空间的时候,当使用完,记得释放掉不就可以了吗?其实也没错,但是如果在new之后,delete之前,抛出了异常,如以下fun2代码所示:
void fun1()
{
throw int(11); //抛出异常
}
void fun2()
{
int* p = new int[10000];
fun1();
delete[] p;
}
int main()
{
try
{
fun2();
}
catch (int& e)
{
cout << "捕获" << endl;
}
system("pause");
return 0;
}
这样写是会造成内存泄漏问题,那这里其实有一种办法可以解决,就是如果发现delete之前发现某个函数抛出了异常,就在delete之前捕获这个异常,并且在catch语句里面进行资源的释放,并且可以再将这个异常重新抛出。如以下代码所示:
void fun2()
{
int *p = new int[10000];
try
{
fun1();
}
catch(int& e)
{
delete[] p;
cout << "重新抛出" << endl;
throw;
}
delete[] p;
}
有没有发现很繁琐?如果代码很多的话,我们很容易混淆,不知道究竟是哪个抛出了异常。所以引出智能指针是很有必要的
二、智能指针的原理
2.1 RAII
(1)RAII(Resource Acquisition Is Initialization -> 资源获得即初始化),是一种利用生命周期来控制程序资源的简单技术。也是一种避免内存泄漏的方法
(2)在对象构造的时候获取资源,接着控制对资源的访问使之在对象的生命周期内有效,最后在对象析构的时候释放资源
(3)我们实际上把管理一份资源的责任交给了一个对象,这样做不仅可以不需要显示的去释放资源,也可以使得对象所需的资源在生命周期内始终有效。
2.2 智能指针的原理
为了使智能指针能够像平常的指针一样,我们对“->” , " * "进行了重载
template<class T>
class SmartPtr
{
private:
T* _ptr;
public:
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{
}
T& operator*() //重载* ,方便解引用
{
return *_ptr;
}
T* operator->() //重载 ->,方便访问结构体成员
{
return _ptr;
}
~SmartPtr()
{
if (_ptr)
delete _ptr;
}
};
int main()
{
int *p = new int;
SmartPtr<int> sp(p);
*sp = 10;
cout << *sp << endl;
system("pause");
return 0;
}
三、C++库中的智能指针
3.1 auto_ptr
头文件:memory
auto_ptr可以说是最原始的智能指针,但是他也会存在一个问题,如以下:
int main()
{
int* p = new int;
auto_ptr<int> ap(p);
auto_ptr<int> copy(ap);
*ap = 10;
system("pause");
return 0;
}
这里程序就会崩溃,这么说吧,当我们将ap拷贝之后,他自己就会成为一个空指针,那再解引用空指针,就会出问题。我们可以模拟实现一下auto_ptr,你就会明白为啥了
3.1.2 auto_ptr的模拟实现
template<class T>
class MyAuto_ptr
{
private:
T* _ptr;
public:
MyAuto_ptr(T* ptr)
:_ptr(ptr)
{}
MyAuto_ptr(MyAuto_ptr<T>& p)
:_ptr(p._ptr) //拷贝构造
{
p._ptr = nullptr; //发生拷贝构造之后自己就置空了,所以解引用空指针就发生了问题
}
MyAuto_ptr<T>& operator=(MyAuto_ptr<T> &p) //重载赋值
{
if (this != &p) //防止自己给自己赋值
{
if (_ptr)
_ptr = nullptr;
_ptr = p._ptr;
p._ptr = nullptr; //将自己置空
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T& operator->()
{
return _ptr;
}
~MyAuto_ptr()
{
if (_ptr)
delete _ptr;
}
};
3.2 unique_ptr
C++ 库里面的unique_ptr,但从字面意思来说unique就是表示特殊的,唯一的意思,它的确很粗暴,因为auto_ptr拷贝构造的缺陷,它直接将拷贝构造和赋值都delete掉.做了防拷贝,如下所示:
template<class T>
class MyUnique_ptr
{
private:
T* _ptr;
public:
MyUnique_ptr(T* ptr=nullptr)
:_ptr(ptr)
{}
MyAuto_ptr(MyAuto_ptr<T>& p) = delete; //拷贝构造
MyAuto_ptr<T>& operator=(MyAuto_ptr<T> &p)=delete //重载赋值
T& operator*()
{
return *_ptr;
}
T& operator->()
{
return _ptr;
}
~MyUnique_ptr()
{
if (_ptr)
delete _ptr;
}
};
我们是不是还没看出它的缺点,那再来看一个代码:
unique_ptr<int> p1(new string("hello world"));
unique_ptr<int> p2;
p2 = p1; // #1、 not allowed
unique_ptr<int> p3;
p3 = unique_ptr<int>(new string("hello world")); //#2、 allowed
其中#1留下悬挂的unique_ptr(p1),这可能导致危害(我觉得…unique_ptr连赋值和拷贝构造函数都没有,应该就出错了)。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 p3 后就会被销毁
那其实真正的问题还是没有解决,它是将指针悬空了,那拷贝构造的问题还是没有解决。
3.3 shared_ptr
shared_ptr从字面来说就是“共享式拥有概念”,多个智能指针可以指向相同的内存空间,该空间和其相关资源会在“最后一个引用被销毁”的时候释放。使用计数来表明资源被几个指针共享。(我老感觉这里有点写时拷贝的感觉…)
class MyShared_ptr
{
private:
T* _ptr;
int* _count; //标记内存被几个智能指针指向的计数
};
3.3.1 shared_ptr 的模拟实现:
template<class T>
class MyShared_ptr
{
public:
MyShared_ptr(MyShared_ptr<T>& p)//拷贝构造函数
:_pr(p._ptr)
,_count(p._count)
{
++(*_count); //增加计数
}
MyShared_ptr<T>& operator=(MyShared_ptr<T>& p)
{
if (_ptr != p._ptr) //防止自己给自己赋值
{
if (--(*count) == 0) //如果是最后一个指向该空间的指针,可以直接删除
{
delete _ptr;
delete _count;
}
_ptr = p._ptr;
_count = p._count;
++(*_count);
}
}
~MyShared_ptr()//析构函数,若当前计数为0,直接释放空间
{
if (--(*_count) == 0)
{
delete _ptr;
delete _count;
}
}
private:
T* _ptr;
int* _count;
};
这里看到share_ptr好像已经接近完美了,已经把拷贝构造和赋值的问题解决了
关于shared_ptr得使用:
shared_ptr<int> sp1(new int(20));
shared_ptr<int> sp2(sp1); //调用拷贝构造
cout<<*sp2<<endl;
3.3.2 shared_ptr存在的问题
但是啊,其实也是有点问题的,原因如下:
智能指针对象中引用计数是多个智能指针对象共享的,如果有几个线程中智能指针的引用计数同时操作,比如 一个++,一个 - -,会导致资源未释放或者程序崩溃的问题。简而言之就是这个操作不是原子的,会引发线程安全问题。我们能解决的就是上锁来保证线程安全,直接贴上代码吧!
template<class T>
class Shared_Ptr {
public:
Shared_Ptr(T* ptr = nullptr)
: _ptr(ptr)
, _count(new int(1))
, _pmtx(new mutex)
{}
Shared_Ptr(Shared_Ptr<T>& ap)
:_ptr(ap._ptr)
, _count(ap._count)
, _pmtx(ap._pmtx)
{
Add();
}
void Add()//加锁的++
{
_pmtx->lock();
++(*_count);
_pmtx->unlock();
}
void Release()//加锁的--
{
bool flag = false;
_pmtx->lock();
if (--(*_count) == 0)
{
delete _ptr;
delete _count;
flag = true;//判断是否释放锁
}
_pmtx->unlock();
if (flag == true)
delete _pmtx;
}
Shared_Ptr<T>& operator=(Shared_Ptr<T>& ap)
{
if (_ptr != ap._ptr)
{
Release();
_ptr = ap._ptr;
_count = ap._count;
Add();
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~Shared_Ptr()
{
Release();
}
private:
T* _ptr;
int* _count;
mutex* _pmtx;
};
3.3.3 shared_ptr存在的循环引用问题
(别急,这是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;
}
写这个是希望cur和prev被智能指针管理,但是报错了。原因如下:
(1)node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
(2)node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
(3)node1和node2析构,引用计数减到1,但是_next还指向下一个节点。并且_prev还指向上一个节点。
(4)也就是说_next析构了,node2就释放了,_prev析构了,node1就释放了。
(5)但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2 成员,这就叫循环引用,谁也不会释放。
解决方法:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
// 原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加 node1和node2的引用计数。
struct ListNode
{
int _data;
weak_ptr<ListNode> _prev;
weak_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;
}
四、总结
- auto_ptr :管理权限转移,在拷贝构造的时候,会将自己的_ptr置为nullptr,解引用的时候会崩溃,有缺陷,一般严禁使用
- unique_ptr:直接将拷贝构造和赋值delete掉,防拷贝,一般鼓励使用
- shared_ptr:设置引用计数,同时要考虑线程安全和循环引用,线程安全问题通过加锁解决,而循环引用问题用weak_ptr解决