C++ 智能指针

原始指针

要想了解智能指针,就需要首先了解原始指针的痛点,原始指针有几点问题

  1. 忘记释放内存 -> 产生内存泄漏
  2. 在尚有指针引用内存的情况下释放内存(使用已经释放掉的对象) -> 产生引用非法内存的指针
  3. 同一块内存释放2次

智能指针的产生本质上都是为了解决这些问题

关于使用new动态分配对象的初始化问题,目前实际编码的结果和书籍内容存在冲突,《C++ Primer》p407 指出int *pi = new int中pi指向一个未初始化的int,其值是未定义,但是从实际编码结果来看,其遵循默认初始化的结果并非未定义,和int a表现出的并不相同(红色标注的位置即为和理解冲突的位置)

智能指针说明

  1. shared_ptr:允许多个指针指向同一个对象
  2. unique_ptr:独占所指向的对象
  3. weak_ptr:伴随类,一种弱引用,指向shared_ptr管理的对象

智能指针初始化

  1. 默认初始化的智能指针中保存一个空指针

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_parentm_child是处在结构体当中的,但是parentchild都被赋值为nullptr了,为什么结构体仍然存在。这里parent和child只是一个指向结构体的指针变量,指针变量为nullptr并不代表结构体所占的内存就被释放了,只有当没有指针指向内存区域时,才会释放对应的内存区域。

因此最终的结果是,parent和child对应的内存区域均没有释放

产生上述问题,是由于从属关系混乱导致的,shared_ptr会增加引用计数,这个指针只要存在,指向的资源就不会被释放,类似拥有这个资源的含义,拥有它的所有权。
child类中,由于m_parentshared_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循环引用例子中的parentchild场景描述并不是非常清晰,假设一个parent可能有多个child,那么应当使用shared_ptrweak_ptr的组合,如果一个parent只能有一个child,那么应当使用unique_ptr和原始指针的组合。

智能指针常用函数和类方法

  1. 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
  1. 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.

  1. 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

Reference

posted @ 2023-06-20 23:04  0x7F  阅读(38)  评论(0编辑  收藏  举报