C++11---智能指针
Posted Moua
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++11---智能指针相关的知识,希望对你有一定的参考价值。
在使用C++编程时,要求使用malloc/new申请出来的空间必须使用free/delete进行释放,如果程序员没有对使用malloc/new申请的空间在使用free/delete进行释放,则可能会造成内存泄露问题。但是,在C++中有些情况下,即使成员进行了释放也可能存在一些安全隐患,例如下面的程序:
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
_MergeSort(a, 0, n - 1, tmp);
// 这里假设处理了一些其他逻辑
vector<int> v(1000000000, 10);
// ...
free(tmp);
}
int main()
{
int a[5] = { 4, 5, 2, 3, 1 };
MergeSort(a, 5);
return 0;
}
- 在MergeSort中使用malloc申请了空间,虽然说在最后使用free进行了释放。
- 但是,如果在malloc和free之间抛出了异常,该异常由main函数中进行捕获,那么free也是无法被执行的。
这时,就需要使用智能指针来解决了。
1、RAII
RAII是一种利用对象的生命周期对资源进行管理的简单方式。
在构造对象时获取资源,接着控制对资源的访问,使之在对象声明周期内都有效,最后在对象析构的时候释放资源。
2、智能指针原理
智能指针就是在RAII的方式上,对指针类加上*重载和->重载,使之具有指针的行为。
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if(_ptr)
delete _ptr;
}
T& operator*() {return *_ptr;}
T* operator->() {return _ptr;}
private:
T* _ptr;
};
struct Date
{
int _year;
int _month;
int _day;
};
int main()
{
SmartPtr<int> sp1(new int);
*sp1 = 10
cout<<*sp1<<endl;
SmartPtr<int> sparray(new Date);
// 需要注意的是这里应该是sparray.operator->()->_year = 2018;
// 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->
sparray->_year = 2018;
sparray->_month = 1;
sparray->_day = 1;
}
简单来说,智能指针就是使用RAII的特性+operator*和operator->使其具备有指针一样的行为。
3、智能指针的演进过程
在只能指针中存在一个问题就是,当使用智能指针拷贝对象时,会发生一些意想不到的事情。
- 在拷贝只能指针对象时,只能是使用值拷贝(针对不同的对象指针有不同的拷贝方式,因此没办法做到深拷贝)。
- 值拷贝存在一个问题,多个对象指向同一块内存。
- 其中一个释放,其他的也就不能访问了。
其实C++中对智能指针的演进过程其实也就是对这个问题的解决过程。
1)auto_ptr
C++98的库中提供的智能指针就是auto_ptr:
// C++库中的智能指针都定义在memory这个头文件中
#include <memory>
class Date
{
public:
Date() { cout << "Date()" << endl;}
~Date(){ cout << "~Date()" << endl;}
int _year;
int _month;
int _day;
};
int main()
{
auto_ptr<Date> ap(new Date);
auto_ptr<Date> copy(ap);
// auto_ptr的问题:当对象拷贝或者赋值后,前面的对象就悬空了
// C++98中设计的auto_ptr问题是非常明显的,所以实际中很多公司明确规定了不能使用auto_ptr
ap->_year = 2018;
return 0;
}
在C++98中auto_ptr解决这个问题的方法是,拷贝构造采用类似移动构造的方式,将原来对象的空间移动到新的对象上。但是,这也存一个问题,前边的对象就会被悬空,如果使用者不注意可能会出现一些意向不到的错误。
// 模拟实现一份简答的AutoPtr,了解原理
template<class T>
class AutoPtr
{
public:
AutoPtr(T* ptr = NULL)
: _ptr(ptr)
{}
~AutoPtr()
{
if(_ptr)
delete _ptr;
}
// 一旦发生拷贝,就将ap中资源转移到当前对象中,然后另ap与其所管理资源断开联系,
// 这样就解决了一块空间被多个对象使用而造成程序奔溃问题
AutoPtr(AutoPtr<T>& ap)
: _ptr(ap._ptr)
{
ap._ptr = NULL;
}
AutoPtr<T>& operator=(AutoPtr<T>& ap)
{
// 检测是否为自己给自己赋值
if(this != &ap)
{
// 释放当前对象中资源
if(_ptr)
delete _ptr;
// 转移ap中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
T& operator*() {return *_ptr;}
T* operator->() { return _ptr;}
private:
T* _ptr;
};
int main()
{
AutoPtr<Date> ap(new Date);
// 现在再从实现原理层来分析会发现,这里拷贝后把ap对象的指针赋空了,导致ap对象悬空
// 通过ap对象访问资源时就会出现问题。
AutoPtr<Date> copy(ap);
ap->_year = 2018;
return 0;
}
2)unique_ptr
auto_ptr中的方式,显然存在很大的缺陷,因此C++11中又提供了unique_ptr
unique_ptr的解决方式简单粗暴,既然在智能指针中拷贝构造会出现问题,unique_ptr干脆直接防拷贝。
// 模拟实现一份简答的UniquePtr,了解原理
template<class T>
class UniquePtr
{
public:
UniquePtr(T * ptr = nullptr)
: _ptr(ptr)
{}
~UniquePtr()
{
if(_ptr)
delete _ptr;
}
T& operator*() {return *_ptr;}
T* operator->() {return _ptr;}
private:
// C++98防拷贝的方式:只声明不实现+声明成私有
UniquePtr(UniquePtr<T> const &);
UniquePtr & operator=(UniquePtr<T> const &);
// C++11防拷贝的方式:delete
UniquePtr(UniquePtr<T> const &) = delete;
UniquePtr & operator=(UniquePtr<T> const &) = delete;
private:
T * _ptr;
};
3)shared_ptr
虽然说前边的两种方式都一定程度上解决了拷贝的问题,但是还是不够完美,因此C++11中又提供了shared_ptr。
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
- shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
template<class T>
class sharedPtr
{
private:
T* _ptr; //
int* pcount;//引用计数
mutex* pmutex; //互斥锁
public:
sharedPtr(T* ptr = nullptr)
:_ptr(ptr),
pcount(new int(1))
, pmutex(new mutex)
{}
//拷贝构造和赋值运算符重载
sharedPtr(const sharedPtr<T>& sp)
:_ptr(sp._ptr)
, pcount(sp.pcount)
, pmutex(sp.pmutex)
{
pmutex->lock();
(*pcount)++;
pmutex->unlock();
}
sharedPtr<T>& operator=(const sharedPtr<T>& sp)
{
//不能自己给自己赋值
if (_ptr != sp._ptr)
{
//释放管理的旧资源
Release();
//拷贝新资源
_ptr = sp._ptr;
pcount = sp.pcount;
pmutex = sp.pmutex;
pmutex->lock();
(*pcount)++;
pmutex->unlock();
}
return *this;
}
//指针一样的行为
T& operator*()
{
return *_ptr;
}
sharedPtr<T>* operator->()
{
return _ptr;
}
~sharedPtr()
{
Release();
}
private:
void Release()
{
//锁要最后释放,因此必须有个标志
bool flag = false;
//释放资源
pmutex->lock();
if (--(*pcount) == 0)
{
delete _ptr;
delete pcount;
flag = true;
}
pmutex->unlock();
if (flag == true)
delete pmutex;
}
};
1)shred_ptr线程安全问题
shared_ptr中_ptr和pcount都是new出来的,多个对象同时操作就有可能出现线程安全问题,但是对pcount的操作都是加锁的,因此pcount不会产生线程安全问题。但是,_ptr每办法加锁,因此多个线程对_ptr操作可能会产生想爱你城安全问题。
void SharePtrFunc(SharedPtr<Date>& sp, size_t n)
{
cout << sp.Get() << endl;
for (size_t i = 0; i < n; ++i)
{
// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
SharedPtr<Date> copy(sp);
// 这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n次,但是最终看到的结果,并一定是加了2n
copy->_year++;
copy->_month++;
copy->_day++;
}
}
int main()
{
SharedPtr<Date> p(new Date);
cout << p.Get() << endl;
const size_t n = 100;
thread t1(SharePtrFunc, p, n);
thread t2(SharePtrFunc, p, n);
t1.join();
t2.join();
cout << p->_year << endl;
cout << p->_month << endl;
cout << p->_day << endl;
return 0;
}
2)循环引用
- 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的引用计数。
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;
}
注意:RAII除了可以用来设计智能指针,还可以用来设计守卫锁,防止异常安全导致的死锁问题。例如unipue_lock和lock_guard
以上是关于C++11---智能指针的主要内容,如果未能解决你的问题,请参考以下文章