C++11 智能指针
- 一、智能指针的使用场景分析
- 二、RAII和智能指针的设计思路
- 三、智能指针的本质及衍生的问题
- 四、C++标准库的智能指针的使用
- 五、智能指针的原理(模拟实现)
- 1. auto_ptr的模拟实现
- 2. unique_ptr的模拟实现
- 3. shared_ptr的模拟实现(简单版)
- 六、定制删除器
- 1. 默认delete释放资源的不足
- 2. 定制删除器的类型
- 3. 比较完整的shared_ptr的实现
- 七、shared_ptr循环引用问题和weak_ptr
- 1. 问题的产生
- 2. 分析原因
- 3. 解决问题
- 4. weak_ptr的原理
- 4.1 weak_ptr的简单实现
- 4.2 weak_ptr的一些成员函数
一、智能指针的使用场景分析
在我们需要动态申请内存时,难免最后会有忘记释放内存的时候,这就导致了内存泄漏。在使用到异常时,某个函数抛出异常后,很可能前面申请的空间也未释放,因此也导致内存泄漏。
例如:
场景1:普通情况下申请空间后忘记释放
void test() {
int* p = new int(10);
double* pp = new double(1.1);
}
int main() {
test();
return 0;
}
场景2:抛出异常后,申请的空间无法释放
下⾯程序中我们可以看到,new了以后,我们也delete了,但是因为抛异常导,后⾯的delete没有得到执⾏,所以就内存泄漏了,所以我们需要new以后捕获异常,捕获到异常后delete内存,再把异常抛出,但是因为new本⾝也可能抛异常,连续的两个new和下⾯的Divide都可能会抛异常,让我们处理起来很⿇烦。智能指针放到这样的场景⾥⾯就让问题简单多了
double Divide(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Divide by zero condition!";
}
else
{
return (double)a / (double)b;
}
}
void test()
{
// 这⾥可以看到如果发⽣除0错误抛出异常,另外下⾯的array和array2没有得到释放。
// 所以这⾥捕获异常后并不处理异常,异常还是交给外⾯处理,这⾥捕获了再重新抛出去。
// 但是如果array2new的时候抛异常呢,就还需要套⼀层捕获释放逻辑,这⾥更好解决⽅案
// 是智能指针。
int* array1 = new int[10];
int* array2 = new int[10]; // 抛异常呢
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (...)
{
cout << "delete []" << array1 << endl;
cout << "delete []" << array2 << endl;
delete[] array1;
delete[] array2;
throw; // 异常重新抛出,捕获到什么抛出什么
}
// ...
cout << "delete []" << array1 << endl;
delete[] array1;
cout << "delete []" << array2 << endl;
delete[] array2;
}
二、RAII和智能指针的设计思路
RAII是Resource Acquisition Is Initialization的缩写,他是⼀种管理资源的类的设计思想,本质是
⼀种利⽤对象⽣命周期来管理获取到的动态资源,避免资源泄漏。RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问,资源在对象的⽣命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。
智能指针类除了满⾜RAII的设计思路,还要⽅便资源的访问,所以智能指针类还会想迭代器类⼀
样,重载 operator*/operator->/operator[] 等运算符,⽅便访问资源。
设计一个简单的智能指针解决上面抛异常导致资源无法释放的问题:
template<class T>
struct SmartPtr {
SmartPtr(T* ptr) :_ptr(ptr) {
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
T& operator[](size_t pos) {
return *(_ptr + pos);
}
~SmartPtr() {
cout << "delete[] ..." << endl;
delete[] _ptr;
}
T* _ptr = nullptr;
};
double Divide(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Divide by zero condition!";
}
else
{
return (double)a / (double)b;
}
}
void Func()
{
SmartPtr<int> sp1 = new int[10];
SmartPtr<int> sp2 = new int[10];
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
//不需要再手动释放资源
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
运行结果:
三、智能指针的本质及衍生的问题
四、C++标准库的智能指针的使用
C++标准库中的智能指针都在< memory >这个头⽂件下⾯,我们包含< memory >就可以是使⽤了,智能指针有好⼏种,除了weak_ptr他们都符合RAII和像指针⼀样访问的⾏为,它们的区别在原理上⽽⾔主要是解决智能指针拷⻉时的思路不同。
-
auto_ptr是C++98时设计出来的智能指针,他的特点是拷⻉时把被拷⻉对象的资源的管理权转移给拷⻉对象,这是⼀个⾮常糟糕的设计,因为它会让被拷⻉对象悬空,访问报错的问题,C++11设计出新的智能指针后,强烈建议不要使⽤auto_ptr。
-
unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯⼀指针,他的特点的不⽀持拷
⻉,只⽀持移动(即,将被移动的指针进行资源交换,通常被移动的指针会被置空)。如果不需要拷⻉的场景就⾮常建议使⽤他。
-
shared_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是⽀持拷⻉,也⽀持移动。如果需要拷⻉的场景就需要使⽤他了。底层是⽤引⽤计数的⽅式实现的。
shared_ptr也支持移动:
- shared_ptr 除了⽀持⽤指向资源的指针构造,还⽀持 make_shared ⽤初始化资源对象的值
直接构造。
shared_ptr的多种构造方式如下:
struct Person {
int _age;
string _name;
};
void test2() {
shared_ptr<Person> sp1(new Person({ 19,"lwx" }));
shared_ptr<Person> sp2 = make_shared<Person>(29,"hlp");
auto sp3 = make_shared<Person>(39, "lpo");
shared_ptr<Person> sp4;
//不可以,报错!!!
shared_ptr<Person> sp5 = new Person({ 29,"cda" });
}
- shared_ptr 和 unique_ptr 都⽀持了operator bool的类型转换,如果智能指针对象是⼀个
空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否为空。
五、智能指针的原理(模拟实现)
1. auto_ptr的模拟实现
template<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;
};
2. unique_ptr的模拟实现
template<class T>
class unique_ptr
{
public:
explicit unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针⼀样使⽤
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//不支持拷贝的做法:
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
unique_ptr(unique_ptr<T>&& sp) //移动构造
:_ptr(sp._ptr)
{
sp._ptr = nullptr;
}
unique_ptr<T>& operator=(unique_ptr<T>&& sp) //移动赋值
{
delete _ptr;
_ptr = sp._ptr;
sp._ptr = nullptr;
}
private:
T* _ptr;
};
3. shared_ptr的模拟实现(简单版)
template<class T>
class shared_ptr {
public:
//默认构造与有参构造
shared_ptr(T* ptr = nullptr)
:_ptr(ptr), _pcount(new int(1)) {
}
//拷贝构造
shared_ptr(const shared_ptr<T>& sp) {
_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
}
//拷贝赋值
void operator= (const shared_ptr<T>& sp) {
if (_ptr != sp._ptr) { //忽略自己给自己赋值
// 和本身就已经共同管理一块空间的对象之间的赋值
if ((*_pcount)-- == 1) { //本对象原本是一块资源的最后管理者
//(即使不是,也做到了减去了一个管理者)
delete _ptr; //释放原来的资源
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
}
}
~shared_ptr() {
if ((*_pcount)-- == 1) { //如果(*_pcount)==1,说明本对象
//是某块资源的最后管理者,判断完后要--
cout << "delete _ptr..." << endl;
delete _ptr;
delete _pcount;
}
}
//使用:
T* get() const
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
T& operator[](size_t pos) {
return _ptr[pos];
}
private:
T* _ptr;
int* _pcount;
};
注意:计数器不能用一个静态成员来充当!!!
六、定制删除器
1. 默认delete释放资源的不足
对于上面简单版的shared_ptr,一般场景下的使用是没问题的,但是如果资源是new[] 出来的就会发生错误,因为new[] 要与delete[] 搭配。
标准库的解决方案是特化出delete[]的版本。
使用:
std::shared_ptr<int[]> sp(new int[10]);
但是,假如这个指针要管理的资源是一个文件呢?文件最终不是被delete的,而是用fclose()关闭文件。如:
std::shared_ptr<FILE> sp(fopen("test.cpp","w"));
这个场景下,普通的shared_ptr是无法解决问题的,即文件打开了,确无法自动关闭。
因此,智能指针⽀持在构造时给⼀个删除器,所谓删除器本质就是⼀个可调⽤对象,这个可调⽤对象中实现你想要的释放资源的⽅式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调⽤删除器去释放资源。
标准库里shared_ptr支持定制删除器:
注: 第一个模版参数U是资源的类型,第二个模版参数D是删除器的类型。
p是资源的指针,del是一个对象。
2. 定制删除器的类型
删除器的类型可以有很多种,可以是仿函数,可以是函数指针,也可以是lambda表达式。
- 例如:仿函数版本的删除器:
struct Fclose {
void operator()(FILE* ptr) {
fclose(ptr);
cout << "fclose..." << endl;
}
};
void test3() {
std::shared_ptr<FILE> sp(fopen("test.cpp", "w"),Fclose());
}
该例子的调用逻辑解析:
- 再例如:以lambda表达式为类型的定制删除器:
3. 比较完整的shared_ptr的实现
template<class T>
class shared_ptr {
public:
//默认构造与有参构造
explicit shared_ptr(T* ptr = nullptr)
:_ptr(ptr), _pcount(new int(1)) {
}
//注:
//这里如果构造时参数只有一个,就走上面的构造,如果
//有两个就走下面这个:
template<class D>
shared_ptr(T* ptr, const D& del)
: _ptr(ptr),
_del(del),
_pcount(new int(1)) {
}
//拷贝构造
shared_ptr(const shared_ptr<T>& sp) {
_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
}
//拷贝赋值
void operator= (const shared_ptr<T>& sp) {
if (_ptr != sp._ptr) { //忽略自己给自己赋值
// 和本身就已经共同管理一块空间的对象之间的赋值
if ((*_pcount)-- == 1) { //本对象原本是一块资源的最后管理者
//(即使不是,也做到了减去了一个管理者)
delete _ptr; //释放原来的资源
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
}
}
~shared_ptr() {
if ((*_pcount)-- == 1) { //如果(*_pcount)==1,说明本对象
//是某块资源的最后管理者,判断完后要--
cout << "delete _ptr..." << endl;
//delete _ptr;
//用删除器:
_del(_ptr);
delete _pcount;
}
}
//使用:
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
T& operator[](size_t pos) {
return _ptr[pos];
}
private:
T* _ptr;
int* _pcount;
//包装器封装_del (因为这里无法拿到D来定义_del):
function<void(T*)> _del = [](T* ptr) {delete ptr; };
//这里默认给_del赋值为一个lambda表达式对象是为了迎合
//当只有一个参数构造时(简单来说就是普通的new空间时,
// 因为走的是上面的普通构造,没有构造出_del),
//也一样可以析构时用删除器_del。
};
注:不支持下面这种构造方法的原因:
七、shared_ptr循环引用问题和weak_ptr
shared_ptr⼤多数情况下管理资源⾮常合适,⽀持RAII,也⽀持拷⻉。但是在循环引⽤的场景下会
导致资源没得到释放内存泄漏,所以我们要认识循环引⽤的场景和资源没释放的原因,并且学会使
⽤weak_ptr解决这种问题。
1. 问题的产生
例如:有下面这样一个场景:
连接成双向链表:
修改成:
结果发生内存泄漏:
2. 分析原因
3. 解决问题
把ListNode结构体中的_next和_prev的类型改成weak_ptr,weak_ptr指向shared_ptr所管理的资源时不会增加它的引⽤计数,因此_next和_prev不参与资源释放管理逻辑,就成功打破了循环引⽤,解决了这⾥的问题。
4. weak_ptr的原理
weak_ptr不⽀持RAII,也不⽀持访问资源,所以weak_ptr构造时不⽀持绑定到资源,只⽀持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引⽤计数,那么就可以解决上述的循环引⽤问题。
4.1 weak_ptr的简单实现
注:
- 这里是对标准库的weak_ptr做了很大的阉割了,库里的实现更为复杂。
- weak_ptr不需要析构函数,因为它不需要管理和释放资源。
- 标准库里的weak_ptr也是有计数器的,因为即使weak_ptr它不管理资源,但是它也应该知道这块资源有几个管理者。
template<class T>
class weak_ptr {
public:
weak_ptr()
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
void operator=(const shared_ptr<T>& sp){
_ptr(sp.get());
}
//不需要析构函数,因为它不需要管理和释放资源
private:
T* _ptr=nullptr;
};
4.2 weak_ptr的一些成员函数
- weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的
shared_ptr已经释放了资源,那么他去访问资源就是很危险的。 - weak_ptr有expired成员函数去检查指向的资源是否过期,use_count也可获取shared_ptr的引⽤计数。
- weak_ptr想访问资源时,可以调⽤lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。
int main() {
std::shared_ptr<string> sp1(new string("111111"));
std::shared_ptr<string> sp2(sp1);
std::weak_ptr<string> wp = sp1;
//1.检查wp所指向的资源是否过期:过期返回1,未过期返回0:
cout << wp.expired() << endl;
//2.查看wp指向的资源有几个shared_ptr对象在管理:
cout << wp.use_count() << endl;
// sp1和sp2都指向了其他资源,则weak_ptr就过期了
sp1 = make_shared<string>("222222");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
sp2 = make_shared<string>("333333");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
wp = sp1;
//std::shared_ptr<string> sp3 = wp.lock();
//将sp1的资源锁住,并交给另一个shared_ptr对象(sp3):
auto sp3 = wp.lock();
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
*sp3 += "###";
cout << *sp1 << endl;
return 0;
}