1. 智能指针的使用场景
下面程序中我们可以看到,new了以后,我们也delete了,但是因为抛异常导,后面的delete没有得到执行,所以就内存泄漏了,所以我们需要new以后捕获异常,捕获到异常后delete内存,再把异常抛出,但是因为new本身也可能抛异常,连续的两个new和下面的Divide都可能会抛异常,让我们处理起来麻烦。智能指针放到这样的场景里面就让问题简单多了。
#include <iostream>
using namespace std;
double Divide(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Divide by zero condition!";
}
else
{
return (double)a / (double)b;
}
}
void Func()
{
// 这⾥可以看到如果发⽣除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;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
2. RAII和智能指针的设计思路
RAII是Resource Acquisition Is Initialization的缩写 ,他是⼀种管理资源的类的设计思想 ,本质是⼀种利用对象生命周期来管理获取到的动态资源,避免资源泄漏 ,这里的资源可以是内存、文件指针、网络连接、互斥锁等等。RAII在获取资源时把资源委托给⼀个对象 ,接着控制对资源的访问, 资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。
智能指针类除了满足RAII的设计思路,还要方便资源的访问,所以智能指针类还会想迭代器类⼀
样,重载 operator*/operator->/operator[] 等运算符,方便访问资源。
template<class T>
class smart_ptr
{
public:
smart_ptr(T* ptr) :_ptr(ptr)
{}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t i)
{
return _ptr[i];
}
~smart_ptr()
{
delete[] _ptr;
}
private:
T* _ptr;
};
void Func()
{
这⾥使⽤RAII的智能指针类管理new出来的数组以后,程序简单多了
smart_ptr<int> p1 = new int[10];
smart_ptr<pair<string,int>> p2 = new pair<string, int>[10];
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
3. C++标准库智能指针的使用
C++标准库中的智能指针都在<memory>这个头文件下面,我们包含<memory>就可以是使用了,
智能指针有好几种,除了weak_ptr他们都符合RAII和像指针⼀样访问的行为,原理上而言主要是解
决智能指针拷贝时的思路不同。
> auto_ptr
auto_ptr是C++98时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给
拷贝对象 ,这是⼀个非常糟糕的设计,因为他会到被拷贝对象悬空,访问报错的问题,C++11设计
出新的智能指针后,强烈建议不要使用auto_ptr。其他C++11出来之前很多公司也是明令禁止使用
这个智能指针的。
#include <iostream>
#include <memory>
using namespace std;
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
~Date()
{
cout << "~Date()" << endl;
}
};
int main()
{
auto_ptr<Date> p1(new Date);
auto_ptr<Date> p2 = p1;
return 0;
}

被拷贝对象p1直接就为空了,p1是一个左值,如果再访问程序就会报错 !!
> unique_ptr
unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯⼀指针,他的特点的不支持拷贝,只支持移动。如果不需要拷贝的场景就非常建议使用他。
unique_ptr<Date> p1(new Date);
//无法拷贝
//unique_ptr<Date> p2 = p1;
//可以move移动构造转移资源
unique_ptr<Date> p2(move(p1));
虽然move移动转移资源后,p1是空的,但是p1本就是左值,使用者显然是知道move可能会带来的后果的。所以,move左值的时候一定要谨慎。
下面简单封装一下unique_ptr:
#include <iostream>
#include <memory>
using namespace std;
namespace my_unique_ptr
{
template<class T>
class unique_ptr
{
public:
//删除拷贝构造和拷贝赋值
unique_ptr(const unique_ptr<T>& uptr) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& uptr) = delete;
//构造函数
explicit unique_ptr(T* ptr) :_ptr(ptr)
{}
//移动构造
unique_ptr(unique_ptr<T>&& uptr) :_ptr(uptr._ptr)
{
uptr._ptr = nullptr;
}
//移动赋值
unique_ptr<T>& operator=(const unique_ptr<T>&& uptr)
{
//释放当前资源
if (_ptr) delete _ptr;
_ptr = uptr._ptr;
uptr._ptr = nullptr;
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t i)
{
return _ptr[i];
}
const T& operator[](size_t i) const
{
return _ptr[i];
}
~unique_ptr()
{
if(_ptr)
{
delete[] _ptr;
cout << "~unique_ptr()" << endl;
}
}
private:
T* _ptr;
};
}
int main()
{
my_unique_ptr::unique_ptr<int> p1(new int[10]);
my_unique_ptr::unique_ptr<int> p2(new int[10]);
for (int i = 0; i < 10; i++)
{
p1[i] = i;
p2[i] = i;
}
my_unique_ptr::unique_ptr<int> p3(move(p1));
my_unique_ptr::unique_ptr<int> p4 = move(p2);
for (int i = 0; i < 10; i++)
{
cout << p3[i] << " ";
}
cout << endl;
return 0;
}
> shared_ptr
shared_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是支持拷贝, 也支持移动 。如果需要拷贝的场景就需要使用他了。底层是用引用计数的方式实现的。
我们先来简单使用一下:
int main()
{
shared_ptr<int> sp1(new int(10));
shared_ptr<int> sp2(new int[10]);
//支持拷贝
shared_ptr<int> sp3 = sp1;
return 0;
}
我们知道智能指针如果支持拷贝,那一定是浅拷贝 。因为智能指针的本质是方便我们访问和管理我们new的资源 。如果是深拷贝,那就违背智能指针的初衷了!!不过,浅拷贝又会引发两个问题:多个指针管理同一片资源,资源的释放时机,资源的释放次数 。所以,我们要确保这份资源只析构一次,并且只有在没有指针管理这份资源的时候才释放!!
因此,在share_ptr的底层中使用了引用计数的方式解决这些问题!!原理就是,用一个计数器管理一篇资源,计数器的数目代表有多少个指针管理这片资源。那我们该怎么实现呢 ??在share_ptr中,我们new一个count,用这个count来维护。这里不能使用static的原因就是静态变量是属于整个类实例化出来的对象的,因此只有一个计数器是无法管理多份资源的!!
下面简单封装了一下share_ptr:
template<class T>
class share_ptr
{
public:
//构造
share_ptr(T* ptr)
:_ptr(ptr),
_pcnt(new int(1))
{
}
//析构
~share_ptr()
{
if (--(*_pcnt) == 0)
{
delete _ptr;
delete _pcnt;
}
}
//拷贝构造
share_ptr(const share_ptr<T>& sptr)
:_ptr(sptr._ptr)
, _pcnt(sptr._pcnt)
{
++(*_pcnt);
}
//拷贝赋值
share_ptr<T>& operator=(const share_ptr<T>& sptr)
{
// 如果指向同一份资源就不要赋值了
if (_ptr != sptr._ptr)
{
//当前引用计数--
if (--(*_pcnt) == 0)
{
delete _ptr;
delete _pcnt;
}
//更改资源
_ptr = sptr._ptr;
_pcnt = sptr._pcnt;
(*_pcnt)++;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t i)
{
return _ptr[i];
}
private:
T* _ptr;
int* _pcnt;
};
> 删除器
无论是unique_ptr还是share_ptr在默认析构时都是使用delete释放资源的 ,所以如果智能指针管理的资源不是new出来的,程序在释放资源时就会崩溃。
//这样写程序就会崩溃
shared_ptr<Date> sp1(new Date[10]);
unique_ptr<Date> sp2(new Date[10]);
为了解决这个问题,shared_ptr支持在构造的时候给定一个删除器,这个删除器本质就是一个可调用对象 ,比如:仿函数,函数指针,lambda表达式 ......而我们的智能指针在大多数情况下用的都是new所以就特化了一个delete[]的版本,我们只需要如下传递模版参数即可,很方便。
shared_ptr<Date[]> sp1(new Date[10]);
unique_ptr<Date[]> sp2(new Date[10]);
而传删除器是一种通用的方法【这里直接传lambda表达式是真的香】。
shared_ptr<Date[]> sp1(new Date[10], [](Date* ptr) {
delete[] ptr;
});
shared_ptr<FILE> sp3(fopen("code1.cpp", "r"),[](FILE* ptr) {
fclose(ptr);
});
对于特化的new来说,unique_ptr和shared_ptr使用删除器都一样,但是如果是其他情况就不一样了。因为unique_ptr和shared_ptr对删除器的设计有所不同,shared_ptr是在类内部定义的构造函数模版,我们直接传对象编译器就会自动推导类型 。但是,unique_ptr却是在类模版多定义了一个模版参数来支持删除器 。所以,在使用unique_ptr传删除器的时候,使用仿函数类型会比较方便,因为仿函数类型可以直接定义对象。但是lambda表达式和函数指针是不能直接定义对象的,所以还是要在后面传递一个实例化的对象。
//仿函数
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
//函数指针
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
//lambda
auto delArrOBJ = [](Date* ptr) {delete[] ptr; };
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
下面我们就自己实现一下shared_ptr的删除器:
template<class T>
class shared_ptr
{
public:
//构造(没有传删除器就走这个构造)
shared_ptr(T* ptr)
:_ptr(ptr),
_pcnt(new int(1))
{}
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr),
_pcnt(new int(1)),
_del(del)
{}
//析构
~shared_ptr()
{
if (--(*_pcnt) == 0)
{
_del(_ptr);
delete _pcnt;
}
}
//拷贝构造
shared_ptr(const shared_ptr<T>& sptr)
:_ptr(sptr._ptr)
, _pcnt(sptr._pcnt)
{
++(*_pcnt);
}
//拷贝赋值
shared_ptr<T>& operator=(const shared_ptr<T>& sptr)
{
// 如果指向同一份资源就不要赋值了
if (_ptr != sptr._ptr)
{
//当前引用计数--
if (--(*_pcnt) == 0)
{
delete _ptr;
delete _pcnt;
}
//更改资源
_ptr = sptr._ptr;
_pcnt = sptr._pcnt;
(*_pcnt)++;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t i)
{
return _ptr[i];
}
private:
T* _ptr;
int* _pcnt;
function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
}
> 使用的小细节
• shared_ptr 除了支持用指向资源的指针构造,还支持 make_shared 用初始化资源对象的值直接构造。
• shared_ptr 和 unique_ptr 都支持了operator bool的类型转换,如果智能指针对象是⼀个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否空。
• shared_ptr 和 unique_ptr 都得构造函数都使用explicit【 在编程语言中,"explicit"作为关键字,表示需要显式转换的数据类型,需手动调用转换函数,而非自动转换**】** 修饰,防止普通指针隐式类型转换成智能指针对象。
4. shared_ptr和weak_ptr
4.1 shared_ptr循环引用问题
shared_ptr在大多数资源管理的场景下都可以很好的解决,但是在循环引用的场景下却会发生资源无法释放,内存泄漏的问题!
在如下场景就会造成循环引用,导致内存泄漏!!
struct ListNode
{
int _data;
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
// 循环引⽤ -- 内存泄露
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
n1->_next = n2;
n2->_prev = n1;
return 0;
}

在上图的分析中**,next管理着prev指向的资源,而prev也管理着next指向的资源** 。但是,它们互相指向,也就是说,它们之中一定要有一个释放的话,其条件都是对方要先释放 。可是,它们之间相互依赖,彼此制衡,形成一个环状的资源释放链,最终导致它们之间谁都无法释放!!
4.2 weak_ptr
为了解决上述问题,C++11中就引入了weak_ptr。weak_ptr既不支持RAII【资源请求立即初始化】,也不支持访问资源 。所以,我们看文档时发现weak_ptr不支持构造时绑定资源,只支持绑定到share_ptr ,但是绑定到share_ptr时并不增加计数器的数目 。因此,weak_ptr可以解决上述问题。
struct ListNode
{
int _data;
//std::shared_ptr<listnode> _next;
//std::shared_ptr<listnode> _prev;
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
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;
cout << wp.expired() << endl;
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;
auto sp3 = wp.lock();
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
*sp3 += "###";
cout << *sp1 << endl;
return 0;
}