C++11 智能指针

C++ 中的智能指针包括 auto_ptr, unique_ptr, shared_ptr, weak_ptr. 其中 auto_ptr已被弃用

unique_ptr

  1. unique_ptr是大部分情况下的首选, 体现独占的语义. 当unique_ptr被销毁时, 其指向的原始指针也自动被销毁.
  2. unique_ptr体现资源的独占, 因此无法被拷贝, 没有拷贝构造和拷贝赋值函数. 但是unique_ptr的所有权是可以通过move操作转移的, 由移动构造和移动赋值函数实现.
  3. unique_ptr默认通过delete实现对象销毁. 你也可以在构造时指定unique_ptr的销毁函数.
  4. 使用默认的销毁函数时, unique_ptr的大小和原始指针相同. 使用自定义销毁器时, unique_ptr会将销毁器的类封装在unique_ptr中, 因此应尽量使用无状态的函数对象(没有捕获的lambda表达式或仿函数)作为销毁器, 此时unique_ptr的大小仍然和原始指针相同.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 64bits platform
cout << sizeof(int*) << endl; // print: 8

unique_ptr<int> p1{};
cout << sizeof(p1) << endl; // print: 8

auto d = [](int *p) { delete p; };
unique_ptr<int, decltype(d)> p2{nullptr, d};
cout << sizeof(p2) << endl; // print: 8

void f(int *m) {
delete m;
}
unique_ptr<int, void(*)(int*)> p3(nullptr, f);
cout << sizeof(p3) << endl; // print: 16
  1. unique_ptr可以像原始指针那样用基类指针指向子类对象, 当然为了能正确析构对象, 你需要将析构函数声明为虚函数.
  2. unique_ptr无法从原始指针隐式转换而来.
  3. unique_ptr也可以用于数组对象 unique_ptr<T[]>, unique_ptr重载了[]操作符.
  4. unique_ptr可以被隐式转换为shared_ptr, 因此一个工厂方法应尽量返回unique_ptr对象.

shared_ptr

  1. shared_ptr体现共享的语义, shared_ptr从一个原始指针构造一个初始的对象, 并创建一个控制块. 从初始对象可以复制多个shared_ptr, 复制的对象共享控制块. 每个shared_ptr会使控制块中引用计数+1, 当所有shared_ptr被销毁时, 控制块中计数变为0, 原始指针亦被销毁.
  2. 从shared_ptr复制(拷贝构造和拷贝赋值)时会使引用计数+1, 移动时(移动构造和移动赋值)引用计数不变.
  3. shared_ptr的大小是固定的2倍原始指针大小, 一个是指向原始指针, 一个指向控制块.
  4. shared_ptr的控制块内存是动态分配的, 在首次创建初始shared_ptr对象时分配.
  5. shared_ptr的引用计数增减必须是原子性的, 这会导致一些性能消耗.
  6. 和unique_ptr一样, shared_ptr也可以自定义销毁器. 但是不同的是, shared_ptr的销毁器是放在控制块内存中的, 不会影响shared_ptr的类型和本身的大小. 这也意味着你可以在同一容器中放置元素类型相同但销毁器不同的shared_ptr.
  7. 首次创建初始对象时会创建控制块内存. 因此, 需要避免从一个原始指针多次创建shared_ptr对象, 这样会导致原始指针被析构两次.
1
2
3
4
5
auto pw = new Widget; // pw是原始指针
shared_ptr<Widget> spw1(pw); // 为*pw创建控制块
shared_ptr<Widget> spw2(pw); // 为*pw创建第二个控制块
// 会导致pw被析构两次
shared_ptr 同样适用于数组(c++17后).

weak_ptr

  1. weak_ptr是shared_ptr的一个增强功能, 通常从shared_ptr创建. 不同的是, weak_ptr不会影响指向对象的引用计数.
  2. weak_ptr使用expired()函数检查对象的引用计数是否为0, 即检查对应的shared_ptr是否过期.你可能会尝试这样使用weak_ptr:
1
2
3
4
5
6
7
8
auto spw = 					// spw构造后
std::make_shared<Widget>(); // 指向Widget

std::weak_ptr<Widget> wpw(spw); // wpw指向相同的Widget

if(!wpw.expired()) {
/// DO ...
}

这样检查shared_ptr是否过期, 然后使用weak_ptr. 但是在多线程情况下这样做可能有一些风险, 多线程下正确的方式是这样使用weak_ptr:

1
std::shared_ptr<Widget> spw1 = wpw.lock();

使用lock()返回一个shared_ptr, 然后使用返回的shared_ptr对象.

控制块中同样保存有weak_ptr的弱引用计数, 当控制块中的引用计数和弱引用计数都为0时, 控制块会被销毁.

enable_shared_from_this
很多情况下, 我们需要在一个对象内部使用包含this指针的shared_ptr对象, 或者从某个函数返回包含this指针的shared_ptr. 这时我们需要继承enable_shared_from_this, 然后用shared_from_this() 创建一个包含this指针的shared_ptr:

1
2
3
4
5
6
7
class Widget: public std::enable_shared_from_this<Widget> { // 这种继承方式叫CRTP
public:
void Widget::process()
{
processedWidgets.emplace_back(shared_from_this());
}
};

enable_shared_from_this的原理是在enable_shared_from_this中存储了一个weak_ptr对象. 当Widget对象被创建时, 这个weak_ptr置为指向nullptr. 当从一个Widget指针创建shared_ptr时, 会检测Widget对象是否继承了enable_shared_from_this. 如果继承了, 会将其内部的weak_ptr指向刚刚创建的shared_ptr.

这样, 这个weak_ptr就关联了一个包含this指针的shared_ptr, shared_from_this()其实就是从weak_ptr再次创建shared_ptr. 如果shared_from_this()前不曾用this创建过shared_ptr, weak_ptr就是指向nullptr的, 这时会抛出bad_weak_ptr异常.

  • make_shared

一般情况下, 更推荐使用make_shared来构造shared_ptr.

make_shared相对于直接从原始指针创建shared_ptr的优点有:

  1. 若创建原始指针和从原始指针创建shared_ptr之间抛出异常, 可能会发生内存泄漏;

make_shared将对象的内存和控制块内存同时分配, 并同时释放. 这样可以提高执行效率.
缺点有:

  1. 无法使用大括号初始化;
  2. 无法自定义销毁器;
  3. 对象内存和控制块内存同时分配同时释放. 这会导致在shared_ptr的引用计数为0, 但weak_ptr引用计数不为0的情况下.
    控制块内存无法释放, 会导致对象的那块内存一直无法释放, 造成内存浪费.

本文内容来自知乎,在此仅作复习回顾使用
原文链接: C++智能指针小结


C++11 智能指针
http://yoursite.com/2022/10/30/智能指针/
作者
Chole
发布于
2022年10月30日
许可协议