跳转至

智能指针

众所周知,指针是 C++ 编程中的一大难关。使用指针的时候,我们需要防止访问已经释放的内存,同时也需要避免释放同一个指针两次。在程序规模还比较小的时候,可能这不是什么大问题,但一旦函数的调用关系复杂起来,遵守上面的规则就没有这么容易了。

在现代 C++ 编程中,为了保证内存安全以及防止内存泄漏,应该使用智能指针,并尽量减少裸指针的使用,也尽量不要使用 newdelete。智能指针保存了内存地址的使用信息,并利用了 C++ RAII 的特性,在智能指针对象生命周期结束时,使用析构函数自动释放指针所对应的内存。最常用的智能指针有 std::unique_ptrstd::shared_ptr 以及 std::weak_ptr

使用智能指针的开销

智能指针相比于裸指针,会带来一定程度上的额外开销。不过,只有当你编写的应用对性能非常敏感,才使用裸指针或引用。

所有权

在介绍智能指针之前,先介绍一个重要的概念:所有权(Ownership)。所有权就是指一个指针应该由谁“拥有”,这个所有者就需要负责释放内存。

所有权可以是唯一的,也就是同时间一个指针只能被一个对象拥有,如果需要传递指针,则需要转移所有权。类似于一个自习室,一次只能有一个人在房间里,这个人要么和另外一个人交换位置,要么自己离开房间并关灯。

所有权也可以是共享的,可以同时被多个对象拥有,这时候就和裸指针差不多。那么,共享所有权的指针,要怎么才能知道什么时候可以安全的使用和释放呢?最常用的方式就是引用计数(Reference Counting),有对象要使用这个指针了,就给引用计数加一,使用完毕了,就给引用计数减一。等到引用计数变为 0 了,就说明没有人使用这个指针了,可以安全地释放了。类似于一个很大的自习室,可以有很多人在里面,但是走的时候怎么知道房间里没有人了呢?可以在门口放一个罐子,有人进来就放一个硬币进去,有人走了就拿回一个硬币。最后拿空罐子的人就知道没有人在自习室里了,可以关灯了。当然,如果有人忘记放硬币了,那就有可能被忘在房间里了,如果有人忘记拿硬币了,房间就永远关不了灯了。

std::unique_ptr

std::unique_ptr 表示一个指针最多只能被引用一次,也就是不存在共享的关系。使用时应该使用 std::make_unique 来生成一个指向对象的 std::unique_ptrstd::unique_ptr 不允许拷贝,也就是说传递 std::unique_ptr 的时候只能使用 std::move 来使用移动语义。

std::shared_ptr

std::shared_ptr 表示一个指针可能被多次引用,也就是存在着共享的关系。使用时应该使用 std::shared_ptr 来生成一个指向对象的 std::shared_ptrstd::shared_ptr 允许拷贝,在使用时可以直接使用按值传递的方式来使用。

std::shared_ptr 中内置了引用计数,当发生拷贝的时候就会给引用计数加一,析构函数则会减一。当引用计数为 0 说明这个指针已经无人使用了,就会调用对象的析构函数去释放内存。

std::weak_ptr

std::weak_ptr 是一个具有迷惑性的名字,它实际上不是一个指针,不能像普通指针一样使用 *-> 来访问对象。它更像是一个兑换券,用来兑换 std::shared_ptrstd::weak_ptr 必须指向一个 std::shared_ptr,但是它不算在 std::shared_ptr 的引用计数里,也就是说哪怕还有 std::weak_ptr 存在,它指向的内存也有可能已经被释放了。它的使用场景比较有限,一种是打破循环引用,一种就是下面的 this 问题。

std::enable_shared_from_this

有的时候,我们需要在对象内部生成一个智能指针返回,例如:

class A {
public:
    std::shared_ptr<A> get_shared_ptr() {
        return std::shared_ptr<A>(this);
    }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    {
       std::shared_ptr<A> ptr = a->get_shared_ptr();
    }
}

但是这样会有双重释放的问题:这个指针的引用计数被初始化为 1,使用完毕后就会释放,而这个对象自己也会调用一次析构函数。为了解决这个问题,可以使用 std::enable_shared_from_this

class A : public std::enable_shared_from_this<A> {
public:
    std::shared_ptr<A> get_shared_ptr() {
        return shared_from_this();
    }
};

继承了这个类之后,对象就可以使用 shared_from_this 方法得到指向自己的 std::shared_ptr 而不会有双重释放的问题了。std::enable_shared_from_this 类里包含一个 std::weak_ptr 对象,而在 std::shared_ptr 构造函数里,会检查这个对象是不是 std::enable_shared_from_this 的派生类,如果是的话,就会将构造的 std::shared_ptr 赋值给 std::weak_ptr 对象。调用 shared_from_this 就会调用到这个 std::weak_ptr 对象,从而避免了双重释放的问题。

从原理也看得出来,enable_shared_from_this 需要这个对象已经被一个 std::shared_ptr 拥有,所以要使用这个函数,必须使用 std::make_shared 来构造对象,否则的话,shared_from_this 的行为是未定义的。

使用智能指针的建议

正如 new 要和 delete 搭配,在使用智能指针后,我们不再需要调用 delete,所以,为了配平,我们也不应该调用 new,而是直接使用 std::make_sharedstd::make_unique

把智能指针当成和普通指针一样,直接传值即可,而不要传递引用。

为了避免函数和特定的智能指针类型绑定,在编写函数的时候,建议是使用对象的引用,例如 void func(A& a),这样我们就可以这样调用函数:

std::shared_ptr<A> shared_a = std::make_shared<A>();
std::unique_ptr<A> unique_a = std::make_unique<A>();
func(*shared_a);
func(*unique_a);

std::optional

有些时候,我们需要使用空指针来表示“没有数据返回”等情况,例如可能我们需要查找一个数,找不到就返回空指针: int* find(int target) 这样的话,使用指针的人很可能意识不到这个指针可能为空。为了进一步增强安全性,我们可以使用 std::optional 这个类。

例如,std::optional<int> 表示一个可能为空的整数,这样我们就不需要使用指针来表达“可能为空”的情况,从根本上避免了访问空指针的内存安全问题。


最后更新: 2022-12-08 14:27:00
本页作者: Howard Lau, mwish