智能指针
- 1 内存泄漏问题
- 2 RAII(Resource Acquisition Is Initialization)
- 2.1 使用RAII思想设计的SmartPtr类
- 2.2 智能指针的原理
- 2.3 小总结智能指针原理
- 3 智能指针的拷贝问题
- 3.1 std::auto_ptr
- 3.2 std::unique_ptr
- 3.3 std::shared_ptr
- 3.3.1 拷贝构造函数
- 3.3.2 赋值函数
- 3.4 std::shared_ptr的线程安全问题
- 3.4 .1智能指针引用计数操作加锁
- 3.4.2 外界内存资源是非线程安全的
- 3.5 std::shared_ptr 引发的循环引用问题
- 4 std::weak_ptr 解决循环引用问题
- 4.1 weak_ptr解决循环引用的原理
- 4.2 简易版weak_ptr的实现
- 5 删除器
- 6 C++11和boost中智能指针的关系
- 7 总结
1 内存泄漏问题
在讲智能指针之前,我们先来看下面这段代码有什么问题?
int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}void Func()
{// 1、如果p1这里new 抛异常会如何?// 2、如果p2这里new 抛异常会如何?// 3、如果div调用这里又会抛异常会如何?int* p1 = new int;int* p2 = new int;cout << div() << endl;delete p1;delete p2;
}int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}
这段代码有内存泄漏问题。
1.对p1来说,如果new空间失败了,它会抛出异常直接跳转到main函数进行处理。所以,如果只是p1抛异常不会造成内存泄漏问题。
2.如果p1申请空间成功,对p2来说,如果new空间失败了,它也会抛出异常直接跳转到main函数进行处理,但是p1的内存空间并没有得到释放,会造成内存泄漏问题。
未使用智能指针的解决方式如下:
int* p1 = new int;int* p2 = nullptr;try{p2 = new int;}catch(...)//表示捕获任意异常{delete p1;//p2抛异常的话,在这里释放掉p1占的内存throw;//重新抛出异常给main函数}cout << div() << endl;delete p1;delete p2;
3.假如p1,p2都申请内存成功,并未抛出异常。但是div()函数抛出了异常,直接跳转到main函数,会导致p1和p2的内存空间都没有释放,造成内存泄漏。
未使用智能指针的解决方式如下:
int* p1 = new int;int* p2 = nullptr;try{p2 = new int;}catch(...)//表示捕获任意异常{delete p1;throw;}try {cout << div() << endl;}catch (...){delete p1;delete p2;throw;}delete p1;delete p2;
}
以上的的解决方法虽然解决了抛异常可能会造成的内存泄漏问题,但是从代码上来说太过繁琐,不够优雅。
可以用RAII的思想来解决问题。
2 RAII(Resource Acquisition Is Initialization)
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
1.不需要显式地释放资源。
2.采用这种方式,对象所需的资源在其生命期内始终保持有效。
2.1 使用RAII思想设计的SmartPtr类
首先先设计一个SmartPtr类,类中有构造函数和析构函数,如果想申请空间和销毁空间,分别交给构造函数和析构函数即可。
template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){delete _ptr;}
private:T* _ptr;
};
完整代码如下:
template <class T>
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){delete _ptr;}
private:T* _ptr;
};
int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}void Func()
{SmartPtr<int> p1(new int);//申请空间,因为有析构函数出了作用域自动释放SmartPtr<int> p2(new int);cout << div() << endl;
}int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}
2.2 智能指针的原理
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。
template <class T>
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;
};
使用案例:
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;
};
struct Date //为了方便演示定义一个日期类
{int year;int month;int day;
};
int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}void Func()
{SmartPtr<int> p1(new int);*p1 = 10;cout << *p1 << endl;SmartPtr<Date> p2(new Date);//这里本来应该是p2.operator->()->year=2023;//语法上可以省略括号:p2->->year=2023;//为了语法可读性,可以写成p2->year=2023;p2->year = 2023;p2->month = 7;p2->day = 4;cout << p2->year << " " << p2->month << " " << p2->day << " " << endl;cout << div() << endl;
}int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}
2.3 小总结智能指针原理
RAII特性
重载operator*和opertaor->,具有像指针一样的行为。
3 智能指针的拷贝问题
在智能指针的基础上,来看一下下面这段代码:
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;
};
int main()
{SmartPtr<int> sp1(new int(1));SmartPtr<int> sp2(sp1);return 0;
}
运行该代码,程序会直接崩溃,原因是同一块空间,析构了两次。
原因是这里的默认的拷贝构造函数是浅拷贝,但是这里不能用深拷贝来解决问题,因为C++原本的指针赋值就是浅拷贝。
C++提供以下这些库来解决浅拷贝问题
3.1 std::auto_ptr
C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。
auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份bit::auto_ptr来了解它的原理
std::auto_ptr官方文档
// C++98 管理权转移 auto_ptrtemplate<class T>class auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& sp):_ptr(sp._ptr){// 管理权转移sp._ptr = nullptr;//把被拷贝的指针置空了}auto_ptr<T>& operator=(auto_ptr<T>& ap){// 检测是否为自己给自己赋值if (this != &ap){// 释放当前对象中资源if (_ptr)delete _ptr;// 转移ap中资源到当前对象中_ptr = ap._ptr;ap._ptr = NULL;}return *this;}~auto_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}}// 像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};// 结论:auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr
int main()
{std::auto_ptr<int> sp1(new int);std::auto_ptr<int> sp2(sp1); // 管理权转移
// sp1被置空*sp2 = 10;cout << *sp2 << endl;cout << *sp1 << endl;return 0;
//}
主要的思想就是为了防止浅拷贝带来的重复析构同一块空间问题,auto_ptr这个库在处理的时候,拷贝完成后,就把被拷贝的指针给置空了,这样就不会出现重复析构了问题了。
但是这种方式显然是不合理的。
接下来重点介绍以下三个库
3.2 std::unique_ptr
C++11中开始提供更靠谱的unique_ptr
std::unique_ptr官方文档
C++11库才更新智能指针实现
C++11出来之前,boost搞出了更好用scoped_ptr/shared_ptr/weak_ptr
C++11将boost库中智能指针精华部分吸收了过来C++11->unique_ptr/shared_ptr/weak_ptr
unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份UniquePtr来了解它的原理
template<class T>class unique_ptr{
public:unique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}}// 像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}//因为类里面会默认写出拷贝构造函数和赋值函数,//所以这里要手动声明一下//后面加个 =delete 就是防止被拷贝的意思unique_ptr(const unique_ptr<T>&sp) = delete;unique_ptr<T>& operator=(const unique_ptr<T>&sp) = delete;
private:T* _ptr;
};
这里直接用库里的测试一下
确实是不让拷贝
3.3 std::shared_ptr
C++11中开始提供更靠谱的并且支持拷贝的shared_ptr
std::shared_ptr官方文档
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
由于要使用到引用计数,如果有多个对象指向同一块空间,这里我们叫做空间a,需要统计有几个对象。
这里采用的方案是再开辟一块空间给引用计数,这块空间我们叫做空间b,当空间a被一个对象指向时,空间b就计数++,当一个对象不再指向该空间时,空间b就减1。当减到0时,说明这是最后一个对象指向空间a了,可以析构了。这样就避免了重复析构的问题。
如下图演示:
当有新增对象指向_ptr时,_count++;
当sp2不再指向_ptr时,_count-=1;
当sp1不再指向_ptr时,_count-=1,此时_count==0,可以析构了。
3.3.1 拷贝构造函数
这里拷贝构造函数代码分析如上图所示
template<class T>class my_shared_ptr{public:my_shared_ptr(T* ptr):_ptr(ptr),_count(new int(1)){}my_shared_ptr(const my_shared_ptr<T>& sp):_ptr(sp._ptr),_count(sp._count){add();//(*_count)++}void release()//--(*_count)并判断是否等于0{if (--(*_count) == 0){delete _ptr;delete _count;}}void add(){(*_count)++;}~my_shared_ptr(){release();//--(*_count)==0,才能析构}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;int* _count;//开辟一块引用计数的空间};
}
3.3.2 赋值函数
假如现在sp1,sp2,sp3指向了同一块空间,sp4指向另一空间。
现在要求赋值
sp1=sp4或者sp4=sp1
分析如下:
sp1=sp4
就是说sp1指向了sp4的空间。那么原来sp1指向的_count就要减去1
然后把sp1指向sp4即可,同时右边的_count要++
sp4=sp1
就是sp4指向的空间改为sp1指向的空间,然后sp4的_count-=1,由于_count==0,所以要析构掉图片右边的两个空间。左边_count++
代码如下:
my_shared_ptr<T>& operator=(my_shared_ptr<T>& sp){if (_ptr != sp._ptr){release();_ptr = sp._ptr;_count = sp._count;add();}return *this;}void release(){if (--(*_count) == 0){delete _ptr;delete _count;}}void add(){(*_count)++;}
3.4 std::shared_ptr的线程安全问题
接下来测试shared_ptr的线程安全问题。需要注意的是shared_ptr的线程安全分为两方面:
- 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时
++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错
乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁
的,也就是说引用计数的操作是线程安全的。- 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。
3.4 .1智能指针引用计数操作加锁
namespace ffg
{template<class T>class my_shared_ptr{public:my_shared_ptr(T* ptr):_ptr(ptr),_count(new int(1)),_mtx(new mutex){}my_shared_ptr(const my_shared_ptr<T>& sp):_ptr(sp._ptr),_count(sp._count),_mtx(sp._mtx){add();}my_shared_ptr<T>& operator=(my_shared_ptr<T>& sp){if (_ptr != sp._ptr){release();_ptr = sp._ptr;_count = sp._count;_mtx = sp._mtx;add();}return *this;}void release()//凡是涉及到++,--的操作都需要加锁处理。注意这里锁的内存空间释放问题{//不能提前delete _mtx,因为_count可能还大于0_mtx->lock();bool flag = false;if (--(*_count) == 0){delete _ptr;delete _count;flag = true;}_mtx->unlock();if (flag){delete _mtx;}}void add()//加锁{_mtx->lock();(*_count)++;_mtx->unlock();}~my_shared_ptr(){release();}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get() const{return _ptr;}int use_count(){return *_count;}private:T* _ptr;int* _count;mutex* _mtx;};
}
测试代码:
3.4.2 外界内存资源是非线程安全的
外界资源未加锁前:
struct Date
{int _year = 0;int _month = 0;int _day = 0;
};
void SharePtrFunc(ffg::my_shared_ptr<Date>& sp, size_t n, mutex& mtx)
{cout << sp.get() << endl;for (size_t i = 0; i < n; ++i){// 这里智能指针拷贝会++计数,智能指针析构会--计数,加锁了,这里是线程安全的。//但是管理的资源并不是线程安全的,外界需要自己加锁处理,所以不是线程安全的。ffg::my_shared_ptr<Date> copy(sp);//mtx.lock();sp->_year++;sp->_month++;sp->_day++;//mtx.unlock();}
}
int main()
{ffg::my_shared_ptr<Date> p(new Date);cout << p.get() << endl;const size_t n = 10000;mutex mtx;thread t1(SharePtrFunc, std::ref(p), n, std::ref(mtx));thread t2(SharePtrFunc, std::ref(p), n, std::ref(mtx));t1.join();t2.join();cout << p->_year << endl;cout << p->_month << endl;cout << p->_day << endl;cout << p.use_count() << endl;return 0;
}
这里的输出应该p->_year,p->_month,p->_day都应该是20000,但是由于线程安全问题,可能会导致结果出现错误。
所以,需要程序员在外界加上锁。
void SharePtrFunc(ffg::my_shared_ptr<Date>& sp, size_t n, mutex& mtx)
{cout << sp.get() << endl;for (size_t i = 0; i < n; ++i){// 这里智能指针拷贝会++计数,智能指针析构会--计数,加锁了,这里是线程安全的。//但是管理的资源并不是线程安全的,外界需要自己加锁处理,所以不是线程安全的。ffg::my_shared_ptr<Date> copy(sp);mtx.lock();sp->_year++;sp->_month++;sp->_day++;mtx.unlock();}
}
int main()
{ffg::my_shared_ptr<Date> p(new Date);cout << p.get() << endl;const size_t n = 10000;mutex mtx;thread t1(SharePtrFunc, std::ref(p), n, std::ref(mtx));thread t2(SharePtrFunc, std::ref(p), n, std::ref(mtx));t1.join();t2.join();cout << p->_year << endl;cout << p->_month << endl;cout << p->_day << endl;cout << p.use_count() << endl;return 0;
}
多次执行:
直接使用C++库的shared_ptr也同理。
3.5 std::shared_ptr 引发的循环引用问题
先来看以下这两段代码:
建立了两个链表节点,n1,n2相互连接
struct ListNode
{ffg::my_shared_ptr<ListNode> _pre;ffg::my_shared_ptr<ListNode> _next;~ListNode(){cout << "~ListNode" << endl;}
};int main()
{ffg::my_shared_ptr<ListNode> n1(new ListNode);ffg::my_shared_ptr<ListNode> n2(new ListNode);n1->_next = n2;//这里注释掉任意一行都能成功析构//n2->_pre = n1;return 0;
}
上面这段代码,在main函数里的
当有n1->_next=n2,或者n2->_pre=n1,不同时存在的时候n1,n2就能成功析构。
但是当这两行代码同时存在的时候,就不能成功析构。
先来分析以下第一种情况
这里假设是
n1->_next = n2;
如下图,当n2和左边的_next 指向右边的ListNode空间时,引用计数为2
然后n2析构了以后,右边的引用计数为1。
接下来到n1析构,引用计数为0,左边的_next随着ListNode的析构也被释放掉了,所以右边的引用计数也变为0,右边的ListNode也被析构了,_prve也不存在了。
接下来分析第二种情况,也就是循环引用的问题
n1->_next = n2;
n2->_pre = n1;
首先是n2先析构,右边的引用计数变为1
接下来是n1析构,左边的引用计数变为1
此时循环引用问题就来了。
n2和n1析构,引用计数都减到1,但是_next还是指向下一个节点,_prve还是指向上一个节点。
也就是说_next析构了,右边的ListNode就能释放,右边的_prev也能释放。
_prev析构了,左边的ListNode就能释放,左边的_next也能释放掉。
所以这两个ListNode互相牵制了,谁也不会释放。
左边的释放条件是右边释放,右边释放条件是左边释放。相互矛盾了。
这就是循环引用问题。
4 std::weak_ptr 解决循环引用问题
注意:
1.weak_ptr是专门设计出来,解决shard_ptr的循环引用问题。
2.它不是常规的智能指针,不支持RAII,不支持资源管理
3.支持像指针一样的功能
这里先使用库里面的weak_ptr看看效果
成功解决了循环引用问题。
4.1 weak_ptr解决循环引用的原理
原理很简单,就是:n1->_next = n2;和n2->_prev = n1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。
4.2 简易版weak_ptr的实现
class weak_ptr{public:weak_ptr():_ptr(nullptr){}weak_ptr(const my_shared_ptr<T>& sp):_ptr(sp.get()){}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get() const{return _ptr;}private:T* _ptr;};
}
测试:
struct ListNode
{ffg::weak_ptr<ListNode> _pre;ffg::weak_ptr<ListNode> _next;~ListNode(){cout << "~ListNode" << endl;}
};int main()
{ffg::my_shared_ptr<ListNode> n1(new ListNode);ffg::my_shared_ptr<ListNode> n2(new ListNode);cout << n1.use_count() << endl;//1cout << n2.use_count() << endl;//1n1->_next = n2;n2->_pre = n1;return 0;
}
从图中可以看出引用计数变为1了,可以说明,_next和_prev不会在增加引用计数,这样就解决了循环引用问题。
5 删除器
如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题。
库里面的删除器测试:
6 C++11和boost中智能指针的关系
- C++ 98 中产生了第一个智能指针auto_ptr.
- C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
- C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
- C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost 的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。
7 总结
智能指针一开始是为了解决某些情况下因抛异常而导致内存泄漏问题。
由RAII思想设计的智能指针,会管理资源的内存和释放。它支持像指针一样的功能
但是从而由智能指针引发了诸如拷贝问题,线程安全问题,循环引用问题。
智能指针是线程安全的,因为库里设计的时候加锁处理了,但是外界的资源不是线程安全的,需要程序员处理。
一开始的C++98库中的auto_ptr解决拷贝问题是不合理的,于是有了unique_ptr(强行让智能指针不能做拷贝),shared_ptr(引用计数解决拷贝问题中的重复析构),weak_ptr是配合shared_ptr使用了,解决了循环引用问题。