More Effective C++ 条款27(下)

类别:编程语言 点击:0 评论:0 推荐:

条款27:要求或禁止在堆中产生对象(下)

到目前为止,这种逻辑很正确,但是不够深入。最根本的问题是对象可以被分配在三个地方,而不是两个。是的,栈和堆能够容纳对象,但是我们忘了静态对象。静态对象是那些在程序运行时仅能初始化一次的对象。静态对象不仅仅包括显示地声明为static的对象,也包括在全局和命名空间里的对象(参见条款47)。这些对象肯定位于某些地方,而这些地方既不是栈也不是堆。

它们的位置是依据系统而定的,但是在很多栈和堆相向扩展的系统里,它们位于堆的底端。先前内存管理的图片到讲述的是事实,不过是很多系统都具有的事实,但是没有告诉我们这些系统全部的事实,加上静态变量后,这幅图片如下所示:

onHeap不能工作的原因立刻变得很清楚了,不能辨别堆对象与静态对象的区别:

void allocateSomeObjects()

{

  char *pc = new char;               // 堆对象: onHeap(pc)

                                     // 将返回true

 

  char c;                            // 栈对象: onHeap(&c)

                                     // 将返回false

 

  static char sc;                    // 静态对象: onHeap(&sc)

                                     // 将返回true

  ...

 

}

现在你可能不顾一切地寻找区分堆对象与栈对象的方法,在走头无路时你想在可移植性上打主意,但是你会这么孤注一掷地进行一个不能获得正确结果的交易么?绝对不会。我知道你会拒绝使用这种虽然诱人但是不可靠的“地址比对”技巧。

令人伤心的是不仅没有一种可移植的方法来判断对象是否在堆上,而且连能在多数时间正常工作的“准可移植”的方法也没有。如果你实在非得必须判断一个地址是否在堆上,你必须使用完全不可移植的方法,其实现依赖于系统调用,只能这样做了。因此你最好重新设计你的软件,以便你可以不需要判断对象是否在堆中。

如果你发现自己实在为对象是否在堆中这个问题所困扰,一个可能的原因是你想知道对象是否能在其上安全调用delete。这种删除经常采用“delete this”这种声明狼籍的形式。不过知道“是否能安全删除一个指针”与“只简单地知道一个指针是否指向堆中的事物”不一样,因为不是所有在堆中的事物都能被安全地delete。再考虑包含UPNumber对象的Asset对象:

class Asset {

private:

  UPNumber value;

  ...

 

};

 

Asset *pa = new Asset;

很明显*pa(包括它的成员value)在堆上。同样很明显在指向pa->value上调用delete是不安全的,因为该指针不是被new返回的。

幸运的是“判断是否能够删除一个指针”比“判断一个指针指向的事物是否在堆上”要容易。因为对于前者我们只需要一个operator new返回的地址集合。因为我们能自己编写operator new函数(参见Effective C++条款8—条款10),所以构建这样一个集合很容易。如下所示,我们这样解决这个问题:

void *operator new(size_t size)

{

  void *p = getMemory(size);         //调用一些函数来分配内存,

                                     //处理内存不够的情况

 

  把 p加入到一个被分配地址的集合;

 

  return p;

 

}

 

void operator delete(void *ptr)

{

  releaseMemory(ptr);                // return memory to

                                     // free store

 

  从被分配地址的集合中移去ptr;

}

 

bool isSafeToDelete(const void *address)

{

  返回address是否在被分配地址的集合中;

}

这很简单,operator new在地址分配集合里加入一个元素,operator delete从集合中移去项目,isSafeToDelete在集合中查找并确定某个地址是否在集合中。如果operator new 和 operator delete函数在全局作用域中,它就能适用于所有的类型,甚至是内建类型。

在实际当中,有三种因素制约着对这种设计方式的使用。第一是我们极不愿意在全局域定义任何东西,特别是那些已经具有某种含义的函数,象operator new和operator delete。正如我们所知,只有一个全局域,只有一种具有正常特征形式(也就是参数类型)的operator new和operator delete 。这样做会使得我们的软件与其它也实现全局版本的operator new 和operator delete的软件(例如许多面向对象数据库系统)不兼容。

 

我们考虑的第二个因素是效率:如果我们不需要这些,为什么还要为跟踪返回的地址而负担额外的开销呢?

最后一点可能有些平常,但是很重要。实现isSafeToDelete让它总能够正常工作是不可能的。难点是多继承下来的类或继承自虚基类的类有多个地址,所以无法保证传给isSafeToDelete的地址与operator new 返回的地址相同,即使对象在堆中建立。有关细节参见条款24和条款31。

我们希望这些函数提供这些功能时能够不污染全局命名空间,没有额外的开销,没有正确性问题。幸运的是C++使用一种抽象mixin基类满足了我们的需要。

抽象基类是不能被实例化的基类,也就是至少具有一个纯虚函数的基类。mixin(mix in)类提供某一特定的功能,并可以与其继承类提供的其它功能相兼容(参见Effective C++条款7)。这种类几乎都是抽象类。因此我们能够使用抽象混合(mixin)基类给派生类提供判断指针指向的内存是否由operator new分配的能力。该类如下所示:

class HeapTracked {                  // 混合类; 跟踪

public:                              // 从operator new返回的ptr

 

  class MissingAddress{};            // 异常类,见下面代码

 

  virtual ~HeapTracked() = 0;

 

  static void *operator new(size_t size);

  static void operator delete(void *ptr);

 

  bool isOnHeap() const;

 

private:

  typedef const void* RawAddress;

  static list<RawAddress> addresses;

};

这个类使用了list(链表)数据结构跟踪从operator new返回的所有指针,list标准C++库的一部分(参见Effective C++条款49和本书条款35)。operator new函数分配内存并把地址加入到list中;operator delete用来释放内存并从list中移去地址元素。isOnHeap判断一个对象的地址是否在list中。

HeapTracked类的实作(我觉得把implementation翻译成“实作”更好 译者注)很简单,调用全局的operator new和operator delete函数来完成内存的分配与释放,list类里的函数进行插入操作和删除操作,并进行单语句的查找操作。以下是HeapTracked的全部实作:

// mandatory definition of static class member

list<RawAddress> HeapTracked::addresses;

 

// HeapTracked的析构函数是纯虚函数,使得该类变为抽象类。

// (参见Effective C++条款14). 然而析构函数必须被定义,

//所以我们做了一个空定义。.

HeapTracked::~HeapTracked() {}

 

 

 

void * HeapTracked::operator new(size_t size)

{

  void *memPtr = ::operator new(size);  // 获得内存

 

  addresses.push_front(memPtr);         // 把地址放到list的前端

  return memPtr;

}

 

void HeapTracked::operator delete(void *ptr)

{

  //得到一个 "iterator",用来识别list元素包含的ptr;

  //有关细节参见条款35

  list<RawAddress>::iterator it =

    find(addresses.begin(), addresses.end(), ptr);

 

  if (it != addresses.end()) {       // 如果发现一个元素

    addresses.erase(it);             //则删除该元素

    ::operator delete(ptr);          // 释放内存

  } else {                           // 否则

    throw MissingAddress();          // ptr就不是用operator new

  }                                  // 分配的,所以抛出一个异常

}

 

bool HeapTracked::isOnHeap() const

{

  // 得到一个指针,指向*this占据的内存空间的起始处,

  // 有关细节参见下面的讨论

  const void *rawAddress = dynamic_cast<const void*>(this);

 

  // 在operator new返回的地址list中查到指针

  list<RawAddress>::iterator it =

    find(addresses.begin(), addresses.end(), rawAddress);

 

  return it != addresses.end();      // 返回it是否被找到

}

尽管你可能对list类和标准C++库的其它部分不很熟悉,代码还是很一目了然。条款35将解释这里的每件东西,不过代码里的注释已经能够解释这个例子是如何运行的。

只有一个地方可能让你感到困惑,就是这个语句(在isOnHeap函数中)

const void *rawAddress = dynamic_cast<const void*>(this);

我前面说过带有多继承或虚基类的对象会有几个地址,这导致编写全局函数isSafeToDelete会很复杂。这个问题在isOnHeap中仍然会遇到,但是因为isOnHeap仅仅用于HeapTracked对象中,我们能使用dynamic_cast操作符的一种特殊的特性来消除这个问题。只需简单地放入dynamic_cast,把一个指针dynamic_cast成void*类型(或const void*或volatile void* 。。。。。),生成的指针指向“原指针指向对象内存”的开始处。但是dynamic_cast只能用于“指向至少具有一个虚拟函数的对象”的指针上。我们该死的isSafeToDelete函数可以用于指向任何类型的指针,所以dynamic_cast也不能帮助它。isOnHeap更具有选择性(它只能测试指向HeapTracked对象的指针),所以能把this指针dynamic_cast成const void*,变成一个指向当前对象起始地址的指针。如果HeapTracked::operator new为当前对象分配内存,这个指针就是HeapTracked::operator new返回的指针。如果你的编译器支持dynamic_cast 操作符,这个技巧是完全可移植的。

使用这个类,即使是最初级的程序员也可以在类中加入跟踪堆中指针的功能。他们所需要做的就是让他们的类从HeapTracked继承下来。例如我们想判断Assert对象指针指向的是否是堆对象:

class Asset: public HeapTracked {

private:

  UPNumber value;

  ...

};

我们能够这样查询Assert*指针,如下所示:

void inventoryAsset(const Asset *ap)

{

  if (ap->isOnHeap()) {

    ap is a heap-based asset — inventory it as such;

  }

  else {

    ap is a non-heap-based asset — record it that way;

  }

}

象HeapTracked这样的混合类有一个缺点,它不能用于内建类型,因为象int和char这样的类型不能继承自其它类型。不过使用象HeapTracked的原因一般都是要判断是否可以调用”delete this”,你不可能在内建类型上调用它,因为内建类型没有this指针。

禁止堆对象

判断对象是否在堆中的测试到现在就结束了。与此相反的领域是“禁止在堆中建立对象”。通常对象的建立这样三种情况:对象被直接实例化;对象做为派生类的基类被实例化;对象被嵌入到其它对象内。我们将按顺序地讨论它们。

禁止客户端直接实例化对象很简单,因为总是调用new来建立这种对象,你能够禁止客户端调用new。你不能影响new操作符的可用性(这是内嵌于语言的),但是你能够利用new操作符总是调用operator new函数这点(参见条款8),来达到目的。你可以自己声明这个函数,而且你可以把它声明为private.。例如,如果你想不想让客户端在堆中建立UPNumber对象,你可以这样编写:

class UPNumber {

private:

  static void *operator new(size_t size);

  static void operator delete(void *ptr);

  ...

};

现在客户端仅仅可以做允许它们做的事情:

UPNumber n1;                         // okay

 

static UPNumber n2;                  // also okay

 

UPNumber *p = new UPNumber;          // error! attempt to call

                                     // private operator new

把operator new声明为private就足够了,但是把operator new声明为private,而把iperator delete声明为public,这样做有些怪异,所以除非有绝对需要的原因,否则不要把它们分开声明,最好在类的一个部分里声明它们。如果你也想禁止UPNumber堆对象数组,可以把operator new[]和operator delete[](参见条款8)也声明为private。(operator new和operator delete之间的联系比大多数人所想象的要强得多。有关它们之间关系的鲜为人知的一面,可以参见我的文章counting objects里的sidebar部分。)

有趣的是,把operator new声明为private经常会阻碍UPNumber对象做为一个位于堆中的派生类对象的基类被实例化。因为如果operator new和operator delete没有在派生类中被声明为public,它们就会被继承下来,继承了基类private函数的类,如下所示:

class UPNumber { ... };             // 同上

 

class NonNegativeUPNumber:          //假设这个类

  public UPNumber {                 //没有声明operator new

  ...

};

 

NonNegativeUPNumber n1;             // 正确

 

static NonNegativeUPNumber n2;      // 也正确

 

NonNegativeUPNumber *p =            // 错误! 试图调用

  new NonNegativeUPNumber;          // private operator new

如果派生类声明它自己的operator new,当在堆中分配派生对象时,就会调用这个函数,必须得找到一种不同的方法防止UPNumber基类部分缠绕在这里。同样,UPNumber的operator new是private这一点,不会对分配包含做为成员的UPNumber对象的对象产生任何影响:

class Asset {

public:

  Asset(int initValue);

  ...

 

private:

  UPNumber value;

};

 

Asset *pa = new Asset(100);          // 正确, 调用

                                     // Asset::operator new 或

                                     // ::operator new, 不是

                                     // UPNumber::operator new

实际上,我们又回到了这个问题上来,即“如果UPNumber对象没有被构造在堆中,我们想抛出一个异常”。当然这次的问题是“如果对象在堆中,我们想抛出异常”。正像没有可移植的方法来判断地址是否在堆中一样,也没有可移植的方法判断地址是否不在堆中,所以我们很不走运,不过这也丝毫不奇怪,毕竟如果我们能辨别出某个地址在堆上,我们也就能辨别出某个地址不在堆上。但是我们什么都不能辨别出来。

本文地址:http://com.8s8s.com/it/it29838.htm