More Effective C++ 阅读笔记(十三)--要求或禁止在堆中产生对象

 More Effective C++ 阅读笔记(十三)--要求或禁止在堆中产生对象
黑月亮 发表于 2005-10-5 16:13:00 

要求或禁止在堆中产生对象

怎样判断对象是否在堆中

    抽象基类是不能被实例化的基类,也就是至少具有一个纯虚函数的基类。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类的实现很简单,调用全局的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++库的其它部分不很熟悉,代码还是很一目了然。条款M35将解释这里的每件东西,不过代码里的注释已经能够解释这个例子是如何运行的。
    只有一个地方可能让你感到困惑,就是这个语句(在isOnHeap函数中)
      const void *rawAddress = dynamic_cast<const void*>(this);
    我前面说过带有多继承或虚基类的对象会有几个地址,这导致编写全局函数isSafeToDelete会很复杂。这个问题在isOnHeap中仍然会遇到,但是因为isOnHeap仅仅用于HeapTracked对象中,我们能使用dynamic_cast操作符(参见条款M2)的一种特殊的特性来消除这个问题。只需简单地放入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函数这点(参见条款M8),来达到目的。你可以自己声明这个函数,而且你可以把它声明为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,而把operator delete声明为public,这样做有些怪异,所以除非有绝对需要的原因,否则不要把它们分开声明,最好在类的一个部分里声明它们。如果你也想禁止UPNumber堆对象数组,可以把operator new[]和operator delete[](参见条款M8)也声明为private。(operator new和operator delete之间的联系比大多数人所想象的要强得多。有关它们之间关系的鲜为人知的一面,可以参见我的文章counting objects里的sidebar部分。)
     有趣的是,把operator new声明为private经常会阻碍UPNumber对象做为一个位于堆中的派生类对象的基类被实例化。因为operator new和operator delete是自动继承的,如果operator new和operator delete没有在派生类中被声明为public(进行改写,overwrite),它们就会继承基类中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

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值