Effective Modern C++ 笔记 - 智能指针

智能指针包裹原始指针,它们的行为看起来像被包裹的原始指针,但避免了原始指针的很多陷阱。你应该更倾向于用智能指针而非原始指针。几乎原始指针能做的所有事情智能指针都能做,而且出错的机会更少。

各种智能指针的API有极大的不同。唯一功能性相似的可能就是默认构造函数。因为有很多关于这些API的详细手册,所以我将只关注那些API概览没有提及的内容,比如值得注意的使用场景,运行时性能分析

18. 用std::unique_ptr管理独占资源

  • 小,快,move-only
  • 可以自定义删除操作
  • 可以管理数组(注: 一般用std::arraystd::vector就够了)
  • 可以用右值转换成std::shared_ptr
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 自定义删除操作
    auto createWidgetUPtr()
    {
    auto loggingDel = [](Widget* pw) {
    std::cout << "UPTR DEL: " << pw << std::endl;
    delete pw;
    };

    std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel);
    return upw;
    }
    如果自定义删除是函数指针的话, unique_ptr的要增大至少一个函数指针的大小.

19. 用std::shared_ptr管理共享资源

std::unique_ptr区别

  • 带引用计数
  • 由于带引用计数, 有双倍指针大小
  • 引用计数是动态分配的, 所以性能略低
  • 增减引用计数是原子操作
  • 也可以自定义删除操作, 只需要在构造时指定
  • 每个对象都有个专门的内存块用于存放自定义分配删除、引用计数、弱引用计数等数据,称为控制块
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    auto createWidgetSPtr()
    {
    auto loggingDel = [](Widget* pw) {
    std::cout << "SPTR DEL: " << pw << std::endl;
    delete pw;
    };

    std::shared_ptr<Widget> spw(new Widget, loggingDel);
    return spw;
    }

shared_from_this

不能用同一个对象的指针分别创建不同的智能指针你懂的, 但有时希望在类方法中产生thisstd::shared_ptr
一个想法是类创建时就生成一个共享指针,后面需要的共享指针都拷贝自最初的。
std::enable_shared_from_this派生,使用shared_from_this()方法可以得到正确的共享指针,前提是在调用shared_from_this()之前必须有已存在的this的控制块(即最初的std::shared_ptr)。为了保证这个前提,建议把构造函数放到private区,用静态的createInstance来构造对象, 返回std::shared_ptr
例子见后一小节

20. 会用std::weak_ptr

  • std::weak_ptr不会增加引用记数
  • std::weak_ptrstd::shared_ptr控制块生成时就建立联系了
  • 通过std::weak_ptr可以知道对象是否已经被删除(expired())
  • 通过lock()方法得到std::shared_ptr,如果对象已删除则返回空的共享指针
  • 可用于缓存、观察者列表以及防止共享指针的循环引用问题。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    class Observer;
    class Subject
    {
    public:
    void insert(std::shared_ptr<Observer> observer)
    {
    observers.push_back(observer);
    }
    void notify(const std::string &s);

    private:
    std::vector<std::weak_ptr<Observer>> observers; // 观察者列表
    };

    class Observer : public std::enable_shared_from_this<Observer>
    {
    public:
    template <typename... Ts>
    static std::shared_ptr<Observer> create(Ts &&... params)
    {
    auto p = new Observer(std::forward<Ts>(params)...);
    auto res = std::shared_ptr<Observer>(p);
    return res;
    }

    void subscribe(Subject &subject)
    {
    // 插入到主题的订阅者列表中, this共享指针应该用shared_from_this
    subject.insert(shared_from_this());
    }

    void onReceived(const std::string &s)
    {
    std::cout << name
    << " received:"
    << s << std::endl;
    }

    private:
    Observer(const std::string &name) : name(name) {}

    std::string name;
    };

    void Subject::notify(const std::string &s)
    {
    for (auto i : observers)
    {
    auto observer = i.lock();
    if (observer)
    {
    observer->onReceived(s);
    }
    }
    }

    int main(int, char **)
    {

    Subject sub;

    auto o1 = Observer::create("one");
    o1->subscribe(sub);

    {
    auto o2 = Observer::create("two");
    o2->subscribe(sub);
    sub.notify("Hello");
    }

    sub.notify("Hello again");

    return 0;
    }

21. 优先用std::make_uniquestd::make_shared代替new

理由

  1. 安全, 如果用new, 假设在构建对象和构建智能指针之间有异常抛出, 就会造成内存泄漏。
    1
    2
    // 如果computePriority()有异常
    process(std::unique_ptr<Widget>(new Widget), computePriority())
  2. new要写两次类名
    1
    std::unique_ptr<Widget> upw(new Widget)`

    弱项

  3. 不适合自定义删除器
  4. 不方便用带大括号的初始化器
  5. 为了效率,make函数把对象和控制块放在同一块内存上。当处理超大对象和std::weak_ptr时,用new可以更早的回收内存。

22. 用Pimpl Idiom招式时, 把特殊成员函数定义在实现文件中

头文件里是一个带void *pImpl私有属性的类,在实现文件中pImpl指向的对象实现功能,头文件暴露的类只做转发。

理由

  1. Pimpl Idiom 可以减少编译依赖, 减少编译时间
  2. std::unique_ptr存放pImpl时, 收回内存需要知道pImpl的内容。默认生成的析构在删除该指针时找不到完整的pImpl定义,应该把析构等特殊函数写到实现中。
  3. 本建议仅针对std::unique_ptr, 用std::shared_ptr没这些问题
1
2
3
4
5
6
7
8
9
10
11
12
13
// widget.h
class Widget {
public:
Widget();
~Widget();
Widget(const Widget&);
Widget& operator=(const Widget&);
Widget(Widget&&);
Widget& operator=(Widget&&);
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// widget.cpp
struct Widget::Impl
{
// impl here
};

Widget::Widget()
: pImpl(std::make_unique<Impl>())
{}

Widget::~Widget() = default;
Widget::Widget(Widget&&) = default;
Widget& Widget::operator=(Widget&&) = default;
Widget::Widget(const Widget& rhs)
:pImpl(nullptr)
{
if(rhs.pImpl) pImpl = std::make_unique<Impl>(*rhs.pImpl)
}
Widget& Widget::operator=(const Widget& rhs)
{
if(!rhs.pImpl) pImpl.reset();
else if(!pImpl) pImpl = std::make_unique<Impl>(*rhs.pImpl*);
else *pImpl = *rhs.pImpl;
return *this;
}