# 第4章 智能指针 **CHAPTER 4 Smart Pointers** 诗人和歌曲作家喜欢爱。有时候喜欢计数。很少情况下两者兼有。受伊丽莎白·巴雷特·勃朗宁(Elizabeth Barrett Browning)对爱和数的不同看法的启发(“我怎么爱你?让我数一数。”)和保罗·西蒙(Paul Simon)(“离开你的爱人必须有50种方法。”),我们可以试着枚举一些为什么原始指针很难被爱的原因: 1. 它的声明不能指示所指到底是单个对象还是数组。 2. 它的声明没有告诉你用完后是否应该销毁它,即指针是否拥有所指之物。 3. 如果你决定你应该销毁指针所指对象,没人告诉你该用`delete`还是其他析构机制(比如将指针传给专门的销毁函数)。 4. 如果你发现该用`delete`。 原因1说了可能不知道该用单个对象形式(“`delete`”)还是数组形式(“`delete[]`”)。如果用错了结果是未定义的。 5. 假设你确定了指针所指,知道销毁机制,也很难确定你在所有执行路径上都执行了**恰为一次**销毁操作(包括异常产生后的路径)。少一条路径就会产生资源泄漏,销毁多次还会导致未定义行为。 6. 一般来说没有办法告诉你指针是否变成了悬空指针(dangling pointers),即内存中不再存在指针所指之物。在对象销毁后指针仍指向它们就会产生悬空指针。 原始指针是强大的工具,当然,另一方面几十年的经验证明,只要注意力稍有疏忽,这个强大的工具就会攻击它的主人。 **智能指针**(*smart pointers*)是解决这些问题的一种办法。智能指针包裹原始指针,它们的行为看起来像被包裹的原始指针,但避免了原始指针的很多陷阱。你应该更倾向于智能指针而不是原始指针。几乎原始指针能做的所有事情智能指针都能做,而且出错的机会更少。 在C++11中存在四种智能指针:`std::auto_ptr`,`std::unique_ptr`,`std::shared_ptr`,` std::weak_ptr`。都是被设计用来帮助管理动态对象的生命周期,在适当的时间通过适当的方式来销毁对象,以避免出现资源泄露或者异常行为。 `std::auto_ptr`是来自C++98的已废弃遗留物,它是一次标准化的尝试,后来变成了C++11的`std::unique_ptr`。要正确的模拟原生指针需要移动语义,但是C++98没有这个东西。取而代之,`std::auto_ptr`拉拢拷贝操作来达到自己的移动意图。这导致了令人奇怪的代码(拷贝一个`std::auto_ptr`会将它本身设置为null!)和令人沮丧的使用限制(比如不能将`std::auto_ptr`放入容器)。 `std::unique_ptr`能做`std::auto_ptr`可以做的所有事情以及更多。它能高效完成任务,而且不会扭曲自己的原本含义而变成拷贝对象。在所有方面它都比`std::auto_ptr`好。现在`std::auto_ptr`唯一合法的使用场景就是代码使用C++98编译器编译。除非你有上述限制,否则你就该把`std::auto_ptr`替换为`std::unique_ptr`而且绝不回头。 各种智能指针的API有极大的不同。唯一功能性相似的可能就是默认构造函数。因为有很多关于这些API的详细手册,所以我将只关注那些API概览没有提及的内容,比如值得注意的使用场景,运行时性能分析等,掌握这些信息可以更高效的使用智能指针。 ## 条款十八:对于独占资源使用`std::unique_ptr` **Item 18: Use `std::unique_ptr` for exclusive-ownership resource management** 当你需要一个智能指针时,`std::unique_ptr`通常是最合适的。可以合理假设,默认情况下,`std::unique_ptr`大小等同于原始指针,而且对于大多数操作(包括取消引用),他们执行的指令完全相同。这意味着你甚至可以在内存和时间都比较紧张的情况下使用它。如果原始指针够小够快,那么`std::unique_ptr`一样可以。 `std::unique_ptr`体现了专有所有权(*exclusive ownership*)语义。一个non-null `std::unique_ptr`始终拥有其指向的内容。移动一个`std::unique_ptr`将所有权从源指针转移到目的指针。(源指针被设为null。)拷贝一个`std::unique_ptr`是不允许的,因为如果你能拷贝一个`std::unique_ptr`,你会得到指向相同内容的两个`std::unique_ptr`,每个都认为自己拥有(并且应当最后销毁)资源,销毁时就会出现重复销毁。因此,`std::unique_ptr`是一种只可移动类型(*move-only type*)。当析构时,一个non-null `std::unique_ptr`销毁它指向的资源。默认情况下,资源析构通过对`std::unique_ptr`里原始指针调用`delete`来实现。 `std::unique_ptr`的常见用法是作为继承层次结构中对象的工厂函数返回类型。假设我们有一个投资类型(比如股票、债券、房地产等)的继承结构,使用基类`Investment`。 ```cpp class Investment { … }; class Stock: public Investment { … }; class Bond: public Investment { … }; class RealEstate: public Investment { … }; ``` ![item18_fig1](media/item18_fig1.png) 这种继承关系的工厂函数在堆上分配一个对象然后返回指针,调用方在不需要的时候有责任销毁对象。这使用场景完美匹配`std::unique_ptr`,因为调用者对工厂返回的资源负责(即对该资源的专有所有权),并且`std::unique_ptr`在自己被销毁时会自动销毁指向的内容。`Investment`继承关系的工厂函数可以这样声明: ```cpp template //返回指向对象的std::unique_ptr, std::unique_ptr //对象使用给定实参创建 makeInvestment(Ts&&... params); ``` 调用者应该在单独的作用域中使用返回的`std::unique_ptr`智能指针: ```cpp { … auto pInvestment = //pInvestment是 makeInvestment( arguments ); //std::unique_ptr类型 … } //销毁 *pInvestment ``` 但是也可以在所有权转移的场景中使用它,比如将工厂返回的`std::unique_ptr`移入容器中,然后将容器元素移入一个对象的数据成员中,然后对象过后被销毁。发生这种情况时,这个对象的`std::unique_ptr`数据成员也被销毁,并且智能指针数据成员的析构将导致从工厂返回的资源被销毁。如果所有权链由于异常或者其他非典型控制流出现中断(比如提前从函数return或者循环中的`break`),则拥有托管资源的`std::unique_ptr`将保证指向内容的析构函数被调用,销毁对应资源。(这个规则也有些例外。大多数情况发生于不正常的程序终止。如果一个异常传播到线程的基本函数(比如程序初始线程的`main`函数)外,或者违反`noexcept`说明(见[Item14](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item14.md)),局部变量可能不会被销毁;如果`std::abort`或者退出函数(如`std::_Exit`,`std::exit`,或`std::quick_exit`)被调用,局部变量一定没被销毁。) 默认情况下,销毁将通过`delete`进行,但是在构造过程中,`std::unique_ptr`对象可以被设置为使用(对资源的)**自定义删除器**:当资源需要销毁时可调用的任意函数(或者函数对象,包括*lambda*表达式)。如果通过`makeInvestment`创建的对象不应仅仅被`delete`,而应该先写一条日志,`makeInvestment`可以以如下方式实现。(代码后有说明,别担心有些东西的动机不那么明显。) ```cpp auto delInvmt = [](Investment* pInvestment) //自定义删除器 { //(lambda表达式) makeLogEntry(pInvestment); delete pInvestment; }; template std::unique_ptr //更改后的返回类型 makeInvestment(Ts&&... params) { std::unique_ptr //应返回的指针 pInv(nullptr, delInvmt); if (/*一个Stock对象应被创建*/) { pInv.reset(new Stock(std::forward(params)...)); } else if ( /*一个Bond对象应被创建*/ ) { pInv.reset(new Bond(std::forward(params)...)); } else if ( /*一个RealEstate对象应被创建*/ ) { pInv.reset(new RealEstate(std::forward(params)...)); } return pInv; } ``` 稍后,我将解释其工作原理,但首先请考虑如果你是调用者,情况如何。假设你存储`makeInvestment`调用结果到`auto`变量中,那么你将在愉快中忽略在删除过程中需要特殊处理的事实。当然,你确实幸福,因为使用了`unique_ptr`意味着你不需要关心什么时候资源应被释放,不需要考虑在资源释放时的路径,以及确保只释放一次,`std::unique_ptr`自动解决了这些问题。从使用者角度,`makeInvestment`接口很棒。 这个实现确实相当棒,如果你理解了: - `delInvmt`是从`makeInvestment`返回的对象的自定义的删除器。所有的自定义的删除行为接受要销毁对象的原始指针,然后执行所有必要行为实现销毁操作。在上面情况中,操作包括调用`makeLogEntry`然后应用`delete`。使用*lambda*创建`delInvmt`是方便的,而且,正如稍后看到的,比编写常规的函数更有效。 - 当使用自定义删除器时,删除器类型必须作为第二个类型实参传给`std::unique_ptr`。在上面情况中,就是`delInvmt`的类型,这就是为什么`makeInvestment`返回类型是`std::unique_ptr`。(对于`decltype`,更多信息查看[Item3](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item3.md)) - `makeInvestment`的基本策略是创建一个空的`std::unique_ptr`,然后指向一个合适类型的对象,然后返回。为了将自定义删除器`delInvmt`与`pInv`关联,我们把`delInvmt`作为`pInv`构造函数的第二个实参。 - 尝试将原始指针(比如`new`创建)赋值给`std::unique_ptr`通不过编译,因为是一种从原始指针到智能指针的隐式转换。这种隐式转换会出问题,所以C++11的智能指针禁止这个行为。这就是通过`reset`来让`pInv`接管通过`new`创建的对象的所有权的原因。 - 使用`new`时,我们使用`std::forward`把传给`makeInvestment`的实参完美转发出去(查看[Item25](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md))。这使调用者提供的所有信息可用于正在创建的对象的构造函数。 - 自定义删除器的一个形参,类型是`Investment*`,不管在`makeInvestment`内部创建的对象的真实类型(如`Stock`,`Bond`,或`RealEstate`)是什么,它最终在*lambda*表达式中,作为`Investment*`对象被删除。这意味着我们通过基类指针删除派生类实例,为此,基类`Investment`必须有虚析构函数: ```cpp class Investment { public: … virtual ~Investment(); //关键设计部分! … }; ``` 在C++14中,函数的返回类型推导存在(参阅[Item3](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item3.md)),意味着`makeInvestment`可以以更简单,更封装的方式实现: ```cpp template auto makeInvestment(Ts&&... params) //C++14 { auto delInvmt = [](Investment* pInvestment) //现在在 { //makeInvestment里 makeLogEntry(pInvestment); delete pInvestment; }; std::unique_ptr //同之前一样 pInv(nullptr, delInvmt); if ( … ) //同之前一样 { pInv.reset(new Stock(std::forward(params)...)); } else if ( … ) //同之前一样 { pInv.reset(new Bond(std::forward(params)...)); } else if ( … ) //同之前一样 { pInv.reset(new RealEstate(std::forward(params)...)); } return pInv; //同之前一样 } ``` 我之前说过,当使用默认删除器时(如`delete`),你可以合理假设`std::unique_ptr`对象和原始指针大小相同。当自定义删除器时,情况可能不再如此。函数指针形式的删除器,通常会使`std::unique_ptr`的从一个字(*word*)大小增加到两个。对于函数对象形式的删除器来说,变化的大小取决于函数对象中存储的状态多少,无状态函数(stateless function)对象(比如不捕获变量的*lambda*表达式)对大小没有影响,这意味当自定义删除器可以实现为函数或者*lambda*时,尽量使用*lambda*: ```cpp auto delInvmt1 = [](Investment* pInvestment) //无状态lambda的 { //自定义删除器 makeLogEntry(pInvestment); delete pInvestment; }; template //返回类型大小是 std::unique_ptr //Investment*的大小 makeInvestment(Ts&&... args); void delInvmt2(Investment* pInvestment) //函数形式的 { //自定义删除器 makeLogEntry(pInvestment); delete pInvestment; } template //返回类型大小是 std::unique_ptr //Investment*的指针 makeInvestment(Ts&&... params); //加至少一个函数指针的大小 ``` 具有很多状态的自定义删除器会产生大尺寸`std::unique_ptr`对象。如果你发现自定义删除器使得你的`std::unique_ptr`变得过大,你需要审视修改你的设计。 工厂函数不是`std::unique_ptr`的唯一常见用法。作为实现**Pimpl Idiom**(译注:*pointer to implementation*,一种隐藏实际实现而减弱编译依赖性的设计思想,《Effective C++》条款31对此有过叙述)的一种机制,它更为流行。代码并不复杂,但是在某些情况下并不直观,所以这安排在[Item22](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item22.md)的专门主题中。 `std::unique_ptr`有两种形式,一种用于单个对象(`std::unique_ptr`),一种用于数组(`std::unique_ptr`)。结果就是,指向哪种形式没有歧义。`std::unique_ptr`的API设计会自动匹配你的用法,比如`operator[]`就是数组对象,解引用操作符(`operator*`和`operator->`)就是单个对象专有。 你应该对数组的`std::unique_ptr`的存在兴趣泛泛,因为`std::array`,`std::vector`,`std::string`这些更好用的数据容器应该取代原始数组。`std::unique_ptr`有用的唯一情况是你使用类似C的API返回一个指向堆数组的原始指针,而你想接管这个数组的所有权。 `std::unique_ptr`是C++11中表示专有所有权的方法,但是其最吸引人的功能之一是它可以轻松高效的转换为`std::shared_ptr`: ```cpp std::shared_ptr sp = //将std::unique_ptr makeInvestment(arguments); //转为std::shared_ptr ``` 这就是`std::unique_ptr`非常适合用作工厂函数返回类型的原因的关键部分。 工厂函数无法知道调用者是否要对它们返回的对象使用专有所有权语义,或者共享所有权(即`std::shared_ptr`)是否更合适。 通过返回`std::unique_ptr`,工厂为调用者提供了最有效的智能指针,但它们并不妨碍调用者用其更灵活的兄弟替换它。(有关`std::shared_ptr`的信息,请转到[Item19](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md)。) **请记住:** - `std::unique_ptr`是轻量级、快速的、只可移动(*move-only*)的管理专有所有权语义资源的智能指针 - 默认情况,资源销毁通过`delete`实现,但是支持自定义删除器。有状态的删除器和函数指针会增加`std::unique_ptr`对象的大小 - 将`std::unique_ptr`转化为`std::shared_ptr`非常简单