智能指针解决的问题
- malloc 出来的空间,没有进行释放,出现内存泄漏,在C++中这个问题极其严重
- 异常安全问题。如果在malloc和free之间抛异常,程序没有执行完毕就退出也会造成内存泄漏,这种问题引发的叫做异常安全问题。
智能指针的使用及原理
原理:智能指针是基于RAII思想,利用对象生命周期来管理和释放资源(内存,文件句柄,互斥量等)。
我们来解释一下这句话:
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。
优点:
- 不用显示的释放资源
- 保证了在对象生命周期内所需的资源始终有效
怎么来使用呢?举个栗子:
template <class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
delete _ptr;
}
private:
T* _ptr;
};
int main()
{
int* tmp = (int*)malloc(sizeof(int));
SmartPtr<int> sp(tmp); //给tmp找了个对象,一直管到他 狗带
//dosomething
return 0;
}
但是这个还不算是智能指针。
既然是指针,那么就必须可以支持指针的操作。指针可以解引用,也可以通过->去
访问所指空间中的内容如,因此,我们还得重载 * 和 ->.
我们完善一下上面的操作:
template <class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
T& operator *()
{
return *_ptr;
}
T* operator ->()
{
return _ptr;
}
~SmartPtr()
{
if (_ptr)
delete _ptr;
}
private:
T* _ptr;
};
struct Date
{
int year;
int month;
int day;
};
int main()
{
SmartPtr<int> s(new int);
*s = 666;
cout << *s << endl;
SmartPtr<Date> sp(new Date);
sp->year = 2019;
sp->month = 1;
sp->day = 29;
cout << sp->year << "." << sp->month << "." << sp->day << endl;
return 0;
}
常见的智能指针类型
- auto_ptr
- unique_ptr
- shared_ptr
详解各种智能指针
一、auto_ptr(C++98) 智能指针的败笔
关于auto_ptr的介绍我就不说了,感兴趣请移步auto_ptr官方文档。
auto_ptr思想就是将管理权转移。
有了上面的核心概念我们来模拟实现一下auto_ptr:
template <class T>
class AutoPtr
{
public:
AutoPtr(T* ptr=nullptr)
{}
AutoPtr(AutoPtr<T>& ap) //将原来的资源转移到新的对象,跟自己脱离关系
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
AutoPtr<T>& operator=(AutoPtr<T> ap)
{
//确认是不是自己给自己赋值
if (this != &ap)
{
//释放当前对象资源
if (_ptr)
delete _ptr;
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~AutoPtr()
{
if (_ptr)
delete _ptr;
}
private:
T* _ptr;
};
struct Date
{
int year;
int month;
int day;
};
int main()
{
AutoPtr<Date> ap(new Date);
//这里在拷贝的时候将ap资源置空,导致再次访问资源的时候指针被悬空,内存访问异常,这就是auto_ptr失败的原因
AutoPtr<Date> copy(ap);
//copy->month = 1;
ap->year = 2019;
cout << copy->month << ap->year << endl;
return 0;
}
可以看出来,这个程序是运行不了的,这也是智能指针设计之初的一个败笔,一定不要去用这个,这是个反例。
二、 unique_ptr(C++11) auto_ptr的升级版,比较靠谱的指针
设计原理 : 直接防止拷贝和赋值,简单粗暴
具体操作看代码:
class UniquePtr
{
public:
UniquePtr(T* ptr=nullptr)
{}
~UniquePtr()
{
if (_ptr)
delete _ptr;
}
private:
//C++98防拷贝方式,将重载赋值运算符和拷贝构造设置为私有
UniquePtr& operator =(UniquePtr<T>& up);
UniquePtr(UniquePtr<T>const& up);
//C++11防拷贝方式
UniquePtr& operator =(UniquePtr<T>& up)=delete;
UniquePtr(UniquePtr<T>const& up)=delete;
private:
T* _ptr;
};
三、shared_ptr 更靠谱并且能够拷贝的方法
原理: 利用引用计数的方法实现多个对象之间共享数据。
思想:
- shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了
看看代码:
template <class T>
class SharedPtr
{
public:
SharedPtr(T* ptr=nullptr)
:_ptr(ptr)
,_pCount(new int(1))
,_pMutex(new mutex)
{
if (_ptr == nullptr)
{
*_pCount = 0;
}
}
SharedPtr(const SharedPtr<T>& sp)
:_ptr(sp._ptr)
, _pCount(sp._pCount)
, _pMutex(sp._pMutex)
{
//如果是一个空指针对象,则不用加引用计数,否则就要将 pCount++
if (_ptr)
AddCount();
}
SharedPtr<T>&operator=(const SharedPtr<T>& sp)
{
if (_ptr != sp._ptr)
{
//释放旧空间
Release();
_ptr = sp._ptr;
_pCount = sp._pCount;
_pMutex = sp._pMutex;
if (_ptr)
AddCount();
}
return *this;
}
int UseCount()
{
return *_pCount;
}
int AddCount()
{
_pMutex->lock();
++(*_pCount);
_pMutex->unlock();
return *_pCount;
}
int SubCount()
{
_pMutex->lock();
--(*_pCount);
_pMutex->unlock();
return *_pCount;
}
void Release()
{
if (_ptr && 0 == SubCount())
{
delete _ptr;
delete _pCount;
delete _pMutex;
}
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
~SharedPtr()
{
Release();
}
private:
T* _ptr;
int* _pCount; //引用计数
mutex* _pMutex; //互斥锁
};
struct Date
{
int year;
int month;
int day;
};
int main()
{
SharedPtr<int> sp1(new int(10));
SharedPtr<int> sp2(sp1);
*sp2 = 20;
cout << sp1.UseCount() << endl;
cout << sp2.UseCount() << endl;
SharedPtr<int> sp3(new int(10));
sp2 = sp3;
cout << sp1.UseCount() << endl;
cout << sp2.UseCount() << endl;
cout << sp3.UseCount() << endl;
sp1 = sp3;
cout << sp1.UseCount() << endl;
cout << sp2.UseCount() << endl;
cout << sp3.UseCount() << endl;
return 0;
}
结果如下:
shared_ptr中的线程安全问题
注意: shared_ptr本身是线程安全的,除了下面两种情况
- 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时 ++或–,这个操作不是原子 的,引用计数原来是1,++了两次,可能还是2。这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作是线程安全的。
- 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题
举个栗子:
struct Date
{
int _year;
int _month;
int _day;
};
void SharePtrFunc(shared_ptr<Date>& sp, size_t n)
{
cout << sp.get() << endl;
for (size_t i = 0; i < n; ++i)
{
// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
shared_ptr<Date> copy(sp);
// 这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n次,但是最终看到的结果,并一定是加了2n
copy->_year++;
copy->_month++;
copy->_day++;
}
}
int main()
{
shared_ptr<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;
}
智能指针引发的循环引用问题
先看段代码:
struct ListNode
{
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;
}
很明显,这个代码是有问题的,我们的ListNode本应该被析构,结果却没有,这是为什么呢?我们来分析一下:
- 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就可以了。
struct ListNode
{
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
原因:
- 析构函数不释放资源,不遵循 RAII,
- 没有增加引用计数,
- 专门辅助解决shared_ptr循环引用的缺陷
好了,到这里C++智能指针部分就基本完成了,有什么疑问欢迎留言。