引言
普通指针使用时存在挂起引用以及内存泄漏的问题,C++ 11中引入了智能指针来解决它
std::unique_ptr
std::auto_ptr,时代的眼泪
std::unique_ptr
是std::auto_ptr
的替代品,解决了C++ 11之前std::auto_ptr
的很多缺漏
简单的看一下std::auto_ptr
的复制构造函数
template<typename T>
class auto_ptr {
public:
//Codes..
auto_ptr(auto_ptr& atp) {
m_ptr = atp.m_ptr;
atp.m_ptr = nullptr;
}
private:
T* m_ptr;
};
可以很容易的看出,该函数将指针所有权从一个对象转移到另外一个对象,且将原对象置空。该函数中std::auto_ptr
实际上在运用引用去实现移动语义。但若是在转移所有权后仍去访问前一个对象(现在已经被置为空指针),程序会崩溃。
std::auto_ptr<int> atp(new int(10));
std::auto_ptr<int> atp2(atp);
// auto _data = atp.data; // undefined behavior
// 此时的atp已经为nullptr,因为“移动函数”将所有权转接给了另外一个对象
u_ptr的移动构造
在C++ 11中引入了右值引用,可在此基础上实现移动构造函数。如今std::auto_ptr
在C++17中被移除,正式被std::unique_ptr
替代
// unique_ptr的移动构造函数
unique_ptr(unique_ptr&& unip) : m_ptr(unip.m_ptr)
{
unip.m_ptr = nullptr;
}
虽然说这个函数与std::auto_ptr
中的做法一样,但它只能接受右值作为参数。传递右值,即为转换指针所有权
std::unique_ptr<int> a(new int(10));
// std::unique_ptr<int> b(a); // error
std::unique_ptr<int> b(std::move(a)); // ok
u_ptr的构造方式
与std::shared_ptr
不同,该智能指针用于不能被多个实例共享的内存管理,也就是说,仅有一个实例拥有内存所有权
对于智能指针而言,声明后会默认置为空
建议使用std::make_unique
来替代直接使用构造函数传入new
指针的方式来创建实例。若在new
时期智能指针指向的类(也就是模板接受到的参数)抛出了异常,或是类的构造函数执行失败,这样会导致智能指针无法管理到新分配的内存,导致内存泄露
std::unique_ptr<int> unip1; // 默认置nullptr
std::unique_ptr<int> unip2(new int(5)); // 旧方法
std::unique_ptr<int> unip3 = std::make_unique<int>(10); // 新规范
// std::unique_ptr<int> unip3 = std::make_unique<int>(new int(10)); // 迷惑操作
std::unique_ptr<std::string> unip4 = std::make_unique<std::string>("Pointer");
std::array<int, 10> arr = { 1,2,3,4,5,6,7,8,9,0 };
std::unique_ptr<std::array<int, 10>> unip5 = std::make_unique<std::array<int, 10>>(arr);
需要注意的是,接受指针参数的ctor
是explicit
的,因此我们不能将一个内置指针隐式转换为一个智能指针
template <class _Dx2 = _Dx, _Unique_ptr_enable_default_t<_Dx2> = 0>
explicit unique_ptr(pointer _Ptr) noexcept : _Mypair(_Zero_then_variadic_args_t{}, _Ptr) {}
// std::unique_ptr<int> unip = new int(10); //错误
std::shared_ptr
与std::unique_ptr
都建议采用std::make_
方法创建智能指针对象
u_ptr的unique性
std::unique_ptr
通过删除拷贝构造函数和拷贝赋值函数来确保unique
性,它是move-only的,独占资源
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
可以通过移动语义转移所有权
std::unique_ptr<int> unip = std::make_unique<int>(10);
// std::unique_ptr<int> copy_unip = unip;
// std::unique_ptr<int> copy_unip(unip);
// 移动构造 unip被置空,所有权转到move_unip上
std::unique_ptr<int> move_unip = std::move(unip);
但我们可以“拷贝或是赋值”一份即将销毁的std::unique_ptr
,最常见的例子是从函数返回一个std::unique_ptr
std::unique_ptr<int> clone()
{
std::unique_ptr<int> unip = std::make_unique<int>(10);
// 返回值实际上调用到移动构造
return unip;
}
// 编译器优化为一次移动构造
std::unique_ptr<int> p1 = clone();
std::unique_ptr<int> p2;
// p2调用一次移动赋值 clone调用一次移动构造
p2 = clone();
std::unique_ptr
对象可以传给左值常量引用常数,因为这样并不会改变内存所有权,也可以使用移动语义进行右值传值(注意区分转移所有权和std::move
的区别)
class Test {};
void right_param(std::unique_ptr<Test>&& t) {}
void cref_param(const std::unique_ptr<Test>& t) {}
int main()
{
auto temp = std::make_unique<Test>();
// 错误 无法将右值引用绑定到左值
// right_param(temp);
// 利用std::move强转为右值类型
right_param(std::move(temp));
cref_param(std::move(temp));
cref_param(temp);
}
u_ptr构造的坑点
对于该智能指针来说,应避免犯以下两个错误(std::unique_ptr
与std::shared_ptr
同理)
-
用同一份指针初始化多个智能指针
int *p = new int(10); std::unique_ptr<int> unip(p); std::unique_ptr<int> unip2(p);
拯救方法,释放其中一个智能指针的管理权
unip.release();
-
混用普通指针与智能指针
int *p = new int(10); std::unique_ptr<int> unip(p); delete p; // 离开作用域后unip将会释放一块已经释放的内存
u_ptr的主动弃权与释放
std::unique_ptr
中有get()
,release()
,reset()
方法
get()
可以获得智能指针中管理的资源,返回的是指针release()
会返回指向所管理的资源的指针,并释放其管理权(并不会析构)reset()
会调用构造函数并回收内存,同时reset()
还可以接受一个内置指针
std::unique_ptr<float> unip = std::make_unique<float>(10);
std::unique_ptr<float> unip2 = std::make_unique<float>(20);
// unip.reset(unip2.get()) // 绝对禁止的操作 会导致挂起引用
unip.reset(unip2.release()); // unip析构其管理的内存后 接收unip2转接过来的所有权
// unip = std::move(unip2); // 与reset-release相同
std::make_unique的实现
运用变参模板,简单实现my_make_unique
//不支持处理数组
template<typename T, typename ... Ts>
std::unique_ptr<T> my_make_unique(Ts ... args)
{
return std::unique_ptr<T> {new T{ std::forward<Ts>(args) ... }}; //泛型工厂函数
}
完整的make_unique
实现
// 普通构造函数
template <class _Ty, class... _Types, enable_if_t<!is_array_v<_Ty>, int> = 0>
unique_ptr<_Ty> make_unique(_Types&&... _Args) {
return unique_ptr<_Ty>(new _Ty(_STD forward<_Types>(_Args)...));
}
// std::unique_ptr<T[]>
template <class _Ty, enable_if_t<is_array_v<_Ty> && extent_v<_Ty> == 0, int> = 0>
unique_ptr<_Ty> make_unique(const size_t _Size) {
using _Elem = remove_extent_t<_Ty>;
return unique_ptr<_Ty>(new _Elem[_Size]());
}
// 过滤定长数组
template <class _Ty, class... _Types, enable_if_t<extent_v<_Ty> != 0, int> = 0>
void make_unique(_Types&&...) = delete;
std::make_的缺陷
-
std::make_
函数都不允许使用定制删除器,deleter
只能在std::unique_ptr
或std::shared_ptr
的构造函数中传入 -
std::make_
函数不能完美的传递std::initializer_list
// auto unip = std::make_unique<std::vector<int>>({ 1,2,3 }); // 错误 std::initializer_list<int> il = { 1,2,3 }; auto unip = std::make_unique<std::vector<int>>(il); // 正确
-
当构造函数是私有或者保护时,无法
std::make_
,std::make_
实际上是类似调用到了构造函数。解决方法是将std::make_
声明为友元函数class PrivateCtorClass { friend std::unique_ptr<PrivateCtorClass> std::make_unique<PrivateCtorClass>(); PrivateCtorClass() : data(100) {} public: int data; }; int main() { std::unique_ptr<PrivateCtorClass> up = std::make_unique<PrivateCtorClass>(); std::cout << up->data << std::endl; }
-
对于
std::shared_ptr
来说,对象的内存可能无法及时回收要理解这一点首先得知道
std::make_shared
和直接调用构造函数两者的内存分配原理对于
std::shared_ptr
而言,内部维护了两个指针指向资源块和控制块,前者是智能指针管理的对象,后者用于记录引用次数。如果使用构造函数初始化智能指针,那么控制块是单独分配的,即资源块与控制块处于两个内存中而若采用
std::make_shared
方法,资源块和控制块将分配到同块内存中,而在程序运行中,内存分配是代价高昂的操作,因此std::make_shared
效率更高虽然
std::make_shared
减少了内存分配次数,提高了性能,但是在回收内存的时候存在一点问题:我们知道,当对象的引用计数降为0时(强引用次数降为0,与弱引用次数无关,std::weak_ptr只是个旁观者),对象被销毁(调用析构函数并释放内存)。但是对于资源快与控制块处于同一块内存上的情况而言,由于内存必须完整地申请释放,因此只有当控制块也可以被回收时,这块内存才会被释放。而控制块中包含了两个计数,一个为强引用(
std::shared_ptr
)计数,一个为弱引用(std::weak_ptr)计数,当两个引用计数都归为0时,控制块才能释放。那么也就是说只要还有一个std::weak_ptr
指向控制块(弱引用计数 > 0),控制块就不能被释放,也就代表这块内存也不能被释放(即使std::shared_ptr已经离开作用域了)总的来说,通过
std::make_shared
分配出来的这块内存,只有当最后一个std::shared_ptr
与std::weak_ptr
都被销毁时,才会被释放。下面为验证代码int main() { // 64位的程序才能申请2G大小的内存 std::shared_ptr<char[]> shrp(new char[INT32_MAX]); std::weak_ptr<char[]> wp(shrp); shrp.reset(); // 在等待任意键的时候 shrp管理的资源已经得到释放 程序内存占用很低 system("pause"); }
int main() { // 64位的程序才能申请2G大小的内存 std::shared_ptr<char[]> shrp = std::make_shared<char[]>(INT32_MAX); std::weak_ptr<char[]> wp(shrp); shrp.reset(); // 在等待任意键的时候 shrp管理的资源没有得到释放 程序内存占用高达2G system("pause"); }
std::shared_ptr
std::shared_ptr
与std::unique_ptr
的主要区别在于前者是使用引用计数的智能指针,后者是独占资源的智能指针。强引用计数维护了“引用同一个真实指针对象的智能指针实例”的数目。这意味着,可以有多个std::shared_ptr
实例可以指向同一块动态分配的内存,当最后一个引用对象离开其作用域时,才会释放这块内存(详情请见上文:std::make_
的缺陷)
与std::unique_ptr
不同,std::shared_ptr
支持拷贝构造,也允许拷贝赋值
std::shared_ptr<int> shrp = std::make_shared<int>(10);
std::shared_ptr<int> shrp2 = shrp;
std::shared_ptr<int> shrp3(shrp2);
std::cout << shrp.use_count() << std::endl; // 3
使用std::shared_ptr管理第三方库
假设第三方库有这么一些代码
struct IntData {
int data = 100;
~IntData() { std::cout << "int data destroy" << std::endl; }
};
class OtherLibHandler {
public:
template<typename T>
void* Create() { return new T(); }
template<typename T>
void Release(void *p) { delete reinterpret_cast<T*>(p); }
};
static OtherLibHandler& GetHandle() {
static OtherLibHandler p;
return p;
}
第三方库通常会通过接口提供原始指针,用来管理其中的内存,既然是原始指针,就总会出现忘记使用库中的释放函数,导致内存泄露的情况
void* p = GetHandle().Create<IntData>();
// Codes..
// GetHandle().Release<IntData>(p); //程序员由于不可抗力导致漏写这行代码了
那么使用智能指针去维护就会显得非常方便,以下仅提供代码思路
auto deleter = [](void* p) { GetHandle().Release<IntData>(p); };
std::shared_ptr<void> shrp(GetHandle().Create<IntData>(), deleter);
包装成一个函数
template<typename T>
std::shared_ptr<T> GetShared(void* p) {
std::shared_ptr<T> shrp(reinterpret_cast<T*>(p), [](void* p) { GetHandle().Release<T>(p); });
return shrp;
}
int main() {
std::shared_ptr<Intdata> shrp = GetShared<IntData>(GetHandle().Create<IntData>());
}
但是包装成函数后会有这么一个问题,GetShared
返回出来的智能指针必须得被接下来(不管是左值或是右值引用,或是常量左值引用),否则该对象将会被释放,导致后续代码去访问野指针的内容
void* p = GetHandle().Create<IntData>();
GetShared<IntData>(p);
// 出现不确定的结果
std::cout << reinterpret_cast<IntData*>(p)->data << std::endl;
那我们可以使用宏来确保一定创建一个对象来接住返回值
#define CreateShared_IntData(p) auto shared_##p = GetShared<IntData>(p);
template<typename T>
std::shared_ptr<T> GetShared(void* p) {
std::shared_ptr<T> shrp(reinterpret_cast<T*>(p), [](void* p) { GetHandle().Release<T>(p); });
return shrp;
}
int main() {
auto origin_p = GetHandle().Create<IntData>();
CreateShared_IntData(origin_p);
// 100
std::cout << shared_origin_p->data << std::endl;
}
常用API
- 使用
use_count()
来查看std::shared_ptr
的强引用次数 reset()
则可以释放关联内存块的所有权(相当于std::unique_ptr
的release()
,但它没有返回值 ),如果是最后一个指向该资源的std::shared_ptr
,就释放这块内存。同时reset()
还可以传入一个新的原始指针,来让智能指针管理新的内存- 三种智能指针都有
swap()
,用于交换两个同种类型的指针对象
s_ptr的坑点
上文中提到过我们应该避免用同一份指针初始化多个智能指针
int *p = new int(10);
std::shared_ptr<int> shrp(p);
std::shared_ptr<int> shrp2(p);
//导致同一片地址的资源被释放两次
因为这两个智能指针是独立初始化的,所以它们之间并没有通讯共享引用计数。std::shared_ptr
的内部实际上维护两个指针,一个用于管理实际的资源,另外一个则指向一个控制块,其中记录了有哪些std::shared_ptr
共同管理同一片内存。
// std::shared_ptr的基类_Ptr_base维护了两个指针
private:
element_type* _Ptr{nullptr};
_Ref_count_base* _Rep{nullptr};
这是在初始化完成的,所以如果单独初始化两个对象,尽管管理的是同一块内存,它们各自的”控制块“没有互相记录的。但是如果是使用复制构造函数还有赋值运算时,控制块将会同步更新,这样就达到了引用计数的目的。
智能指针的deleter
智能指针与动态数组
在C++17之前,std::shared_ptr
的构造与std::unique_ptr
存在一点差异:std::unique_ptr
支持动态数组,而std::share_ptr
不支持动态数组
std::unique_ptr<int[]> unip(new int[10]); // 合法
std::shared_ptr<int[]> shrp(new int [10]); // 在C++ 17之前时非法操作 不能传入数组类型作为模板参数
std::shared_ptr<int> shrp2(new int[10]); // 可编译,但存在未定义行为
因此,对于shrp2
而言,在它析构时会发生这种情况
int* p = new int[10];
delete p;
这对于new int[10]
而言肯定是非法的,对它应使用delete[]
自定义deleter-s_ptr
对此我们有两种解决方法,一种是传入std::default_delete
,另外一种是自行构造删除器
std::shared_ptr<int> shrp(new int[10], std::default_delete<int[]>());
// lambda表达式转换为函数指针
std::shared_ptr<int> shrp1(new int[10], [](int* p) { delete[] p; });
void deleter(int* p) { delete[] p; }
std::shared_ptr<int> shrp2(new int[10], deleter);
// 还可以封装一个my_make_shared_dynamic_arr的方法
template<typename T>
decltype(auto) my_make_shared_dynamic_arr(std::size_t size)
{
return std::shared_ptr<T>(new T[size], std::default_delete<T[]>());
}
std::shared_ptr<int> shrp3 = my_make_shared_dynamic_arr(10);
虽然说能用了,但存在着几个缺点
- 我们想管理的是
int[]
类型,但std::shared_ptr
的模板类型却为int
- 需要显示提供删除器
- 无法使用
std::make_shared
,无法保证异常安全 - 由于
std::shared_ptr<int>
没有重载operator[]
,故需要使用.get()[Index]
来获取数组中的元素
有人可能会说了,那使用std::shared_ptr<int*>
不就行了吗,数组退化成指针
其实事情并没有这么简单
int* p = new int[10]{};
std::shared_ptr<int*> shrp(&p, [](int** p) { delete[] *p; });
std::cout << (*(shrp.get()))[5] << std::endl;
自定义deleter-u_ptr
对于std::shared_ptr
来说,自定义一个删除器是比较简单的,而对于std::unique_ptr
来说,情况有点不同
std::shared_ptr<int> shrp(new int(10), [](int* p) { delete p; }); // 正确
// std::unique_ptr<int> unip(new int(10), [](int* p) { delete p; }); // 错误
std::unique_ptr
在指定删除器时需要在模板参数中传入删除器的类型
// 一个接受int*且无返回值的函数指针
std::unique_ptr<int,void(*)(int*)> unip(new int(10), [](int* p) { delete p; });
如果在lambda表达式中捕获了变量,那么需要使用std::function
来进行包装,因为捕获了变量的lambda表达式无法转换为函数指针
// std::unique_ptr<int, void(*)(int*)> unip(new int(10), [&](int* p) {delete p; });
std::unique_ptr<int, std::function<void(int*)>> unip(new int(10), [&](int* p) { delete p; });
除了用lambda表达式,我们还可以这么干
template<typename T>
void deleter(T* p) { delete[] p; }
//使用decltype类型推断
std::unique_ptr<int, decltype(deleter<int>)*> unip(new int(10), deleter<int>);
//使用typedef取别名
typedef void(*deleter_int)(int*);
std::unique_ptr<int, deleter_int> unip(new int(10), deleter<int>);
//联合typedef与decltype
typedef decltype(deleter<int>)* deleter_int_TD;
std::unique_ptr<int, deleter_int_TD> unip(new int(10), deleter<int>);
利用using
+decltype
,间接直观
template<typename T>
void deleter(T* p) { delete[] p; }
template<typename T>
using deleter_t = decltype(deleter<T>)*;
int main() {
std::unique_ptr<int, deleter_t<int>> unip(new int(10), deleter<int>);
}
C++17后的做法
在C++ 17中,std::shared_ptr
支持传入T[]
类型作为模板参数
std::shared_ptr<int[]> shrp(new int[10]{});
std::cout << shrp[3] << std::endl; // 0
在C++20中,可以使用std::make_shared
来创建智能指针
// 分配一个管理有10个int元素的动态数组的std::shared_ptr
std::shared_ptr<int[]> shrp = std::make_shared<int[]>(10);
// 同理也有
std::unique_ptr<int[]> unip = std::unique_ptr<int[]>(10);
使用智能指针管理动态二维数组
正常我们申请动态二维数组是这么做的
int** p = new int*[3];
for (int i = 0; i < 3; i++)
p[i] = new int[5]{};
for (int i = 0; i < 3; i++)
delete[] p[i];
delete[] p;
有了智能指针后,我们可以做出这么一件明明可以用std::vector
但就是偏偏要折磨自己的事情
// C++20下编译通过 创建一个3*5的元素都为10的二维数组
std::shared_ptr<std::shared_ptr<int[]>[]> sp = std::make_shared<std::shared_ptr<int[]>[]>(3);
for (int i = 0; i < 3; i++)
sp[i] = std::make_shared<int[]>(5, 10);
死锁与自锁
如果两个强引用类型的智能指针互相引用会如何
//照搬知乎上的代码
class Person
{
public:
Person(std::string name) :
m_name(std::move(name)), m_partner(nullptr)
{
std::cout << m_name << " created" << std::endl;
}
~Person()
{
std::cout << m_name << " destoryed" << std::endl;
}
friend void partnerUp(std::shared_ptr<Person>& shrp1, std::shared_ptr<Person>& shrp2)
{
if (!shrp1 || !shrp2)
return;
shrp1->m_partner = shrp2;
shrp2->m_partner = shrp1;
}
private:
string m_name;
std::shared_ptr<Person> m_partner;
};
int main()
{
{
auto shrp1 = std::make_shared<Person>("Lucy");
auto shrp2 = std::make_shared<Person>("Ricky");
partnerUp(shrp1, shrp2); // 互相设为伙伴
}
return 0;
}
运行之后可以发现控制台只输出了两行created字符,也就是说对象并没有被析构,导致了内存的泄漏。当作用域结束时,理应执行析构函数,但是当析构shrp1时,却发现shrp2内部引用了shrp1,那么就得先析构shrp2,但是发现shrp1中内部又引用了shrp1,互相引用导致了 “ 死锁 ” ,最终导致内存泄漏
“自锁”同理。
auto shrp = std::make_shared<Person>("Lucy");
partnerUp(shrp, shrp);
那么这种情况下就需要使用到std::weak_ptr了
std::weak_ptr
std::weak_ptr
能够引用std::shared_ptr中的内存,但是它只作为旁观者的存在,并不享有内存的所有权,也就是说使用std::weak_ptr
去接受一个std::shared_ptr
,并不会增加强引用计数,当然也不会影响到std::shared_ptr
的析构,有效的阻止了 “ 死锁 ”, “ 自锁 ” 的问题
class Person
{
//Codes..
private:
string m_name;
std::weak_ptr<Person> m_partner; //此时程序能够正常析构了
};
w_ptr的API
-
std::weak_ptr
一般与std::shared_ptr搭配使用,使用lock()
可以返回一个他所监视的std::shared_ptr
-
使用
use_count()
可以获取他所监视的std::shared_ptr
的强引用奇数std::shared_ptr<int> shrp = std::make_shared<int>(10); std::weak_ptr<int> wkp = shrp; auto shrp2 = wkp.lock(); auto shrp3 = wkp.lock(); std::cout<< shrp2.use_count() << std::endl; // 3
-
使用
expired()
可以返回一个bool
值,若管理对象已删除则返回false
,否则返回true
bool expired() const noexcept { return this->use_count() == 0; }
注意,
std::weak_ptr
和std::shared_ptr
一样,都是派生自_Ptr_base
的,use_count()
是基类中的方法
题外话:std::initializer_list
C++ 11中包含了花括号初始的方法,叫做std::initializer_list
小括号调用到了类型的构造函数,包含了隐式转换的规则,而花括号并没有调用到构造函数,而是用到了列表初始化,这种方法当碰到精度降低,范围变窄等情况(narrowing conversion)时,编译器都会给出报错
double num = 3.1415926;
int test1(num); //编译器给出警告,该转换可能会丢失数据
int test2{num}; //编译器给出报错,从double转换到int需要收缩转换
同时,C++ 11也支持使用者在自定义类的初始化时使用列表初始化
class Test
{
public:
Test(const std::initializer_list<int>& i_list)
{
vec.reverse(i_list.size());
for (auto i : i_list)
vec.push_back(i);
}
private:
std::vector<int> vec;
};
int main()
{
Test t1{ 1, 2, 3 };
Test t2 = { 1, 2, 3, 4 };
}