本文译改自 Smart developers use smart pointers 系列文章的前两篇。相较于原文已经进行了大量的内容删减、补充和替换,本文不再标记为【翻译】。
建议对本文感到困惑,以及对该主题内容非常感兴趣的读者阅读原文
滥用指针、糟糕的内存管理通常会把程序变成一团乱麻,全面破坏程序的可读性、可维护性和安全性。小心翼翼地手动管理指针也会明显破坏程序的抽象层次,成为业务逻辑代码中的噪音。
智能指针能在一定程度上解决上述的问题,恰当地使用智能指针能让程序更加简洁易读、安全和高效。
RAII 与智能指针
RAII 是一种在 C++ 用类来包装资源的方法。此处用一个例子来说明如何用 RAII 的方法来包装裸指针:
template <typename T>
class SmartPointer
{
public:
explicit SmartPointer(T* p): p_(p) {}
~SmartPointer() { delete p_; }
private:
T* p_;
}
事实上,C++ 提供的智能指针的原理差不多就是上面这样——将资源包在对象里,并在对象的析构函数中释放资源,以此确保资源不会因为被遗忘而丢失。
智能指针让我们可以在栈上通过指针使用分配在堆上的资源,而且确保能够在出当前作用域时,堆上的资源被自动释放。
智能指针有着指针的行为,当智能指针本身被销毁时,智能指针会自动销毁其所指向的堆上对象。
unique_ptr 、 shared_ptr 和 weak_ptr
std::unique_ptr
unique_ptr
代表着这个指针是一块内存资源的唯一拥有者。unique_ptr
维护一个指针,并在其析构函数中释放该指针。
unique_ptr
的一大优点能够在语义上表达一定的意义和意图。就比如下面这个代码:
std::unique_ptr<House> buildAHouse();
返回值的类型是一个 std::unique_ptr
,这其实就是在告诉你该函数将会给你一个指向房子的指针,并且你就是这个房子的拥有者。除了给你返回这个 unique_ptr
的函数之外,没有任何人能删除该指针。
因为只有你拥有这个指针,所以你也可以放心地修改指针所指向的对象。std::unique_ptr
是工厂函数的首选指针返回类型。
另一方面,std::unique_ptr
也可以用于参数传递,能够表达相同的所有权转移:
class House {
public:
House(std::unique_ptr<PileOfWood> wood);
...
}
在这种情况下,房子会获得 PileOfWood
的所有权。
需要注意的是,即使你收到了一个 unique_ptr
,其实也并不能保证其他人无法访问该指针。如果另一段代码保留了你的 unique_ptr
中包装的指针,并且用那个指针修改其指向的对象,那么你再通过 unique_ptr
修改该对象就会影响到这个“另一段”代码的执行。但由于你是所有者,你就可以安全地修改指针所指向的对象,其他的所有代码设计都应该提前考虑到这个问题。
如果的确还是不希望传回的指针被修改,可以通过加个 const 的方式来实现:
std::unique_ptr<const House> buildAHouse();
// 出于某种原因,我不希望你修改我传给你的房子
为确保只能有一个 unique_ptr
拥有内存资源,std::unique_ptr
是不允许拷贝的。可以通过移动来将所有权从一个 unique_ptr
转移给另一个 unique_ptr
,这也是在将 unique_ptr 作为参数传递和作为返回值返回时用到的方法。
在函数中,移动可以通过以值形式返回 std::unique_ptr
的方式来实现,也可以像下面这样显式实现:
std::unique_ptr<int> p1 = std::make_unique(42);
std::unique_ptr<int> p2 = move(p1); // 现在 p2 拥有资源,p1 一无所有
如果尝试使用一个已经被 move 过的指针(比如上面代码中的 p1
),将在运行时出现段错误
std::shared_ptr
shared_ptr
允许同时有多个 shared_ptr
指向同一块内存资源。其内部会维护一个计数器,该计数器计算有多少个 shared_ptr
正指向同一个资源,当该计数器归零时释放对应的资源。
即: shared_ptr
是允许拷贝的,而且其能够通过内部维护的计数器确保资源会被且仅会被释放一次。
这些特性让 shared_ptr 看起来是解决内存管理的终极方案,其让我们可以随意地把指针传来传去——然而事实并非如此。出于下面几种原因,该指针并非首选的智能指针:
- 让多个所有者同时维护同一个资源,会显著增加系统的复杂度。尽管
std::unique_ptr
并不阻止读取和修改资源,但能确保该指针是这块资源仅有的所有者,我们可以预期这个指针会对资源负责(至少在一定程度上是这样的); - 有多个同时能够获取资源的维护者,会加大保证线程安全的难度;
- 保留引用计数会消耗更多的内存和计算时间
std::weak_ptr
weak_ptr
可以和 shared_ptr
一起维护指向同一个共享对象的引用,但 weak_ptr
并不会增加这个对象的引用计数。这也就意味着当最后一个引用该对象的 std::shared_ptr
被销毁时,无论是否还有 weak_ptr
指向该对象,该对象都会被销毁掉。
正因如此,weak_ptr
需要自己检查其所指向的对象是否存在。想要对对象的存在性进行检查,需要用 weak_ptr
构造 shared_ptr
来实现:
void useMyWeakPointer(std::weak_ptr<int> wp)
{
if(std::shared_ptr<int> sp = wp.lock()) {
// 资源仍然存在并可用
} else {
// 资源已不可用
}
}
weak_ptr
的一大用途是打破 shared_ptr 循环引用:
#include <memory>
#include <iostream>
struct B;
struct A {
std::shared_ptr<B> b;
~A() { std::cout << "~A()\n"; }
};
struct B {
std::shared_ptr<A> a;
~B() { std::cout << "~B()\n"; }
};
void useAnB() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b = b;
b->a = a;
}
int main() {
useAnB();
std::cout << "Finished using A and B\n";
}
运行上面的代码,输出如下:
Finished using A and B
输出中没有看到 ~A()
和 ~B()
,这说明直到函数返回
这段代码在堆上构建了两个对象,分别用 shared_ptr
a 和 b 指向他们。而这两个对象内部又都有一个 shared_ptr
指向对方。虽然 shared_ptr
a 、b 在栈上,但它们各自自带的 shared_ptr
都在堆上,这就导致代码执行即使离开了当前作用域, a 、 b 的引用计数器也仍然不为 0 。就这样,我们跟丢了 a 和 b ,预期中应该起作用的智能指针 shared_ptr 在此 “失了智” 。
解决方案就是把 A 、 B 其中一个定义中的 shared_ptr
换为 weak_ptr
,改成下面这样:
struct A {
std::weak_ptr<B> b;
~A() { std::cout << "~A()\n"; }
};
这样,预期的结果就能够正确发生了