智能指针详解
前言:本篇博客介绍C++中的四个智能指针auto_ptr、shared_ptr、weak_ptr、 unique_ptr
。其中,auto_ptr
存在很大的缺陷,被C++11
弃用。
我们为什么要使用智能指针呢?
C++
的内存管理是让很多事都需要程序员自己去处理,例如:当我们写一个new
语句时,就一定要存在对应的delete
语句去释放资源,但是我们不能避免程序还未执行到delete
时就跳转
了或者在函数中没有执行到最后的delete
语句就返回了,如果我们不在每一个可能跳转或者返回的语句前释放资源
,就会造成内存泄露
。使用智能指针
可以很大程度上的避免
这个问题,因为智能指针
实质上就是一个类,当超出了类的作用域
是,类会自动调用析构函数
,析构函数会自动释放资源
。
了解RAII: RAII
(Resource Acquisition Is Initialization)是一种利用对象生命周期
来控制程序资源
(如内存、文件句 柄、网络连接、互斥量等等)的简单技术。在对象构造时获取资源
,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源
。RAII实质上是把管理一份资源的责任托管给了一个对象。
//RAII,利用对象的生命周期管理资源
//下面的类只体现了智能,没有体现指针,要体现指针,必须重载*和->
template<class T>
class SmartPtr
{
public:
//构造时获取获取资源
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{}
//析构时释放资源
~SmartPtr()
{
if (_ptr)
delete _ptr;
}
private:
T* _ptr;
};
使用RAII管理资源有一些好处:
- 不需要显式地
释放
资源。 - 对象所需的资源在其
生命期
内始终保持有效
什么是智能指针?
首先要声明的是RAII
并不是智能指针,RAII
只是利用对象声明周期管理资源的一种技术,要为只能指针,必须要让该对象具备指针的特性,也就是可以解引用和->
。
模拟实现一个简单的智能指针:
template<class T>
class SmartPtr
{
public:
//构造时获取获取资源
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{}
//析构时释放资源
~SmartPtr()
{
if (_ptr)
delete _ptr;
}
//像指针一样
//重载operator*和operator->
T& operator*()
{
return *_ptr;
}
//当该指针指向结构体时
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
//test
struct Date
{
int _year;
int _month;
int _date;
};
int main()
{
SmartPtr<Date> ptr(new Date);
//相当于ptr.operator->()->_year = 2018;
//这里为了提高可读性省略了一个->
ptr->_year = 2019;
ptr->_month = 1;
ptr->_date = 1;
return 0;
}
auto_ptr指针: C++98
版本的库中就提供了auto_ptr
的智能指针。但是由于其存在一些不能接受的bug
,所以现在已经弃用,但是我们仍然应该学习它的思想。auto_ptr的原理是独占所有权,管理权转移,任何时候只能存在一个对象
管理资源。
根据库中的代码模拟实现一份简单的auto_ptr学习原理:
//模拟实现auto_ptr
template<class T>
class AutoPtr
{
public:
AutoPtr(T* ptr)
:_ptr(ptr)
{}
~AutoPtr()
{
if (_ptr)
delete _ptr;
}
//拷贝赋值(管理权转移,永远只能保持一个对象去管理资源)
//公司明确规定不能使用auto_ptr
//缺陷:使得第一个对象悬空
AutoPtr(AutoPtr<T>& ap)
{
_ptr = ap._ptr;
ap._ptr = nullptr;
}
//重载赋值
AutoPtr& operator=(AutoPtr<T>& sp)
{
//防止自己给自己拷贝
if (this != &sp)
{
//先释放本对像管理的资源
if (_ptr)
{
delete _ptr;
}
//赋值
_ptr = sp._ptr;
sp._ptr = nullptr;
}
//支持连续赋值
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
从上述代码可以看出auto_ptr
最大的缺陷是当拷贝对象和赋值时,旧的对象已经悬空
,不能对其进行任何操作,否则出现异常。它们在拷贝和赋值时必须要释放掉旧对象管理的资源,因为如果不释放就会是一种浅拷贝,两个指针同时指向一个资源,在释放的时候会释放两次。
unique_ptr: 为了解决auto_ptr
的缺陷,C++11标准库引入了新的unique_ptr。unique_ptr
智能指针的实现原理简单并且粗暴,它的实现原理就是防止拷贝和赋值。
下面模拟实现一个简单的unique_ptr:
template<class T>
class UniquePtr
{
public:
UniquePtr(T* ptr)
:_ptr(ptr)
{}
~UniquePtr()
{
if (_ptr)
delete _ptr;
}
//重载*
T& operator*()
{
return *_ptr;
}
//重载->
T* operator->()
{
return _ptr;
}
private:
//防拷贝和赋值
//C++98写法,声明为私有只声明不实现
UniquePtr(UniquePtr<T>& const up);
UniquePtr& operator=(UniquePtr<T>& const up);
//C++11写法,delete
UniquePtr(UniquePtr<T>& const up) = delete;
UniquePtr& operator=(UniquePtr<T>& const up) = delete;
private:
T* _ptr;
};
shared_ptr指针: 为了解决unique_ptr
不能拷贝的缺陷,C++11标准库又引入了新的支持拷贝和赋值的智能指针shared_ptr
。shared_ptr的原理是通过引用计数的
方式来实现多个shared_ptr
对象之间共享资源。
shared_ptr
在其内部,给每个资源都维护了着一份计数
,用来记录该份资源被几个对象
共享。- 在对象被
销毁
时(也就是析构函数调用),就说明该对象不使用资源
了,对象的引用计数减一
。 - 如果引用计数减到0,就说明自己是
最后一个
使用该资源的对象,在析构
时必须释放该资源 - 如果没有减到0,说明除了自己还有
其他对象
在使用该份资源,不能释放该资源,否则其他对象就变成野指针
。
根据库中的代码模拟实现一份简单的shared_ptr:
template<class T>
class SharedPtr
{
public:
SharedPtr(T* ptr = nullptr)
:_ptr(ptr)
, _count(new int(1))
, _lock(new mutex())
{
if (ptr == nullptr)
*_count = 0;
}
//利用互斥量实现原子性的++操作
int AddRefCount()
{
_lock->lock();
++(*_count);
_lock->unlock();
return *_count;
}
//实现原子性的--操作
int SubRefCount()
{
_lock->lock();
--(*_count);
_lock->unlock();
return *_count;
}
~SharedPtr()
{
Release();
}
SharedPtr(const SharedPtr<T>& sp)
:_ptr(sp._ptr)
, _count(sp._count)
, _lock(sp._lock)
{
if (_ptr)
AddRefCount();
}
SharedPtr<T>& operator=(const SharedPtr<T>& sp)
{
//避免自己给自己赋值
if (_ptr != sp._ptr)
{
//释放当前对象管理的资源
Release();
_ptr = sp._ptr;
_count = sp._count;
AddRefCount();
}
//支持连续赋值
return *this;
}
//像指针一样
//重载operator*
T& operator*()
{
return *_ptr;
}
//重载->
T* operator->()
{
return _ptr;
}
//返回引用计数
int UseCount()
{
return *_count;
}
private:
void Release()
{
if (_ptr && SubRefCount() == 0)
{
delete _ptr;
delete _count;
delete _lock;
}
}
private:
T* _ptr;//维护资源的指针
int* _count;//引用计数必须是在栈上,这样才可以保证拷贝或赋值的多个对象使用一个公共的引用计数
//也不能为静态的变量,静态的对象是所有对象共享
mutex* _lock;//互斥锁,保护引用计数,因为++和--不是一个原子性的操作
};
shared_ptr的线程安全问题:
智能指针对象中引用计数
是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或--
,这 个操作不是原子的,引用计数原来是1
,++了两次
,可能还是2
,这样引用计数就错了。会导致资源未释放或者多次释放的问题。所以只能指针中引用计数++、--
是需要加锁的,也就是说unique_ptr
引用计数的操作是线程安全
的。 但是智能指针管理的对象存放在堆
上,两个线程中同时去访问,会导致线程安全
问题。
void Test(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->_date++;
}
}
int main()
{
SharedPtr<Date> sp(new Date);
sp->_year = 2019;
sp->_month = 1;
sp->_date = 1;
cout << sp.Get() << endl;
const size_t n = 2;
thread t1(Test, sp, n);
thread t2(Test, sp, n);
t1.join();
t2.join();
cout << sp->_year << endl;
cout << sp->_month << endl;
cout << sp->_date << endl;
return 0;
}
总结:
- shared_ptr
本身引用计数
是线程安全的 - shared_ptr管理的
资源
不是线程安全的
shared_ptr的循环引用问题:
struct Node
{
int _data;
shared_ptr<Node> _prev;
shared_ptr<Node> _next;
~Node(){ cout << "~Node()" << endl; }
};
int main()
{
shared_ptr<Node> node1(new Node);
shared_ptr<Node> node2(new Node);
//ues_count == 1
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
//ues_count == 2
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
node1和node2
两个智能指针对象指向两个节点,引用计数变成1
,不需要手动delete
。node1
的_next
指向node2
,node2
的_prev
指向node1
,它们的引用计数变成2
。node1
和node2
析构,引用计数减到1
,但是node1->_next
还指向node2
节点,node2->_prev
还指向node1。- 当
_next
析构时,node1
释放资源,当_prev
析构时。node2
释放资源。但是_next
属于node1
的成员,node1
释放了,_next
才会析构,而node1
由_prev
管理,_prev
属于node2
成员,所以这就叫循环引用
,谁也不会释放。
解决shared_ptr循环引用问题: 在引用计数的场景下,把节点中的_prev
和_next
改成weak_ptr
就可以了 。原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加 node1和node2的引用计数,所以不会造成循环引用的问题。
struct Node
{
int _data;
weak_ptr<Node> _prev;
weak_ptr<Node> _next;
~Node(){ cout << "~Node()" << endl; }
};
int main()
{
shared_ptr<Node> node1(new Node);
shared_ptr<Node> node2(new Node);
//ues_count == 1
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
//ues_count == 1
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
代码运行结果:
如果不是new出来的空间,而是malloc出来的空间,可以通过shared_ptr设计的仿函数删除器来释放:
template<class T>
//仿函数:重载(),使得对象能够像函数一样使用
//仿函数删除器
struct FreeFunc
{
void operator()(T* ptr)
{
cout << "free:" << ptr << endl;
free(ptr);
}
};
template<class T>
struct DeleteFunc
{
void operator()(T* ptr)
{
cout << "delete:" << ptr << endl;
delete(ptr);
}
};
int main()
{
FreeFunc<int> fc;
shared_ptr<int> sp1((int*)malloc(4), fc);
DeleteFunc<int> dc;
shared_ptr<int> sp2(new int(4), dc);
return 0;
}
代码运行结果:
RAII可以设计守卫锁,防止异常安全导致的死锁问题:
template<class Mutex>
class LockGuard
{
public:
LockGuard(Mutex& mutex)
:_mutex(mutex)
{
_mutex.lock();
}
~LockGuard()
{
_mutex.unlock();
}
private:
LockGuard(const LockGuard<Mutex>&) = delete;
Mutex& _mutex;//必须要加引用,否则锁住的不是同一个互斥量对象
};
mutex _mtx;
static int n = 0;
void Func()
{
for(size_t i = 0; i< 10000; i++)
{
LockGuard<mutex> lock(_mtx);
n++;//保护n
}
}
int main()
{
int begin = clock();
thread t1(Func);
thread t2(Func);
t1.join();
t2.join();
int end = clock();
cout << n << endl;//20000
cout << "time:" << end-begin << endl;
return 0;
}
利用对象的生命周期
来控制互斥锁,防止在其他异常的情况下没有释放锁
的情况。