导引
为什么有智能指针,是为了解决什么问题?(内存泄漏)
对裸指针操作容易出现以下情况:
- 访问无效数据(指针未初始化 野指针;指针指向的资源被释放 悬空指针,访问非法内存)
- 指针越界(直接对原始指针加减)
- 忘记释放分配的内存(狭义内存泄漏)
以上各种不当行为造成运行错误、内存泄漏、资源丢失等问题。
智能指针的出现就是为了能够安全的使用动态对象,确保在离开指针所在作用域时,自动正确的销毁动态分配的对象,防止内存泄漏(尽可能,非要取出裸指针运算那也没办法)。
如何解决上述问题? (RAII)
那肯定得看看它内部是通过什么原理来实现的,这里仅从思想上简要描述:
-
智能指针采用RAII方法, 将资源生命周期与对象生命周期绑定,在构造函数中初始化资源,离开作用域时在构造函数中自动释放内存。
这应该是智能指针的核心了。类似的采用RAII机制的还有C++中的一些互斥锁,如lock_guard, unique_lock
-
使用代理模式包装裸指针,重载*和->操作符,使其用起来和用原始指针保持一样的手感。
有哪些智能指针?
-
几种智能指针: auto_ptr, unique_ptr, shared_ptr, weak_ptr
-
auto_ptr(C++11弃用,C++17移除)
弃用auto_ptr的原因:- 拷贝构造函数和赋值语句的意义不明确,使用浅拷贝方式时,两个对象拥有同一块资源,在对象析构时资源会被释放两次造成程序崩溃;而如果更改成深拷贝的方式,会导致原对象失去对资源的所有权
- 析构函数也不能判断是析构一个对象还是一组对象
可以认为auto_ptr只是应用了RAII机制,其他的细节没有考虑到
-
unique_ptr
- 唯一性智能指针,指针独占使用,更加安全
- 删除了拷贝构造和赋值操作符重载,增加了移动构造和移动赋值
-
shared_ptr
- 共享性智能指针,指针共享使用,有引用计数,但有少量管理成本,不能过度使用
- 使用make_unique()和make_shared()创建智能指针(C++14标准),可以用auto简化声明
-
weak_ptr:弱引用指针
主要是为了解决使用shared_ptr共享指针时出现的循环引用的问题
-
-
智能指针的演变,之间的逻辑关系
autoptr之所以会出现问题,最大的原因是它它允许拷贝语义的同时,没有提供对拷贝带来的问题的解决方法,可以理解为它只是用了RAII机制来包装裸指针,除此之外没有考虑其他细节。
那么针对它拷贝语义带来的两个指针指向同一资源的问题,有两种解决思路:
- 一种解决方法就是直接禁用拷贝,这是unique_ptr的做法,它删除了拷贝构造和拷贝赋值,同时添加了移动构造和移动赋值,只允许一个指针独占一个资源,不允许共享;
- 另一种解决方法是允许共享,但提供引用计数的功能,使得能够知道有多少个指针在共享同一资源,这是sharedptr的做法
-
进一步理解shared_ptr(目前是shared_ptr,其他的待补充)
- 功能描述&实现原理:
-
shared_ptr可以使多个指针指向同一个对象,用引用计数器记录一共有多少个指针共享该对象,计数为0时对象自动释放对象资源并将原始指针置为空,避免悬空指针的产生。
-
具体实现:
这个类的内部主要有两个指针成员,一个指向资源;一个指向资源的引用计数。
可以从从构造函数、拷贝构造函数、赋值、析构函数的角度来理解其代码实现的大致思路:- 直接创建新对象时的初始化
当创建新的智能指针对象时,在构造函数中初始化指向资源的指针,并将引用计数置为1(当然如果创建的是空对象,指针为空且引用计数为0) - 拷贝形式的初始化
当使用已有的shared_ptr对象初始化一个新的shared_ptr对象时,在拷贝构造函数中,拷贝资源指针和引用计数器指针,并将引用计数+1.
(note个人理解:这里的拷贝是浅拷贝,直接拷贝指针,而不是指针指向的值,如果使用深拷贝,那么每个shared_ptr对象将会独立地持有资源和引用计数器,而不是共享) - 拷贝赋值
当使用一个shared_ptr对另一个进行赋值时,赋值操作符左操作数的引用计数-1,右操作数的引用计数+1。左操作数的引用计数-1后若为0,需要先释放资源。 - 析构
当任何 shared_ptr 对象超出作用域时,则在其析构函数中,将关联指针的引用计数减1。如果引用计数变为0,则表示没有其他 shared_ptr 对象与此内存关联,在这种情况下,使用delete释放资源,并将两个指针成员置为空。 - 容易出现的问题:
- 循环引用
- 释放两次:如果再初始化shared_ptr时是采用裸指针直接初始化的方式,可能造成使用同一个裸指针重复初始化了多个智能指针,造成每个智能指针都有独立的引用计数,当其中某个计数清零调用析构释放内存后,会造成悬空指针(另外的智能指针并不知道其指向的资源已经被释放了),剩下的智能指针再次释放资源时就会出现重复释放的问题。
- 直接创建新对象时的初始化
-
- 功能描述&实现原理:
// 智能指针的几种初始化方法,以shared_ptr为例
// 使用make_shared创建(推荐)
shared_ptr<MYCLASS> foo = make_shared<MYCLASS> ();
// 从原始指针创建(可能出错)
int* p = new int(10);
shared_ptr<int> ptr1(p); // 调用构造函数,开辟新的引用计数资源
shared_ptr<int> ptr1_1(p); // 此时有两个智能指针指向同一个资源,但各自的引用计数却是独立的。
// 在参数中new出临时的指针变量传递
shared_ptr<int> ptr2(new int(20));
// 用另一个智能指针初始化
shared_ptr<int> ptr3(ptr2); // 调用拷贝构造函数,不开辟新的引用计数资源,只是在原基础上计数+1