18:使用std::unique_ptr
管理具备专属所有权的资源
std::unique_ptr
表示独占所有权,因此无法拷贝(拷贝构造、拷贝赋值是delete的),只能进行移动操作从而转移资源控制权- 例外:可以从函数返回一个
std::unique_ptr
1 2 3 4 5
std::unique_ptr<int> func(int x){ auto delInt = [&](int* p) { cout<<"My deleter"<<endl; delete p;} std::unique_ptr<int, decltype(delInt)> pInt(new int(x), delInt); return pInt; }
- 例外:可以从函数返回一个
- 删除器是
std::unique_ptr
类型的一部分- 在不定义删除器的情况下,
std::unique_ptr
内存占用和原始指针相同 - 如果自定义删除器,则
std::unique_ptr
内存占用会变大
- 在不定义删除器的情况下,
std::unique_ptr
可以指向数组,默认删除器为delete[]
:std::unique_ptr<int[]> p(new int[5]{1,2,3,4,5});
- 但是数组形式用到的场合很少,尽量使用STL
std::unique_ptr
可以直接隐式转换为std::shared_ptr
- 典型应用:针对继承体系,作为工厂函数的返回值类型
1 2 3 4 5 6 7 8 9 10 11
class Animal {}; class Dog: public Animal {}; class Cat: public Animal {}; template <typename... Ts> auto makeAnimal(AnimalType type, Ts&&... AnimalInfo){ // C++14中函数返回值可以写为auto,因此unique_ptr的删除器可以放在函数内部,否则显式写出返回类型时需要知道删除器类型,因此删除器只能写在函数外部 auto delAnimal = [](AnimalType* ptr) { makeMyLog(ptr); delete ptr; } std::unique_ptr<Animal, decltype(delAnimal)> up(nullptr, delAnimal); if(type == Dog) up.reset(new Dog(std::forward<Ts>(AnimalInfo)...)); // 参数是万能引用,这里进行完美转发 if(type == Cat) up.reset(new Cat(std::forward<Ts>(AnimalInfo)...)); // 使用reset使得指针独占资源的所有权,不能直接将原始指针赋值给智能指针 return up; // 返回unique_ptr }
- 参考
19:使用std::shared_ptr
管理具备共享所有权的资源
std::shared_ptr
可以拷贝,通过引用计数来管理资源的生命周期std::shared_ptr
内存模型- 一个
std::shared_ptr
大小通常为普通指针的两倍:一个指针指向资源,另一个指针指向控制块 - 控制块中通过原子操作维护引用计数,保存deleter(因此deleter不属于
std::shared_ptr
类型的一部分),保存弱计数等
- 一个
std::shared_ptr
的使用:- 使用
std::make_shared
、std::unique_ptr
、原始指针创建std::shared_ptr
,会为资源创建一个控制块- 如果资源有多个控制块,就会被多次析构,因此尽量避免使用原始指针构造
std::shared_ptr
- 如果资源有多个控制块,就会被多次析构,因此尽量避免使用原始指针构造
- 使用
std::shared_ptr
或std::weak_ptr
创建一个std::shared_ptr
,不会创建一个新的控制块 this
的陷阱:1 2 3 4 5 6 7 8 9 10 11
vector<shared_ptr<Animal>> eatList; // 追踪哪些Animal调用了eat方法 struct Animal{ virtual void eat(){ eatList.emplace_back(this); // eatList.push_back(shared_ptr<Animal>(this)); } }; struct Cat: public Animal{}; struct Dog: public Animai{}; shared_ptr<Animal> myCat(new Cat); myCat->eat(); // 针对同一个对象创建了两个控制块
- 解决方法一:使类继承自
std::enable_shared_from_this
,类内部使用shared_from_this
方法,搜索当前对象的控制块,如果有就不用创建控制块了,如果没有则抛出异常,因此适合于当前对象已经创建过控制块的情况1 2 3 4 5 6 7 8 9 10 11
vector<shared_ptr<Animal>> eatList; // 追踪哪些Animal调用了eat方法 struct Animal: public std::enable_shared_from_this<Animal>{ virtual void eat(){ eatList.emplace_back(shared_from_this()); // eatList.push_back(shared_ptr<Animal>(shared_from_this())); } }; struct Cat: public Animal{}; struct Dog: public Animai{}; shared_ptr<Animal> myCat(new Cat); myCat->eat();
- 解决方法二:见127页,代码是自己实现的,有误
1 2 3 4 5 6 7 8 9 10 11 12 13
vector<shared_ptr<Animal>> eatList; // 追踪哪些Animal调用了eat方法 struct Animal: public std::enable_shared_from_this<Animal>{ public: template <typename... Ts> static shared_ptr<Animal> create(Ts&&... params) { return shared_ptr<Animal>(Animal(std::foward<Ts>(params)...)); } virtual void eat(){ eatList.emplace_back(shared_from_this()); // eatList.push_back(shared_ptr<Animal>(shared_from_this())); } private: Animal() {} // 构造函数 }; struct Cat: public Animal{}; struct Dog: public Animai{};
- 解决方法一:使类继承自
- 使用
- 参考
20:对于类似std::shared_ptr
但是可能空悬的指针使用std::weak_ptr
std::weak_ptr
通常视为std::shared_ptr
的辅助工具,通过std::shared_ptr
构造std::weak_ptr
std::weak_ptr
不会影响对象的引用计数- 但是
std::weak_ptr
没有解引用操作,必须调用lock
转换为std::shared_ptr
来访问对象- 例子:
if(shared_ptr<int> p = wp.lock()> cout<<*p<<endl;
- 例子:
- 典型应用:
- 避免
shared_ptr
循环引用:将其中一个shared_ptr
改为weak_ptr
1 2 3 4 5 6 7 8 9 10
struct A{ std::shared_ptr<B> pb; // std::weak_ptr<B> pb; }; struct B{ std::shared_ptr<A> pa; }; std::shared_ptr<A> pa = std::make_shared<A>(); std::shared_ptr<B> pb = std::make_shared<B>(); pa->pb = pb; // pb和pa->pb同时指向同一个对象B,引用计数为2 pb->pa = pa;
- 带缓存的工厂方法
1 2 3 4 5 6 7 8 9
std::shared_ptr<const Widget> fastLoadWidget(WidgetID id){ static std::unordered_map<WidgetID, weak_ptr<const Widget>> cache; std::shared_ptr<const Widget> widgetPtr = cache[id].lock(); if(!widgetPtr){ // 缓存中没有 widgetPtr = loadWidget(id); // 调用原始工厂方法创建,并加入到缓存中 cache[id] = widgetPtr; } return widgetPtr; }
- 观察者设计模式:多个观察者(observer)对象同时监听一个主题(subject)对象,主题对象会在其发生状态改变时发出通知。主题对象不会控制其观察者的生存期,但需要确认当一个观察者对象被析构后,主题对象不会再访问它。一种合理的设计就是让每个主题对象持有指向其观察者对象的
std::weak_ptr
,以便在使用之前确认它是否空悬。
- 避免
- 参考:
21:优先选用std::make_unqiue
和std::make_shared
,而非直接使用new
- make函数可以传入任意集合的参数,然后完美转发给构造函数,并动态创建一个对象,返回智能指针
- 支持auto
- 避免异常:将[[ch03-资源管理#17:以独立语句将new的对象置入智能指针| effective C++ item17:以独立语句将new的对象置入智能指针]]改进为使用make函数
1 2 3 4 5 6 7
void func(shared_ptr<Widget> sp, int priority); void func(shared_ptr<Widget>(new Widget), priority); // 可能由于异常导致内存泄露 void func(make_shared<Widget>(), priority); // 不会由于异常导致内存泄露 // 如果需要自定义删除器,并且又可以避免异常 shared_ptr<Widget> sp(new Widget, myDeleter); func(std::move(sp), priority); // 直接传递一个右值,避免了修改引用计数
- 效率更高:make函数只需要申请一次内存(同时存储对象和控制块),但是使用
shared_ptr<Widget>(new Widget)
需要申请两次内存(一次对象,一次控制块) - make函数的缺点:
- 无法自定义deleter
- 语义歧义:比如使用
()
和{}
初始化vector代表不同的方式,make函数可以完美转发()
,不支持完美转发{}
1 2 3 4
auto sp1 = make_shared<vector<int>>(2,3); // {3,3}; shared_ptr<vector<int>> sp2(new vector{1,2,3,4,5}); auto initList = {1,2,3,4,5}; auto sp3 = make_shared<vector<int>>(initList); // 不支持:make_shared<vector<int>>({1,2,3,4,5});
- 不建议对自定义内存管理方式的类使用 make 函数:通常情况下,类自定义的
operator new
和operator delete
被设计成用来分配和释放能精确容纳该类大小的内存块,但std::allocate_shared
所要求的内存大小并不等于动态分配对象的大小,而是在其基础上加上控制块的大小。 - 若存在非常大的对象和比相应的
std::shared_ptr
生存期更久的std::weak_ptr
,不建议使用 make 函数,会导致对象的析构和内存的释放之间产生延迟- 如果只申请一块内存(make函数),如果后来
shared_ptr
的引用计数为0,但是weak_ptr
的引用计数不为0时,对象销毁会被延长,只有当weak_ptr
的引用计数为0时,控制块才被释放 - 如果使用new的话,可以立即销毁对象
- 如果只申请一块内存(make函数),如果后来
- 参考
22:使用Pimpl习惯用法时,将特殊成员函数的定义放到实现文件中
- PImpl技术(Pointer to Implementation,编译防火墙):将类的实现放在另一个单独的类中,并通过不透明的指针进行访问。因此可以有效减少编译依赖。
- 原理:一个只声明但是不定义的类型是不完整类型,声明指向它的指针是可以通过编译的
- 常见错误:
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
// in "widget.h" #include <memory> class Widget { public: Widget(); private: struct Impl; std::unique_ptr<Impl> pImpl; }; //==================================================================================// // in "widget.cpp" #include "widget.h" #include <string> struct Widget::Impl { std::string name; }; Widget::Widget(): pImpl(std::make_unique<Impl>()){} //==================================================================================// // in "main.cpp" #include "widget.h" int main(){ Widget w; // 报错:/usr/include/c++/9/bits/unique_ptr.h:79:16: error: invalid application of ‘sizeof’ to incomplete type ‘Widget::Impl’ return 0; }
- 报错原因:在析构
Widget w
时,此时看到的Impl
是不完整类型- 在编译
widget.cpp
时没有问题:g++ -c widget.cpp -o widget.o
- 在编译
main.cpp
时出问题:g++ -c main.cpp -o main.o
- 没有定义
Widget
的析构函数,因此使用自动生成的析构函数(默认是inline
的) - 本来如果声明了
Widget
的析构函数,编译时无法进行处理,后面链接时链接到定义,运行时才能析构pImpl
(因为经过链接,此时也知道Impl
是完整类型) - 但是正因为自动生成的析构函数是
inline
的,编译时就可以展开,此时析构pImpl
当然看到的Impl
是不完整类型(还没有链接到widget.o
)
- 没有定义
- 在编译
- 报错原因:在析构
- 使用说明
- 考虑到如上报错和[[ch03-转向现代C++#17:理解特殊成员函数的生成机制|item17:理解特殊成员函数的生成机制]],因此最好将拷贝控制成员和析构函数自定义,且声明与实现分离(防止进行内联)
- 为了实现PImpl技术,使用
unique_ptr
是最合适的,因为pImpl
指针独享Impl
的所有权,如果使用shared_ptr
则上述报错不会出现(因为删除器不属于类型的一部分,属于控制块,不会包含删除器的代码)
- 参考
- 实例
widget.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
#include <memory> class Widget{ public: Widget(std::string s); Widget(const Widget& rhs); Widget& operator=(const Widget& rhs); Widget(Widget&& rhs); Widget& operator=(Widget&&); ~Widget(); std::string getName() const; private: // std::string _name; struct Impl; std::unique_ptr<Impl> pImpl; };
widget.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
#include "widget.h" #include <iostream> #include <string> struct Widget::Impl{ Impl(std::string name): _name(name) {}; std::string _name; std::string getName() const {return _name;} }; Widget::Widget(std::string s): pImpl(std::make_unique<Impl>(s)) {} Widget::~Widget() {} Widget::Widget(const Widget& rhs): pImpl(std::make_unique<Impl>(*rhs.pImpl)) {} Widget& Widget::operator=(const Widget& rhs){ *pImpl = *rhs.pImpl; return *this; } Widget::Widget(Widget&& rhs) =default; Widget& Widget::operator=(Widget&& rhs) =default; std::string Widget::getName() const { return pImpl->getName();}
main.cpp
1 2 3 4 5 6 7 8
#include "widget.h" #include "iostream" int main(){ Widget w("zhang"); std::cout<<w.getName()<<std::endl; return 0; }