什么是智能指针
c++中没有垃圾回收机制,所以很容易在疏忽的情况下造成内存泄漏的问题,为了解决这种情况,所以有了RAII,通过智能指针对于资源进行自动的释放。
智能指针的原理
RAII是一种利用对象的生命周期来控制资源的一种技术。
- 在对象构造时获取资源,使之在对象的生命周期中对于资源的控制访问始终有效,在析构时将资源进行释放
- 实际上是将一份资源交给一个对象进行托管
好处:
- 不需要显示的进行资源的释放
- 对象所需的资源在其生命周期中始终有效
智能指针的原理:
- RAII的特性
- 具有指针一样的行为,需要重载operator* 与 operator->
实现一个简单的智能指针
template<class T>
class Smart_ptr{
public:
Smart_ptr(T* ptr = nullptr):_ptr(ptr){}
~Smart_ptr(){
if(_ptr != nullptr)
delete _ptr;
}
T& operator*(){
return *_ptr;
}
T* operator->(){
return _ptr;
}
private:
T* _ptr;
}
- 创建的智能指针是一个对象,必须通过重载* 与 -> 使其具有和指针一样的行为
使用
struct Date{
int _year;
int _month;
int _day;
};
int main(){
Smatr_ptr<int> sp1(new int);
*sp1 = 20;
Smatr_ptr<Date> sp2(new Date);
sp2->_year = 2019;
//这里本来应该是sp2->->_year = 2019;
//但是在语法上为了增加可读性所以就省略了一个->
}
c++ 库中的智能指针
- c++中的智能指针都定义在这个库中
auto_ptr
- 在c++98中提供了auto_ptr智能指针
- auto_ptr 实现原理:管理权转移
- auto_ptr拷贝或赋值之后前面的对象就会悬空,就会无法通过之前的对象访问到资源
- 当发生拷贝或者赋值时就会将资源的管理转移到拷贝或赋值的新对象上去,原对象不再指向资源,防止被两次释放而导致的程序崩溃
跟据auto_ptr 的原理实现一个简单的auto_ptr
template<class T>
class Auto_ptr{
public:
Auto_ptr(T* ptr):_ptr(ptr){}
~Auto_ptr(){
if(_ptr){
delete _ptr;
}
}
Auto_ptr(Auto_ptr<T>& ap):_ptr(ap._ptr){
ap._ptr = nullptr;
}
Auto_ptr& operator=(Auto_ptr<T>& ap){
if(this != &ap){
if(_ptr){
delete _ptr; //释放当前资源
}
_ptr = ap._ptr;
ap._ptr = nullptr; //管理权转移后,资源清空,不会因为两个指
// 针清理同一块资源而导致程序崩溃,但会导致之前的对象悬空,无法使用其再次访问资源
}
}
T* operator->(){
return _ptr;
}
T& operator*(){
return *_ptr;
}
private:
T* _ptr;
}
- auto_ptr 因为悬空的问题所以不常用,是具有一定缺陷的
unique_ptr
- c++11所提供的智能指针
- 实现原理:防拷贝,通过防止拷贝和赋值,解决auto_ptr中对象悬空的问题
unique简单的模拟实现,主要为了解其原理
templtae<class T>
class Unique_ptr{
public:
Unique_ptr(T* ptr = nullptr):_ptr(ptr){}
~Unique_ptr(){
if(_ptr){
delete _ptr;
}
}
T* operator->(){
return _ptr;
}
T& operator*(){
return *_ptr;
}
private:
T* _ptr;
//c++98中的方式,声明成私有,只声明不实现
Unique_ptr(Unique_ptr<T> const &);
Unique_ptr& operator=(Unique_ptr<T> const &);
//c++11中使用已删除函数
Unique_ptr(Unique_ptr<T> const &) = delete;
Unique_ptr& operator=(Unique_ptr<T> const &) = delete;
}
shared_ptr
- c++11 中提供的可靠的支持拷贝的智能指针
shared_ptr的原理
- share_ptr通过引用计数的原理实现多个share_ptr之间的资源共享
- 在shared_ptr的内部,每个资源都维护了一份引用计数,用来记录;该资源被几个对象共享
- 在对象的析构函数调用时,就说明该对象不再使用该资源了,就将资源的引用计数减一
- 如果引用计数是0, 就说明该对象是最后一个使用该资源的,所以就要将该资源释放
- 如果应用计数不是0,就说明还有其他的对象在使用该资源,如果释放就会是其他对象称为野指针
通过实现简单的shered_ptr 了解其原理
#include <iostream>
#include <thread>
#include <mutex>
template<class T>
class Shared_ptr{
public:
Shared_ptr(T* ptr = nullptr):_ptr(ptr), _refcount(new int(1)), _lock(new std::mutex){
if(_ptr == nullptr){
*_refcount = 0;
}
}
~Shared_ptr(){
Realese();
}
Shared_ptr(Shared_ptr<T>& sp):_ptr(sp._ptr), _refcount(sp._refcount), _lock(sp._lock){
//不为空时才增加引用计数
if(_ptr != nullptr)
AddrefCount();
}
Shared_ptr& operator=(Shared_ptr<T>& sp){
//判断是否自己给自己赋值
if(this == &sp){
Realese(); //释放旧的管理的资源
_ptr = sp._ptr;
_refcount = sp._refcount;
_lock = sp._lock;
if(_ptr)
AddrefCount();
}
}
T* operator->(){
return _ptr;
}
T& operator*(){
return *_ptr;
}
int Get(){
return *_refcount;
}
private:
T* _ptr; //指向资源的指针
int* _refcount; //引用计数,开辟在堆上,通过一个指针实现多个对象之间的共享
std::mutex* _lock; //互斥锁,保护引用计数,使用同一资源的对象使用同一把锁
void Realese(){
SubrefCount();//引用计数减一
if(_ptr && _refcount == 0){
delete _ptr;
delete _lock;
delete _refcount;
}
if(_ptr == nullptr){
delete _lock;
delete _refcount;
}
}
void SubrefCount(){
_lock->lock();
--(*_refcount);
_lock->unlock();
}
void AddrefCount(){
_lock->lock();
++(*_refcount);
_lock->unlock();
}
};
shared_ptr 中的线程安全问题
- 引用计数是该管理该资源所有对象所共享的,开辟在堆上
- ++ – 操作并不是原子操作,而引用计数属于临界资源,多线程中可能会造成线程安全问题,所以需要互斥锁的保护
shared_ptr的循环引用
struct ListNode{
int _data;
Shared_ptr<ListNode> _next;
Shared_ptr<ListNode> _prev;
~ListNode(){
cout <<" ~ListNode()" << endl;
}
};
int main(){
//循环引用
Shared_ptr<ListNode> node1(new ListNode);
Shared_ptr<ListNode> node2(new ListNode);
cout << node1.Get() << endl;
cout << node2.Get() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.Get() << endl;
cout << node2.Get() << endl;
return 0;
}
结果
[wens@localhost shared_ptr]$ ./shared_ptr
1
1
2
2
分析:
- node1 与 node2 两个指针对象指向两个节点 ,引用计数加一
- node1->_next 指向 node2 ,使node2的引用计数变为2
- node2->_prev 指向 node1 ,使node1的引用计数变为2
- 当node1 与node2 析构时,两个结点的引用计数分别减一
- 但_next还指向上一个节点,_prev还指向上一个结点
- 只有_prev析构了node1就释放了,_next析构了node2就释放了,但_next属于node1 ,_prev属于node2,这样都等着对方释放,就时循环引用
#####如何解决循环引用
- 在应用计数的场景下,节点_prev 与 _next 改为weak_ptr
struct ListNode
{
int _data;
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
}
- weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象
- weak_ptr是一种弱引用关系,而shared_ptr 是一种强引用关系
- weak_ptr 在功能上类似于普通指针, 然而一个比较大的区别是, 弱引用能检测到所管理的对象是否已经被释放, 从而避免访问非法内存。
- weak_ptr 不对内存的资源及进行管理,不会增加引用计数,可以检测到资源的状态
- weak_ptr 是为了解决shared_ptr中的循环引用,只能处理已经知道的循环引用即在编译期间处理,程序运行期间出现的循环引用则可能导致内存泄漏
仿函数删除器
- 可以释放多种资源,不仅可以释放new出来的还可以释放其他的
template<class T>
struct FreeFunc {
void operator()(T* ptr){
cout << "free:" << ptr << endl;
free(ptr);
}
};
template<class T>
struct DeleteArrayFunc {
void operator()(T* ptr){
cout << "delete[]" << ptr << endl;
delete[] ptr;
}
};
int main(){
FreeFunc<int> freeFunc;
shared_ptr<int> sp1((int*)malloc(4), freeFunc);
DeleteArrayFunc<int> deleteArrayFunc;
shared_ptr<int> sp2(new int[10], deleteArrayFunc);
return 0;
}
- c++ boost中给出了更实用的scoped_ptr 和 shared_ptr 和 weak_ptr
- C++ 11,引入了unique_ptr和shared_ptr和weak_ptr
- unique_ptr 对应的时boost中的scoped_ptr
使用RAII思想的lock_guard
- 除了释放内存资源,利用RAII还可以防止造成死锁
//简单的模拟了解其原理
template <Mutex>
class LockGuard{
public:
LockGuard(Mutex& mtx)
:_mutex(mtx){
_mutex.lock();
}
~LockGuard(){
_mutex.unlock();
}
LockGuard(const LockGuard<Mutex>&) = delete;
private:
//必须使用引用,否则锁的及不是一个互斥量对象
Mutex& _mutex;
};
- lock_guard 可以自动上锁解锁,当创建好锁所对象后上锁,析构时解锁
- 在lock_guard对象的生命周期内会保持一直上锁的状态
- lock_guard对象的生命周期并不影响Mutex锁对象的生命周期,只负责上锁解锁