智能指针
C++11 的面试圣经 —— 智能指针
我发现这个人讲的还是不错的,就是语速太快……不过好在他的 lecture 都有官方字幕。
智能指针发展历史
auto_ptr : C++98 的遗老,C++17被移除
unique_ptr: C++11. 用来替代 auto_ptr,C++14 加入了 std::make_unique()
shared_ptr: C++11. 引用计数,支持std::make_shared() 。C++17 加入了 std::shared_ptr<T[]>
weak_ptr : C++11. 弱引用。
C++20 : std::make_shared<T[]>
std::unique_ptr :独占所有权
std::unique_ptr 可以自动替你管理资源。
原始指针是可以拷贝的,那么如果我拷贝了原始指针,那么谁来清理资源呢?这个不太好说。
std::unique_ptr 是 move-only,它的移动构造会将原来的指针置为 nullptr。
只有一个指针指向资源,std::unique_ptr 会自动帮你管理资源。
此外,std::unique_ptr 有一个对 T[] 的特化:
template<typename T>
class unique_ptr<T[]> {
T* p_ {nullptr};
~unique_ptr() {
delete [] p_;
}
};
std::unique_ptr 还有一个模板参数:Deleter。你可以显式的传入一个 deleter。
template<typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
T* p_{nullptr};
Deleter d_;
~unique_ptr() {
if (p_) d_(p_);
}
};
template<typename T>
struct default_delete {
void opeartor()(T *p) const {
delete p;
}
};
假设我们使用一个 FILE*
struct FileCloser {
void operator()(FILE* fp) const {
assert(fp != nullptr);
fclose(fp);
}
};
FILE *fp = fopen("input.txt", 'r');
std::unique_ptr<FILE, FileCloser> uptr(fp);
这样的话可以更加异常安全,而且可以完美适配 C API。
如果你使用类似于 OpenSSL 这样的 C API 的话,就可以使用这个用法。unique_ptr 可以作为 low-level (C API), non-RAII, raw resource 和 高级 API 间的粘合剂。
使用智能指针时的推荐做法
- 像对待裸指针一样对待智能指针
- pass by value
- return by value(当然)
- 对指针传引用太异味了,自然对智能指针也是
- 如果一个函数接受
unique_ptrby value,那么意味着所有权的转移 - 智能指针通常作为实现细节以及胶水
- 在接口中暴露 unique_ptr/shared_ptr 有点 code smell,你应该把他们放在类里。
std::shared_ptr:共享所有权
控制块
std::shared_ptr 代表共享所有权,使用 引用计数 实现。计数归零就会析构对象。引用计数可以使用一个 std::atomic<int>
对于一个 std::shared_ptr ,一般有两个成员,一个指向被管理对象的指针,另外一个指向控制块(control block)的指针。
控制块包含:引用计数、弱引用计数、自定义 deleter、指向管理对象的指针。
每个被管理的对象拥有一个控制块。
拷贝 shared_ptr,会拷贝两个指针,然后引用计数 +1。如果销毁 shared_ptr ,引用计数 -1
shared_ptr 通过控制块参与所有权的管理。
那么为什么控制块要有一个指向控制对象的指针呢?
类的布局
考虑以下的结构:
struct Fruit {int juice;};
struct Vegetable {int fiber;};
struct Apple : Fruit {int red;};
struct Tomato : Fruit, Vegetable {int sauce;};
Apple 继承 Fruit,实际上的布局大概是 |juice|red| 这样。
类似的,Tomato 大概是 |juice|fiber|sauce|
Apple is a Fruit,也就是说我有一个指向 Apple 的指针的同时也代表了指向 Fruit,先是 Fruit 的成员之后才是 Apple 的成员。
Tomato 类似。
就是说,如果我有一个 std::shared_ptr<Fruit> 和一个 std::shared_ptr<Vegetable>,他们都指向了 Tomato。指向 vegetable 的那个指针会有一些偏移。并没有指向直接需要管理的对象。
所以控制块中需要一个指针来决定对谁来执行 delete,在这里就是保存一个 tomato 对象的指针。
shared_ptr 的 aliasing construct
using Vec = std::vector<int>;
std::shared_ptr<int> foo() {
auto elts = {1, 2, 3, 4, 5};
std::shared_ptr<Vec> pvec = std::make_shared<Vec>(elts);
return std::shared_ptr<int>(pvec, &(*pvec)[2]); // 与 pvec 共享所有权,但指向 &(*pvec)[2]
}
int main() {
std::shared_ptr<int> ptr = foo();
for (int i = -2; i < 3; ++i) {
std::cout << ptr.get()[i] << '\n';
}
}
在以上的代码中,shared_ptr 中指向对象的成员指针指向的是 vec[2],但控制块中的指针指向的是 vector
最后一个 shared_ptr 销毁时就会销毁 vector
优先选择 make_unique()/make_shared()
现代 C++ 的目的之一就是,没有 new/delete 出现,且只调用 new 看起来也很难受。
比如下面这样:
std::shared_ptr<Widget> w(new Widget());
use(w)
也就是说,如果没调用 delete,那也应该尽量避免 new。标准库所以提供了 make_foo()
auto w = std::make_shared<Widget>();
use(w);
make_shared 也可以被优化,可以少一次内存分配,现在的库基本都能做到。例如:
template<typename T, typename... Args>
std::shared_ptr<T> make_shared(Args&&... args) {
auto* raw_ptr = new ControlBlockAnd<T>(std::forward<T>(args)...);
return std::shared_ptr<T>::From(raw_ptr);
}
总之:
- 多使用 make_shared/make_unique 避免 new
- 你不 new 就不会内存泄漏
- make_shared 可以优化
顺便,unique_ptr 可以隐式转换为 shared_ptr
std::shared_ptr<Widget> sptr = std::make_unique<Widget>();
std::weak_ptr:解决 shared_ptr 的悬垂
weak_ptr 在内存上看和 shared_ptr 差不多,都有一个指向管理对象的指针和一个指向控制块的指针。
区别在于,如果你拷贝 weak_ptr ,那么它会增加 弱引用计数 。
如果引用计数归零,那么对象会被销毁,而此时如果弱引用计数不为0,就会出现 weak_ptr 悬垂。shared_ptr 知道控制块还有 weak_ptr 在使用,所以也不会销毁控制块。
你并不能解引用 weak_ptr
weak_ptr并不是指针,它只是你在未来构造shared_ptr时的门票。- 你可以显式类型转换,或者调用 weak_ptr.lock(),虽然不会 lock 任何东西,它只是会返回一个 shared_ptr(如果对象没有被销毁的话)
- 如果想 get a ticket,那么只能使用显式类型转换
void recommended(std::weak_ptr<T> wptr) {
// std::shared_ptr<T> sptr{wptr}; 不要这么做
std::shared_ptr<T> sptr = wptr.lock();
if (sptr != nullptr) {
use(sptr);
}
}
其实可以在 if 语句内直接声明(这也是 if 内声明有用的几个情景之一)
if (auto sptr = wptr.lock()) {
use(sptr);
}
顺便一提,另一个情景是 RTTI
if (RedWidget *p = dynamic_cast<RedWidget*>(widgetpr)) {
use_red_widget(p);
}
通过 raw ptr 获得 shared_ptr
我们之前说 weak_ptr 是 ticket for shared_ptr,那么如果你只有一个裸指针怎么办?
class Widget {
std::weak_ptr<Widget> wptr_ = ???;
public:
std::shared_ptr<Widget> shared_from_this() const {
return wptr.lock();
}
};
自然我们不会每次都自己写,所以我们把 weak_ptr 放到基类,叫做 std::enable_shared_from_this,他的作用就是提供 shared_from_this() 成员函数。
这个你自己是实现不了的,因为它跟 shared_ptr 的构造函数有关联。
它使用的是 CRTP 模式。
- trick 的是 Widget 的
shared_from_this()成员函数返回的是shared_ptr<Widget>。我们通过某种方式,让基类知道了子类的名字。方法是我们让 base 类是一个模板,模版参数是子类的名字。这样的话,Widget继承自std::enable_shared_from_this<Widget>,这样就可以了。