原始指针
要想了解智能指针,就需要首先了解原始指针的痛点,原始指针有几点问题
- 忘记释放内存 -> 产生内存泄漏
- 在尚有指针引用内存的情况下释放内存(使用已经释放掉的对象) -> 产生引用非法内存的指针
- 同一块内存释放2次
智能指针的产生本质上都是为了解决这些问题
关于使用new动态分配对象的初始化问题,目前实际编码的结果和书籍内容存在冲突,《C++ Primer》p407 指出int *pi = new int
中pi指向一个未初始化的int,其值是未定义,但是从实际编码结果来看,其遵循默认初始化的结果并非未定义,和int a
表现出的并不相同(红色标注的位置即为和理解冲突的位置)
智能指针说明
- shared_ptr:允许多个指针指向同一个对象
- unique_ptr:独占所指向的对象
- weak_ptr:伴随类,一种弱引用,指向shared_ptr管理的对象
智能指针初始化
- 默认初始化的智能指针中保存一个空指针
unique_ptr
首先unique_ptr作为智能指针,是在原始指针基础上出现的。原始指针容易出现内存泄漏问题,智能指针利用RAII方法来解决这一问题。
unique_ptr为了避免重复释放(double free)问题出现,禁止拷贝(删除了拷贝构造函数) 这样的特性带来的问题是无法进行参数传递(因为参数传递需要拷贝构造函数),有两种解决方法 想要解决这个问题,首先需要理解为什么unique_ptr会禁止拷贝,对一个指针进行拷贝,两个指针指向相同的内存空间,若释放了一个指针,那么会导致另一个指针空悬。
上述问题出现的原因可以总结为:一份资源的多个备份,在生命周期发生变更时,无法保证数据一致性
如果说生命周期不发生改变,那么多个备份也是没有问题的,因此在确保函数无需接管资源生命周期的控制权时,可以通过get()
方法获取到原始指针,完成函数参数传递
如果生命周期可能发生变更,那么需要确保资源始终只有一份,那么参数传递可以通过移动语义完成
shared_ptr
#include <memory>
std::shared_ptr<typename> object;
unique_ptr虽然解决了原始指针资源易忘记释放的问题,但是为了解决重复释放问题,采用了禁止拷贝的方式,在实际应用场景中较难使用 因此出现了采用引用计数方案的(类似软链接的实现方式,共享Inode,当计数为0时才会真正释放资源)shared_ptr,记录有多少个shared_ptr指向相同对象,在引用计数变为0后,自动释放对象
shared_ptr
构造函数是explicit
的,因此无法进行隐式类型转换式的初始化,必须通过直接初始化,以下代码展示了这一点
shared_ptr<int> p1 = new int(1024); // 错误,由于explicit的存在,无法进行隐式类型转换式的初始化
shared_ptr<int> p2(new int(1024)); // 正确,可以进行直接初始化
关于shared_ptr
和原始指针
智能指针和原始指针是可以混合使用的,但由于智能指针的生命周期控制权在于类,而原始指针的控制权在于开发者,当两者混合后,可能会因为生命周期产生一些问题,以以下代码为例进行说明
void process(shared_ptr<int> ptr) {}
int *x(new int(1024)); // x是一个普通指针
process(shared_prt<int>(x)); // 正确,可以用原始指针初始化智能指针
int j = *x; // 未定义,x为空悬指针
shared_ptr利用引用计数对指针生命周期进行控制,直接将shared_ptr指针作为实参和用原始指针初始化得到的shared_ptr作为实参的区别在于引用计数不同,前者引用计数为2,后者仅为1。process函数返回后引用计数分别变为1和0,前者没有问题,后者由于引用计数变为0会将内存释放,原始指针变为空悬指针,解引用操作为未定义行为。
总结来说,由于无法得知智能指针负责的对象何时会释放,因此使用原始指针访问智能指针负责对象是危险行为
一个智能指针是可以获取到一个对应的原始指针的,二者指向相同的内存地址,此时就变为了上述智能指针和原始指针混用的情况,需要注意生命周期问题。
c++中除了shared_pointer和weak_pointer是浅拷贝,unique_pointer禁止拷贝,其他均为深拷贝
weak_ptr
weak_ptr是shared_ptr的弱化版本,其不改变引用计数
shared_ptr
虽然利用引用计数解决了unique_ptr
实际应用较困难的问题,但是恰恰是这个机制也为其带来了一些问题。
若智能指针作为类的成员变量,并且类之间存在一定的从属关系,此时可能会出现循环引用的问题,导致最终资源不会被释放,以以下代码为例
#include <memory>
struct C {
std::shared_ptr<C> m_child;
std::shared_ptr<C> m_parent;
};
int main() {
std::shared_ptr<C> parent = std::make_shared<C>();
std::shared_ptr<C> child = std::make_shared<C>();
parent->m_child = child;
child->m_parent = parent;
parent = nullptr;
child = nullptr;
return 0;
}
以上代码实现的从属关系如下图所示,
从上图可以看出,parent和child两片内存区域最初的引用计数均为2
parent = nullptr
产生的效果如下图所示,这里要明白parent就是一个指针,赋值为nullptr只是让这个指针不再指向原来的内存区域。只有当这个指针是最后一个指向这片内存区域的指针时,修改其指向才会释放内存区域,由于此时除了parent的指针外,还存在其他指针指向parent对应的内存区域,因此此时内存并不会释放。parent和child内存区域的引用计数分别变为1和2(由于parent指针已经释放,故无法通过该指针获取引用计数值,可以额外创建一个parent指针副本来获取引用计数)
child = nullptr
产生的效果如下图所示,最终parent和child对应内存区域的引用计数分别为1和1,因此对应的内存区域并不会释放。这里比较难理解的是m_parent
和m_child
是处在结构体当中的,但是parent
和child
都被赋值为nullptr
了,为什么结构体仍然存在。这里parent和child只是一个指向结构体的指针变量,指针变量为nullptr并不代表结构体所占的内存就被释放了,只有当没有指针指向内存区域时,才会释放对应的内存区域。
因此最终的结果是,parent和child对应的内存区域均没有释放
产生上述问题,是由于从属关系混乱导致的,shared_ptr
会增加引用计数,这个指针只要存在,指向的资源就不会被释放,类似拥有这个资源的含义,拥有它的所有权。
在child
类中,由于m_parent
是shared_ptr
指针,这个意思就相当于child拥有parent,显然这是不正确,因此一种解决方式就是把类中m_parent
修改为weak_ptr
#include <memory>
struct C {
std::shared_ptr<C> m_child;
std::weak_ptr<C> m_parent;
};
int main() {
std::shared_ptr<C> parent = std::make_shared<C>();
std::shared_ptr<C> child = std::make_shared<C>();
parent->m_child = child;
child->m_parent = parent;
parent = nullptr;
child = nullptr;
return 0;
}
此时,代码呈现出的逻辑效果如下图所示,图中实线箭头表示shared_ptr
,虚线箭头表示weak_ptr
,注意weak_ptr
并不影响引用计数。
此时parent和child对应的内存引用计数分别为1和2
parent = nullptr
之后,由于parent对应内存引用计数变为0,因此此内存区域被释放,parent->m_child指针也被销毁,因此child对应内存区域引用计数变为1。对应结构如下图所示
child = nullptr
之,由于此时child指针是最后一个指向child内存区域的指针,当其不再指向这篇内存区域后,引用计数变为0,同时内存区域被释放,效果如下图所示
四种指针之间的关系
原始指针和unique_ptr:原始指针可理解为unique_ptr的弱引用 weak_ptr和shared_ptr:weak_ptr是shared_ptr的弱引用 由于智能指针会自行进行资源回收,因此访问弱引用时指针可能已经失效,weak_ptr相较于原始指针提供了失效检测功能,当指向内存空间被释放时,不会出现访问错误
具体是用哪种组合取决于实际的应用场景,在讲述shared_ptr
循环引用例子中的parent
和child
场景描述并不是非常清晰,假设一个parent
可能有多个child
,那么应当使用shared_ptr
和weak_ptr
的组合,如果一个parent
只能有一个child
,那么应当使用unique_ptr
和原始指针的组合。
智能指针常用函数和类方法
- shared_ptr
functions | meaning |
---|---|
shared_ptr<T> sp |
null intelligence pointer, which can point to a T type object |
make_shared<T>(args) |
use args to initialize a shared_ptr and return it |
shared_ptr<T> p(q) |
p is the copy of shared_ptr q, this operation will increate the counter of q |
p = q |
decrease the reference count of original object of p, increase it of q |
p |
use p for conditional judgement, if p refers to an object, the result is true |
*p |
dereference p, get the object which is refered by p |
p->mem |
equal to (*p).mem |
p.get() |
get origin pointer |
swap(p, q) / p.swap(q) |
exchange the pointer of p and q |
p.use_count() |
get the reference count of intelligence pointer p |
p.unique() |
if the reference count of p equals 1, it returns true, otherwise false |
- unique_ptr
functions | meaning |
---|---|
unique_ptr<T> up |
null intelligence pointer, which can point to a T type object |
p = q |
decrease the reference count of original object of p, increase it of q |
p |
use p for conditional judgement, if p refers to an object, the result is true |
*p |
dereference p, get the object which is refered by p |
p->mem |
equal to (*p).mem |
p.get() |
get origin pointer |
swap(p, q) / p.swap(q) |
exchange the pointer of p and q |
u = nullptr |
release the object pointed by u and set u to be nullptr |
u.release() |
u gives up the control power to pointer, return the pointer and set u to be nullptr |
u.reset(p) |
if p is nullptr, release the object pointed by u, otherwise release the original object and set u point to p |
How does unique_ptr control to only point to one object ? To be precise, one object can be pointed by multiple unique_ptr, but it will cause some errors, so we say unique_ptr is not allowed to point to multiple objects. The reason is that unique_ptr doesn't use the count to memorize the object number which is pointed by intelligence pointer, so if code leave the action scope, the unique_ptr will release the source, if one object is pointed by multiple unique_ptr, it will cause repeated release, so we say unique_ptr can't point to multiple objects.
A falliable point In my previous understanding, a null pointer, which type is MyClass, can't call functions of MyClass. If we do it, we will get a Segmentation fault error. But when a unique_ptr which value is nullptr, it can also call the get() function, and the result is also nullptr.
To be honest, I don't know its principle now.
- weak_ptr
functions | meaning |
---|---|
weak_ptr<T> w |
null intelligence pointer, which can point to a T type object |
weak_ptr<T> w(sp) |
w is a weak_ptr which points to a object that is same as shared_ptr sp |
w = p |
p is a shared_ptr or a weak_ptr |
w.reset() |
set w to be nullptr |
w.use_count() |
return the count of shared_ptr which shares the same object with w |
w.expired() |
if w.use_count() equals 0, it will return true, otherwise return false |
w.lock() |
if w.expired() return true, return a null shared_ptr, otherwise return a shared_ptr which points to w |