在管理动态分配的内存时,一个最棘手的问题就是决定何时释放这些内存,而智能指针就是用来简化内存管理的编程方式。智能指针一般有独占和共享两种所有权模型。
------------------------------------------------------------------------------------------------------------
20.1 holder和trule
本节将介绍两种智能指针类型:holder类型独占一个对象;而trule可以使对象的拥有者从一个holder传递给另一个holder。

20.1.1 安全处理异常
正常情况下,程序只有一个入口一个出口,异常的出现使程序多了其他出口,导致程序可能会提前终止,对异常的不当使用会导致许多问题,特别是内存泄漏问题。即便我们可以通过异常处理机制来解决这种问题,但我们会发现异常执行路径会影响程序正常的执行路径了,并且对象的释放操作不得不在两个不同的地方执行:一个在正常执行路径,一个在异常执行路径。
对于动态分配的内存,只要遵循“谁申请谁释放”的原则,一般都不会导致内存泄漏,但异常的出现令这种内存管理变得更加复杂,智能指针旨在解决这个问题。
同时,通常都应该避免使用会抛出异常的析构函数,因为当一个异常被抛出的时候,析构函数都是被自动调用的;而此时如果再抛出另一个异常,那么将会导致程序立即中止。
智能指针的优点在于:我们可以很方便的管理动态分配的内存(不再需要在析构函数中释放对象),同时,也避免了抛出异常而导致的资源泄漏。

20.1.2 holder
智能指针会在下面两种情况下释放所指向的对象:本身被释放,或者把另一个指针赋值给它。下面我们模拟实现一个智能指针:

// pointers/holder.hpp

template<typename T>
class Holder
{
private:
T* ptr; // 引用它所持有的对象(前提是该对象存在)
public:
// 缺省构造函数:让该holder引用一个空对象
Holder() : ptr() { } // 针对指针的构造函数:让该holder引用该指针所指向的对象
// 这里使用explicit,禁止隐式转型(也即禁止了使用赋值语法来初始化Holder对象,如“holderObj = originObj”形式的赋值语法)
// 但依然可以通过对象构造的形式来给对象初始化,如"Holder holderObj(originObj)",这里是显式转型。
explicit Holder (T* p) : ptr(p) {} // 析构函数:释放所引用的对象(前提是该对象存在)
~Holder() {
delete ptr;
} // 针对新指针的赋值运算符
Holder<T>& operator= (T* p){
delete ptr;
ptr = p;
return *this;
} // 指针运算符
T& operator* () const {
return *ptr;
} T* operator-> () const {
return ptr;
} // 获取所引用的对象(前提是该对象存在)
T* get() const {
return ptr;
} // 释放对所引用对象的所有权
void release() {
ptr = ;
} // 与另一个holder交换所有权
void exchange_with(Holder<T>& h) {
swap(ptr, h.ptr);
} // 与其他的指针交换所有权
void exchange_twith(T*& p) { // 参数是什么语法?传入指针p的引用?
swap(ptr, p);
} private:
// 不想外提供拷贝构造函数和拷贝赋值运算符
// 不允许一个Holder对象A赋值给另一个Holder对象B.
Holder(Holder<T> const&);
Holder<T>& operator= (Holder<T> const&);
};

从语义上讲,该holder独占ptr所引用对象的所有权。而且,这个对象一定要用new操作来创建,因为在销毁holder所拥有对象的时候,需要用到delete。接下来,release()成员函数释放holder对其持有对象的所有权。另外,上面的普通赋值运算符也设计得比较巧妙,它会销毁和释放任何被拥有的对象,因为另一个对象会替代原先的对象被holder所拥有,而且赋值运算符也不会返回原先对象的一个holder或指针(而是返回新对象的一个holder)。最后,我们添加了两个exchange_with()成员函数,从而可以在不销毁原有对象的前提下,方便地替换该holder所拥有的对象。
所以,我们可以如下使用上面的Holder创建两个对象:

void do_two_things()
{
Holder<Something> first(new Something);
firsh->perform(); Holder<Something> second(new Something);
second->perform();
}

20.1.3 作为成员的holder
我们也可以在类中使用holder来避免资源泄漏。要注意的是,只有那些完成构造之后的对象,它的析构函数才会被调用。因此,如果在构造函数内部产生异常,那么只有那些构造函数已正常执行完毕的成员对象,它的析构函数才会被调用。

// pointers/refmem2.hpp

#include "holder.hpp"

class RefMembers
{
private:
Holder<MemType> ptr1; // 所引用的成员
Holder<MemType> ptr2; public:
// 缺省构造函数
// - 不可能出现资源泄漏
RefMembers() : ptr1(new MemType), ptr2(new MemType) { } // 拷贝构造函数
// - 不可能出现资源泄漏
RefMembers (RefMembers const& x) : ptr1(new MemType(*x.ptr1)), ptr2(new MemType(*x.ptr2)) { } // 赋值运算符
const RefMembers& operator= (RefMembers const& x){
*ptr1 = *x.ptr1;
*ptr2 = *x.ptr2;
return *this;
} // 不需要析构函数
// (缺省的析构函数将会让ptr1和ptr2删除它们所引用的对象)
...
};

要注意的是,我们在这里可以省略用户定义的析构函数,但一定要编写拷贝构造函数和赋值运算符

20.1.4 资源获取于初始化

Holder所用到的基本思想是一种称为“资源获取去初始化”或RAII的模式(RAII在博文xxxxx有相关讲解,可供参考)。

20.1.5 hodler局限
包括 20.1.6 和 20.1.7两小节,介绍了holder在参数传递,返回返回值处理时的不足之处,以及复制holder、跨函数调用来复制holder所会产生的问题(这部分内容在博文xxxx中有相关讲解,可供参考)。并引出下一节trule的内容。

20.1.8 trule
为了解决上一小节留下的问题,我们引进了一个专门用于传递holder的辅助类模板,并把它称为trule。在语言中,它是一个术语,来自于transfer capsule的缩写。下面是其定义:

// pointers/trule.hpp

#ifndef TRULE_HPP
#define TRULE_HPP template <typename T>
class Holder; template <typename T>
class Trule
{
private:
T* ptr; // trule所引用的对象(如果有的话) public:
// 构造函数,确保trule只能作为返回类型,用于将holder从被调用函数传递给调用函数
// 显式构造函数(会自动屏蔽默认无参构造函数),只能通过Holder构造Trule对象
Trule (Holder<T>& h){
ptr = h.get();
h.release();
} // 拷贝构造函数
// 这里,trule通常是作为那些想传递holders的函数的返回类型,也就是
      说trule对象总是作为临时对象(rvalues,右值)出现;因此它们的类型也就只能是
      常引用(reference-to-const)类型。
Trule (Trule<T> const& t){
ptr = t.ptr;
// 由于Trule不能作为一份拷贝,也不能含有一份拷贝,如果我们希望实现类似于拷贝操作,
        就必须移除原trule的所有权。我们是通过将被封装指针置为空来实现这种移除操作的。而最后
        这个置空操作显然只能针对non-const对象,所以才有了这种把const强制转型为non-const的做法。
// 另外,由于原来的对象实际上并没有被定义为常类型,所以即使这样做有些别扭,但在这种情况下这种转型却能合法地实现。
// 因此,对于最后需要把一个holder转换为trule,并且将其返回的函数,如果要声明这类函数的
          返回类型,我们就必须把它声明为trule<T>类型,而绝对不能声明为trule<T> const,
          这点需特别注意。如下面例子中的函数load_something()
const_cast<Trule<T>&>(t).ptr = ; // 置空操作
} // 析构函数
~Trule() {
delete ptr;
} private:
// 对于trule的用法,除了作为传递holder对象的返回类型,我们要防止把它用于其他地方。
      于是,一个接收non-const引用对象的拷贝构造函数和一个类似的拷贝赋值运算符,都被声
      明为私用函数,防止外界直接调用。通过禁止将trule作为左值的方法,因为左值允许取
        址和赋值操作,这种特性容易导致其用于其他地方而没有报错。
Trule(Trule<T>&);
Trule<T>& operator= (Trule<T>&); // 禁止拷贝赋值
friend class Holder<T>;
}; #endif // TRULE_HPP

还有一点需要注意的是,上面的代码并不完全是把一个holder完全转换为一个trule:如果是这样的话,holder就必须是一个可修改的左值。这也是我们为什么要使用一个单独的类型来实现trule,而不是将它的功能合并到holder类模板中的原因。

最后,对于上面实现的trule,只有被holder模板所辨识并且使用之后,才能算是完整的。如下:

// pointers/holder2.hpp

template <typename T>
class Holder
{
// 前面已经定义的成员
... public:
Holder(Trule<T> const& t){
ptr = t.ptr;
const_cast<Trule<T>&>(t).ptr = ;
} Holder<T>& operator= (Trule<T> const& t) {
delete ptr;
ptr = t.ptr;
const_cast<Trule<T>&>(t).ptr = ;
return *this;
}
};

为了充分演示对holder/trule作了哪些改善,我们可以重写load_something()例子,如下:

// pointers/truletest.cpp

#include "holder2.hpp"
#include "trule.hpp" class Something
{
}; void read_something(Something* x)
{
} // 返回类型为Trule<Something>,通过将Holder<Something>转换成返回类型(也即,通过trule传递返回值)
Trule<Something> load_something()
{
Holder<Something> result(new Something);
read_something(result.get());
return result;
} int main()
{
// 接收load_something函数返回的Trule<Something>类型的值,并通过Holder内部接收Trule对象的构造函数初始化Holder对象ptr
Holder<Something> ptr(load_something());
....
}

20.2 引用计数
设计一个引用计数的智能指针,基本思想是:对于每个被指向的对象,都保存一个计数,用于代表指向该对象的指针的个数,当计数值减少到0时,就删除此对象。
我们首先面对的问题是:计算器在什么地方?这里可以有两种方式,一种是把计算器放在对象中,但如果对象早期已经设计好,则无法再把计算器放入对象;另一种也是通常会使用的就是使用专用的(内存)分配器。
我们面对的第二个问题是:对象的析构和释放。我们有可能会需要使用非标准方式(比如C的free(),或者delete[]运算符释放对象数组)来释放对象,故而,我们还需要指定一种单独的对象(释放)policy。
对于大多数用CountingPtr计数的对象,我们可以使用下面这个简单的对象policy:

// pointers/stdobjpolicy.hpp
class StandardObjectPolicy
{
public:
template<typename T> void dispose(T* object){
delete object;
}
}; // pointers/stdarraypolicy.hpp
class StandardArrayPolicy
{
public:
template<typename T> void dispose(T* array){
delete[] array;
}
};

在考虑了上面两个问题之后,我们现在开始定义我们的CountingPtr模板:

// pointers/countingptr.hpp

template <typename T,
typename CounterPolicy = SimpleReferenceCount, // 计算器的policy
typename ObjectPolicy = StandardObjectPolicy> // 对象(释放)policy
class CountingPrt : private CounterPolicy, private ObjectPolicy
{
private:
// typedef 两个简单的别名
typedef CountPolicy CP;
typedef ObjectPolicy OP; T* object_pointer_to; // 所引用的对象
// 如果没有引用任何对象,则为NULL public:
// 缺省构造函数(没有显式初始化,即没有加上explicit关键字)
CountingPtr(){
this->object_pointed_to = NULL;
} // 一个针对转型的构造函数(转型自一个内建的指针)
explicit CountingPtr(T* p) {
this->init(p); // 使用普通指针初始化
} // 拷贝构造函数
CountingPtr(CountingPtr<T, CP, OP> const& cp)
: CP((CP const&)cp), // 拷贝policy
OP((OP const&)cp){
this->attach(cp); // 拷贝指针,并增加计数值
} // 析构函数
~CountingPtr(){
this->detach(); // 减少计数值,如果计数值为0,则释放该计数器
} // 针对内建指针的赋值运算符
CountingPtr<T, CP, OP>& operator= (T* p){
// 计数指针不能指向*p
assert(p != this->object_pointed_to);
this->detach(); // 减少计数值,如果计数值为0,则释放该计数器 this->init(p); // 用一个普通指针进行初始化
return *this;
} // 拷贝赋值运算符(要考虑自己给自己赋值)
CountingPtr<T, CP, OP>&
operator= (CountingPtr<T, CP, OP> const& cp){
if(this->object_pointed_to != cp.object_pointed_to){
this->detach(); // 减少计数值,如果计数值为0,则释放该计数器 CP::operator=((CP const&)cp); // 对policy进行赋值
OP::operator=((OP const&)op);
this->attach(cp); // 拷贝指针并增加计数值
}
return *this;
} // 使之成为智能指针的运算符
T* operator->() const {
return this->object_pointed_to;
} T& operator* () const {
return *this->object_pointed_to;
} // 以后在这里将可能会增加一些其他的接口
.... private:
// 辅助函数
// - 用普通指针进行初始化(前提是普通指针存在)
void init(T* p){
if (p != NULL)
{
CounterPolicy::init(p);
}
this->object_pointed_to = p;
} // - 拷贝指针并且增加计数值(前提是指针存在)
void attach(CountingPtr<T, CP, OP> const& cp){
this->object_pointed_to = cp.object_pointed_to;
if (cp.object_pointed_to != NULL)
{
CounterPolicy::increment(cp.object_pointed_to);
}
} // - 减少计数值(如果计数值为0, 则释放计数器)
void detach(){
if (this->object_pointed_to != NULL)
{
CounterPolicy::decrement(this->object_pointed_to);
if (CounterPolicy::is_zero(this->object_pointed_to))
{
// 如果有必要的话,释放计数器
CounterPolicy::dispose(this->object_pointed_to);
// 使用object policy来释放所指向的对象
ObjectPolicy::dispose(this->object_pointed_to);
}
}
}
};

上面代码需要注意:
(1)在拷贝赋值操作中,要判断是否为自赋值;
(2)由于空指针并没有一个可关联的计数器,所以在减少计数值之前,必须先显式地检查空指针的情况;
(3)在前面的代码中,我们使用继承来包含两种policy。这样做确保了在policy类为空的情况下,并不需要占用存储空间(前提是我们的编译器实现了空基类优化);

20.2.5 一个简单的非侵入式计数器
从总体看来,我们已经完成了CountingPtr的设计,下面我们需要为计数policy编写代码。
于是,我们先来看一个针对计数器的policy,它并不把计数器存储于所指向对象的内部,也就是说,它是一种非侵入式的计数器policy(或者称为非插入式的计数器policy)。对于计数器而言,最主要的问题是如何分配存储空间。事实上,同一个计数器需要被多个CountingPtr所共享;因此,它的生命周期必须持续到最后一个智能指针被释放之后。通常而言,我们会使用一种特殊的分配器来完成这种任务,这种分配器专门用于分配大小固定的小对象。

// pointers/simplerefcount.hpp

#include <stddef.h> // 用于size_t的定义
#include "allocator.hpp" class SimpleReferenceCount
{
private:
size_t* counter; // 已经分配的计数器
public:
SimpleReferenceCount(){
counter = NULL;
} // 缺省的拷贝构造函数和拷贝赋值运算符都是允许的
// 因为它们只是拷贝这个共享的计数器
public:
// 分配计数器,并把它的值初始为1
template <typename T> void init(T*) {
Counter = alloc_counter();
*counter = ;
} // 释放该计数器
template <typename T> void dispose(T*) {
dealloc_counter(counter);
} // 计数值加1
template<typename T> void increment(T*){
++*counter;
} // 计数值减1
template<typename T> void decrement(T*){
--*counter;
} // 检查计数值是否为0
template<typename T> bool is_zero(T*){
return *counter == ;
}
};

20.2.6 一个简单的侵入式计数器模板
侵入式(或插入式)计数器policy就是将计数器放到被管理对象本身的类型中(或者可能存放到由被管理对象所控制的存储空间中)。显然,这种policy通常需要在设计对象类型的时候就加以考虑;因此这种方案很可能会专用于被管理对象的类型。

// pointers/memberrefcount.hpp

template<typename ObjectT,        // 包含计数器的类型
typename CountT, // 计数器的类型
CountT Object::*CountP> // 计数器的位置,需要在设计ObjectT对象的时候就考虑到计数器
class MemberReferenceCount
{
public:
// 缺省构造函数和析构函数都是允许的 // 让计数器的值初始化为1
void init(ObjectT* object){
object->*CountP = ;
} // 对于计数器的释放,并不需要显式执行任何操作
void dispose(ObjectT*){ } // 计数器加1
void increment(ObjectT* object){
++object->*CountP;
} // 计数器减1
void increment(ObjectT* object){
--object->*CountP;
} // 检查计数值是否为0
template<typename T> bool is_zero(ObjectT* object){
return object->*CounP == ;
}
};

如果使用这种policy的话,那么在类的实现中,就可以很快地写出类的引用计数指针类型。其中类的设计框架大概如下:

class ManagedType
{
private:
size_t ref_count;
public:
typedef CountingPtr<ManagedType,
MemberReferenceCount
<ManagedType, // 包含计数器的对象类型
size_t, // 计数器类型
&ManagedType::ref_count> >
Ptr;
....
};

有了上面这个定义之后,我们就可以使用ManageeType::Ptr,方面地引用“那些用于访问ManagedType对象的”引用计数指针类型(在此为智能指针类型CountingPtr)。

书中还介绍了关于智能指针的其他一些功能实现,包括常数性相关内容、隐式转型,以及比较等等,有兴趣自行查阅学习,这里不介绍。

C++ template —— 智能指针(十二)的更多相关文章

  1. 【C++】智能指针简述(二):auto_ptr

    首先,我要声明auto_ptr是一个坑!auto_ptr是一个坑!auto_ptr是一个坑!重要的事情说三遍!!! 通过上文,我们知道智能指针通过对象管理指针,在构造对象时完成资源的分配及初始化,在析 ...

  2. c++智能指针《二》 std::tr1::shared_ptr

    转载http://www.cnblogs.com/kadinzhu/archive/2011/12/12/2284826.html 看<effective c++>,作者一直强调用std: ...

  3. Rust 智能指针(二)

    1. Rc<T> 引用计数指针 Rc<T> 是引用计数指针,可以使用clone使得指针所指向的数据具有多个所有者. enum List { Cons(i32, Rc<Li ...

  4. PCL智能指针疑云 &lt;二&gt; 使用同一智能指针作为PCL预处理API的输入和输出

    问题介绍: slam构建地图,先进行降采样,再进行可视化或存储.然而经过降采样后,代码没有报错的情况下,点云数据散成一团.将代码和点云数据展示如下, pcl::VoxelGrid<Lidar:: ...

  5. C++ 引用计数技术及智能指针的简单实现

    一直以来都对智能指针一知半解,看C++Primer中也讲的不够清晰明白(大概是我功力不够吧).最近花了点时间认真看了智能指针,特地来写这篇文章. 1.智能指针是什么 简单来说,智能指针是一个类,它对普 ...

  6. 第十二章Fundamental Data Types 基本数据类型

    目录: 12.1 数值概论 12.2整数 12.3浮点数 12.4 字符和字符串 12.5布尔变量 12.6枚举类型 12.7具名常量 12.8数组 12.9创建你自己的类型 12.1   数值概论 ...

  7. C++ Primer : 第十二章 : 动态内存之shared_ptr与new的结合使用、智能指针异常

    shared_ptr和new结合使用 一个shared_ptr默认初始化为一个空指针.我们也可以使用new返回的指针来初始化一个shared_ptr: shared_ptr<double> ...

  8. C++ template的一些高级用法(元编码,可变参数,仿函数,using使用方法,. C++ 智能指针)

    1 .  通用函数可变参数模板 对于有些时候,我们无法确切的知道,函数的参数个数时,而又不想过多的使用所谓的函数重载,那么就可以效仿下面的例子: #include<iostream> #i ...

  9. C++ 智能指针二

    /* 智能指针shared_ptr注意点 */ #include <iostream> #include <string> #include <memory> // ...

随机推荐

  1. iOS - OC 语言新特性

    前言 相对于 Java,OC 语言是一门古老的语言了,而它又是一门不断发展完善的语言.一些新的编译特性,为 OC 语言带来了许多新的活力.在 Xcode7 中,iOS9 的 SDK 已经全面兼容了 O ...

  2. Excel加载期间出现问题 解决方案

         今天在处理Excle表格的时候出现了如图所示的问题,资料比较重要,需要进行恢复:       出现问题的原因就是在制作的时候,产生了某些临时的htm文件,但是只保留了excel,将那些临时文 ...

  3. Android开发:向下一个activity传递数据,返回数据给上一个activity

    1.向下一个activity传递数据 activity1 Button button=(Button) findViewById(R.id.button1); button.setOnClickLis ...

  4. scrapy pipelines导出各种格式

    scrapy在使用pipelines的时候,我们经常导出csv,json.jsonlines等等格式.每次都需要写一个类去导出,很麻烦. 这里我整理一个pipeline文件,支持多种格式的. # -* ...

  5. docker hub加速访问设置

    前言:docker是国外的,虽然有个版本开源免费,但是其docker  hub还是在国外,刚刚安装后,拉镜像就会发现,连接请求超时,中断服务,这是因为防火墙的问题,所以要将源指向国内的知名docker ...

  6. MyBatis探究-----核心配置文件mybatis-config.xml中配置mappers的几种方式

    1.package name="映射文件所在包名" 必须保证接口名(例如IUserDao)和xml名(IUserDao.xml)相同,还必须在同一个包中 例如:<packag ...

  7. 深入理解之css中的border属性

    1. border-width:不支持不百分比 1)受本身的使用场景决定. 例子:左边为手机,右边为显示器,但是他们边框的宽度是差不多的,不会因为设备大就让边框宽度变大. 2. border-widt ...

  8. Prometheus-自定义Node_Exporter

    标量(Scalar):一个浮点型的数字值 标量只有一个数字,没有时序. 需要注意的是,当使用表达式count(http_requests_total),返回的数据类型,依然是瞬时向量.用户可以通过内置 ...

  9. js的一些总结

    函数 函数是一等公民 函数可以有属性,并且能连接到它的构造方法 函数可以像一个变量一样存在内存中 函数可以当做参数传给其他函数 函数可以返回其他函数 加法操作表 Number + Number -&g ...

  10. css outline实践研究

    outline具有和border很相似的属性,但多少又有些区别,就是因为这些区别才让它闪闪发光,先目睹一下. <style> div{ width:100px; height:100px; ...