Effective Modern C++ 笔记 - 智能指针
智能指针包裹原始指针,它们的行为看起来像被包裹的原始指针,但避免了原始指针的很多陷阱。你应该更倾向于用智能指针而非原始指针。几乎原始指针能做的所有事情智能指针都能做,而且出错的机会更少。
各种智能指针的API有极大的不同。唯一功能性相似的可能就是默认构造函数。因为有很多关于这些API的详细手册,所以我将只关注那些API概览没有提及的内容,比如值得注意的使用场景,运行时性能分析
18. 用std::unique_ptr
管理独占资源
- 小,快,move-only
- 可以自定义删除操作
- 可以管理数组(注: 一般用
std::array
或std::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
10auto 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
不能用同一个对象的指针分别创建不同的智能指针你懂的, 但有时希望在类方法中产生this
的std::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_ptr
在std::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
74class 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_unique
和std::make_shared
代替new
理由
- 安全, 如果用
new
, 假设在构建对象和构建智能指针之间有异常抛出, 就会造成内存泄漏。1
2// 如果computePriority()有异常
process(std::unique_ptr<Widget>(new Widget), computePriority()) - 用
new
要写两次类名1
std::unique_ptr<Widget> upw(new Widget)`
弱项
- 不适合自定义删除器
- 不方便用带大括号的初始化器
- 为了效率,
make
函数把对象和控制块放在同一块内存上。当处理超大对象和std::weak_ptr
时,用new
可以更早的回收内存。
22. 用Pimpl Idiom招式时, 把特殊成员函数定义在实现文件中
头文件里是一个带void *pImpl
私有属性的类,在实现文件中pImpl
指向的对象实现功能,头文件暴露的类只做转发。
理由
- Pimpl Idiom 可以减少编译依赖, 减少编译时间
- 用
std::unique_ptr
存放pImpl
时, 收回内存需要知道pImpl
的内容。默认生成的析构在删除该指针时找不到完整的pImpl
定义,应该把析构等特殊函数写到实现中。 - 本建议仅针对
std::unique_ptr
, 用std::shared_ptr
没这些问题
1 | // widget.h |
1 | // widget.cpp |