智能指针的概念和实现

Book Cover

概念

智能指针从字面上看,首先是一个对象,而它的行为(或接口)是在模拟一个指针,但又比指针更加智能。指针的主要作用是引用资源,指针的最大问题是在复杂情况下很难管理好它指向的资源的生命周期。因此智能指针要做的就是在管理资源生命周期这件事情上更加“智能”。C++11已经在<memory>模块中提供了常用的三种智能指针,它们分别是 unique_ptr, shared_ptrweak_ptr

但理解智能指针的原理和实现细节也是十分有意义的。了解原理可以保证更加合理的使用标准提供的智能指针,保证功能和效率;标准STL的指针就像其他STL工具一样,针对绝对大多数的生产场景保证功能和效率,但是在个别情况下可能不是最佳选择,过于臃肿拖沓,有时甚至无法使用(个别不支持C++11嵌入式开发,比如TI的TMS320系列编译器),如果你能够理解实现细节,就能实现裁剪版本或是修改版本的智能指针,保证效率和所需功能的最大化追求。

按照 Andrei Alexandrescu 在《现代C++设计》 一书中的思路,首先列出智能指针的策略点,就是可变化的功能块,再在下一部分描述实现时的注意事项。

  • 存储:智能指针首先要模拟通用指针,因此需要做到通用指针的最重要功能就,即存储资源:->,* 操作符和对原始资源的访问控制;
  • 所有权:所谓智能的部分就是所有权管理,或者叫做资源生命周期管理;
  • 值语义:由于不同于通用指针,它是一个对象,就具有了“值语义”,需要合理实现相等、比较、类型转换等操作;
  • 错误、异常管理;

实现

存储

智能指针内部需要有真正的资源“指针”,可以是 handle、文件 handle、原始指针,通常就是模板参数中那个最主要的类型 T,还需要实现 operator-> 和解引用的 operator*,以及是否向外界暴露真正的资源“指针”。操作符重载的方式都比较容易理解和实现,但对于暴露原始“指针”,C++ 标准将其作为成员函数shared_ptr::get,不见得是最好的方式。最好是能够用友元全局函数,毕竟指针指向对象的接口中难免也会有get函数,通过成员函数暴露原始资源指针,容易造成代码阅读的困扰。此外在销毁资源时,默认调用的是T的析构函数,也可以在template中提供接口来负责销毁资源。

所有权

所有权的变化体现在构造函数、析构函数、拷贝构造和赋值操作符的行为上,也是标准库提供的三种最重要智能指针的原因。在《现代C++设计》中提出的所有权策略更多,包括深拷贝、写时拷贝、引用计数、引用链表和破坏式拷贝。

  • unique_ptr : 不允许拷贝和赋值;只能够在构造函数中初始化,析构中销毁资源(破坏式拷贝);
  • shared_ptr : 允许拷贝和赋值,并在其中记录所有指向资源的智能指针;在资源初始化赋值给第一个智能指针时开始记录引用了该资源的智能指针,在最后一个指向该资源的智能指针的析构函数中销毁资源和用于记录结构(引用计数拷贝);
  • weak_ptr : 作为shared_ptr的观察者,用在shared_ptr指针系统中可能存在循环引用的地方,防止“你指向我,我指向你”导致的内存泄漏。因此它在的构造函数需要接收一个shared_ptr或者是weak_ptr (拷贝构造),而不能直接接受资源“指针”。而在拷贝、赋值时候需要能够持续监视shared_ptr内部的资源和记录结构,在析构时无论怎样都不销毁资源;

值语义

  • 首先不建议直接暴露原始资源,也就是不建议实现隐式类型转换为原始指针。如果非要这样,就需要禁止 delete 直接作用于智能指针,不然就不叫智能指针了(通过void*隐式转换,使得delete无法确认析构的确切调用方法);
  • 相等性操作符,等于和不等于,应该尽量保证和资源“指针”的含义一致。比如裸指针的相等判断的作用是判断是否是同一个资源;首先不能实现到bool的隐式类型转换,不然相等性不等性比较操作 (a == b; a != b)就会变得无效,因为操作符两边的智能指针先隐式转换为bool再进行等于与否的比较,显然不是希望的逻辑。由此带来的问题就是不能再出现这样的代码 if (p),顶多可以通过重载!操作符来实现这样的操作 if(!p)。同时为了能够比较基类和继承的子类,最好还要实现模板类式的比较操作。
  • 次序比较操作,原则上当你要比较两个指针的大小时,比较的是内存地址,因此也就意味着这是一个有先后、连续性开辟的内存空间,而这种内存空间往往不不需要进行资源的开辟和析构的智能管理。所以原则上不应该实现智能指针的加减、大小比较操作。如果非要实现,比较操作可以只实现 <>操作符,其他的都有它们来间接实现。因此可以先实现其它的操作符,留下<>的全局比较操作,由用户来决定是否实现次序比较。

错误异常管理

需要决定再构造函数中是否能够接收空指针,这也决定了在->操作符中是否要进行预检指针是否为空。在->操作符中进行检测,在频繁使用->操作符时候对性能的影响会逐渐明显;

参考实现

关于《现代 C++设计》

Andrei Alexandreu 在此书中将对象分解位一个个的策略点,我认为可以理解为变化点,将这些策略点抽象成为接口类,然后通过模板将这些抽象接口组装成为对象。通过实现自己的实体策略类,就可以组装出功能表现不同但语义相同的对象。这种方式灵活性非常大,性能也比多重继承高,但是对当时的编译器提出了太大的挑战,并没有成为太主流的设计方法。其实在设计上和代码的可读性上是非常有优势的,但是C++的模板奇淫技巧太多,给这种设计方式的学习和传播制造了太大的障碍。

猜你喜欢

转载自www.cnblogs.com/songyuncen/p/12076162.html