您的位置:首页 > 编程语言 > C语言/C++

More Effective C++ 读书摘要(五、技巧1)Item25 - 27

2009-04-19 21:04 706 查看
Item25. 使构造函数和非成员函数具有虚函数的行为:

有三个类,注意其继承关系:
class NLComponent {               //用于 newsletter components 
public:                           // 的抽象基类
  ...                             //包含只少一个纯虚函数
};   

class TextBlock: public NLComponent {
public:
  ...                             // 不包含纯虚函数
};  

class Graphic: public NLComponent {
public:
  ...                             // 不包含纯虚函数
};

class NewsLetter {                // 一个 newsletter 对象
public:                           // 由NLComponent 对象
  ...                             // 的链表组成
private:
  list<NLComponent*> components;
};


其构造函数如下:

class NewsLetter {
public:
  NewsLetter(istream& str);
  ...
private:
  // 为建立下一个NLComponent对象从str读取数据,
  // 建立component 并返回一个指针。
  static NLComponent * readComponent(istream& str);
  ...
};

NewsLetter::NewsLetter(istream& str)
{
  while (str) {
    // 把readComponent返回的指针添加到components链表的最后,
    // "push_back" 一个链表的成员函数,用来在链表最后进行插入操作。
    components.push_back(readComponent(str));
  }
}


readComponent所做的工作是:它根据所读取的数据建立了一个新对象,或是TextBlock或是Graphic。因为它能建立新对象,它的行为与构造函数相似,而且因为它能建立不同类型的对象,我们称它为虚拟构造函数。虚拟构造函数是指能够根据输入给它的数据的不同而建立不同类型的对象。



还有一种特殊种类的虚拟构造函数――虚拟拷贝构造函数,也有着广泛的用途。虚拟拷贝构造函数能返回一个指针,指向调用该函数的对象的新拷贝:

class NLComponent {
public:
  // declaration of virtual copy constructor
  virtual NLComponent * clone() const = 0;
  ...
};

class TextBlock: public NLComponent {
public:
  virtual TextBlock * clone() const         // virtual copy
  { return new TextBlock(*this); }          // constructor
  ...
};

class Graphic: public NLComponent {
public:
  virtual Graphic * clone() const            // virtual copy
  { return new Graphic(*this); }             // constructor
  ...
};




类的虚拟拷贝构造函数只是调用它们真正的拷贝构造函数。因此”拷贝”的含义与真正的拷贝构造函数相同(浅拷贝或深拷贝、是否引用计数)。注意上述代码的实现利用了最近才被采纳的较宽松的虚拟函数返回值类型规则。被派生类重定义的虚拟函数不用必须与基类的虚拟函数具有一样的返回类型。



NLComponent有了虚拷贝构造函数以后,要为NewLetter实现一个的(常规的)拷贝构造函数变得很容易:

class NewsLetter {
public:
  NewsLetter(const NewsLetter& rhs);
  ...
private:
  list<NLComponent*> components;
};

NewsLetter::NewsLetter(const NewsLetter& rhs)
{
  // 遍历整个rhs链表,使用每个元素的虚拟拷贝构造函数
  // 把元素拷贝进这个对象的component链表。
  // 有关下面代码如何运行的详细情况,请参见条款35.
  for (list<NLComponent*>::const_iterator it =
          rhs.components.begin();
       it != rhs.components.end();
       ++it) {
    // "it" 指向rhs.components的当前元素,调用元素的clone函数,
    // 得到该元素的一个拷贝,并把该拷贝放到
    //这个对象的component链表的尾端。
    components.push_back((*it)->clone());
  }
}




让非成员函数具有虚函数的行为

class NLComponent {
public:
  virtual ostream& print(ostream& s) const = 0;
  ...
};

class TextBlock: public NLComponent {
public:
  virtual ostream& print(ostream& s) const;
  ...
};

class Graphic: public NLComponent {
public:
  virtual ostream& print(ostream& s) const;
  ...
};

inline ostream& operator<<(ostream& s, const NLComponent& c)
{
  return c.print(s);
}


具有虚拟行为的非成员函数很简单。你编写一个虚拟函数来完成工作,然后再写一个非虚拟函数,它什么也不做只是调用这个虚拟函数。为了避免这个句法花招引起函数调用开销,你可以内联这个非虚函数。



Item26. 限制类对象的个数:

允许一个或零个对象

class Printer {
public:
  static Printer& thePrinter();
  ...
private:
  Printer();
  Printer(const Printer& rhs);
  ...
};

Printer& Printer::thePrinter()
{
  static Printer p;
  return p;
}




客户端使用printer时有些繁琐:
Printer::thePrinter().reset();
Printer::thePrinter().submitJob(buffer);



另一种办法是把thePrinter移出全局域,放入namespace(命名空间)

namespace PrintingStuff {
  class Printer {                          // 在命名空间 
  public:                                 // PrintingStuff中的类
    void submitJob(const PrintJob& job);
    void reset();
    void performSelfTest();
    ...
    friend Printer& thePrinter();
  private:
    Printer();
    Printer(const Printer& rhs);
    ...
};

Printer& thePrinter()                   // 这个函数也在命名空间里
 {
    static Printer p;
    return p;
 }
}                                             // 命名空间到此结束


在thePrinter的实现上有两个微妙的不引人注目的地方,值得我们看一看。



第一,单独的Printer是位于函数里的静态成员而不是在类中的静态成员,这样做是非常重要的。在类中的一个静态对象实际上总是被构造(和释放),即使不使用该对象。与此相反,只有第一次执行函数时,才会建立函数中的静态对象,所以如果没有调用函数,就不会建立对象。(不过你得为此付出代价,每次调用函数时都得检查是否需要建立对象。)与一个函数的静态成员相比,把Printer声明为类中的静态成员还有一个缺点,它的初始化时间不确定。



第二个细微之处是内联与函数内静态对象的关系。不要建立包含局部静态数据的非成员内联函数。
也可以只需简单地计算对象的数目,一旦需要太多的对象,就抛出异常,这样做也许会更好。如下所示,这样建立printer对象:

class Printer {
public:
  class TooManyObjects{};                   // 当需要的对象过多时
                                                      // 就使用这个异常类  
  Printer();
  ~Printer();
    ...
private:
  static size_t numObjects;
  Printer(const Printer& rhs);           // 这里只能有一个printer,
                                                   // 所以不允许拷贝
};                                  // (参见Effective C++ 条款27)


以下为实现:

// Obligatory definition of the class static
size_t Printer::numObjects = 0;
Printer::Printer()
{
  if (numObjects >= 1) {
    throw TooManyObjects();
}
//继续运行正常的构造函数;
  ++numObjects;
}

Printer::~Printer()
{
//进行正常的析构函数处理;
  --numObjects;
}




不同情形下的对象创建
刚刚的Printer类在以下的情形下会有问题。

class CPFMachine {                           // 一种机器,可以复印,打印
private:                                           // 发传真。
  Printer p;                                      // 有打印能力
  FaxMachine f;                                 // 有传真能力
  CopyMachine c;                             // 有复印能力
  ...
};

CPFMachine m1;                        // 运行正常
CPFMachine m2;                         // 抛出 TooManyObjects异常


问题是Printer对象能存在于三种不同的环境中:只有它们本身;作为其它派生类的基类;被嵌入在更大的对象里。存在这些不同环境极大地混淆了跟踪“存在对象的数目” 的含义,因为你心目中的“对象的存在” 的含义与编译器不一致。



通常你仅会对允许对象本身存在的情况感兴趣,你希望限制这种实例(instantiation)的数量。如果你使用最初的Printer类示例的方法,就很容易进行这种限制,因为Printer构造函数是private,(不存在friend声明)带有private构造函数的类不能作为基类使用,也不能嵌入到其它对象中。



你不能从带有private构造函数的类派生出新类,这个事实衍生出了一种阻止派生类的通用方法,这种方法不需要和限制对象实例数量的方法一起使用。



现在考虑有一个类FSA,表示一个finite state automata(有限态自动机) 。(这种机器能用于很多环境下,比如用户界面设计),并假设你允许建立任意数量的对象,但是你想禁止从FSA派生出新类。(这样做的一个原因是表明在FSA中存在非虚析构函数。)

class FSA {
public:
  // 伪构造函数
  static FSA * makeFSA();
  static FSA * makeFSA(const FSA& rhs);
  ...
private:
  FSA();
  FSA(const FSA& rhs);
  ...
};

FSA * FSA::makeFSA()
{ return new FSA(); }

FSA * FSA::makeFSA(const FSA& rhs)
{ return new FSA(rhs); }


可以把makeFSA返回的指针存储在auto_ptr中(参见条款9);当它们自己退出生存空间时,这种对象能自动地删除它们所指向的对象:
// 间接调用缺省FSA构造函数
auto_ptr<FSA> pfsa1(FSA::makeFSA());

// indirectly call FSA copy constructor
auto_ptr<FSA> pfsa2(FSA::makeFSA(*pfsa1));
... // 象通常的指针一样使用pfsa1和pfsa2,
//不过不用操心删除它们。



允许对象来去自由

如前的方案使用thePrinter函数封装对单个对象的访问,以便把Printer对象的数量限制为一个,这样做的同时也会让我们在每一次运行程序时只能使用一个Printer对象。即不能编写以下的代码:

create Printer object p1;
use p1;
destroy p1;

create Printer object p2;
use p2;
destroy p2;


(thy:因为thePrinter管理的是一个静态对象。)



我们需要做的就是把先前使用的对象计数的代码与makePrinter()伪构造函数代码合并在一起:

class Printer {
public:
  class TooManyObjects{};
  // 伪构造函数
  static Printer * makePrinter();
  static Printer * makePrinter(const Printer& rhs);
  ...
private:
  static size_t numObjects;
  static const size_t maxObjects = 10;       // 见下面解释

  Printer();
  Printer(const Printer& rhs);
};

// Obligatory definitions of class statics
// 实现
size_t Printer::numObjects = 0;
const size_t Printer::maxObjects;

Printer::Printer()
{
  if (numObjects >= maxObjects) {
    throw TooManyObjects();
  }
  ...
}

Printer::Printer(const Printer& rhs)
{
  if (numObjects >= maxObjects) {
    throw TooManyObjects();
  }
  ...
}

Printer * Printer::makePrinter()
{ return new Printer; }

Printer * Printer::makePrinter(const Printer& rhs)
{ return new Printer(rhs); }




一个用于对象计数的基类

template<class BeingCounted>
class Counted {
public:
  class TooManyObjects{};                     // 用来抛出异常
  static int objectCount() { return numObjects; }

protected:
  Counted();
  Counted(const Counted& rhs);
  ~Counted() { --numObjects; }

private:
  static int numObjects;
  static const size_t maxObjects;
  void init();                                // 避免构造函数的
};                                            // 代码重复

template<class BeingCounted>
Counted<BeingCounted>::Counted()
{ init(); }

template<class BeingCounted>
Counted<BeingCounted>::Counted(const Counted<BeingCounted>&)
{ init(); }

template<class BeingCounted>
void Counted<BeingCounted>::init()
{
  if (numObjects >= maxObjects) throw TooManyObjects();
  ++numObjects;
}




现在能使用Counted模板修改Printer类:

class Printer: private Counted<Printer> {
public:
  // 伪构造函数
  static Printer * makePrinter();
  static Printer * makePrinter(const Printer& rhs);
  ~Printer();

  void submitJob(const PrintJob& job);
  void reset();
  void performSelfTest();
  ...
  using Counted<Printer>::objectCount;     // 参见下面解释
  using Counted<Printer>::TooManyObjects;  // 参见下面解释

private:
  Printer();
  Printer(const Printer& rhs);
};


Printer使用了Counter模板来跟踪存在多少Printer对象,因为除了Printer的编写者,没有人关心这个事实。它的实现细节最好是private,这就是为什么这里使用private继承的原因。



Counted所做的大部分工作对于Printer的客户端来说都是隐藏的,但是这些客户端可能很想知道有当前多少Printer对象存在。Counted模板提供了objectCount函数,用来提供这种信息,但是因为我们使用private继承,这个函数在Printer类中成为了private。为了恢复该函数的public访问权,我们使用using声明:

class Printer: private Counted<Printer> {
public:
  ...
  using Counted<Printer>::objectCount; // 让这个函数对于Printer
                                       //是public
  ...    
};


这里没有检测对象的数量是否已经超过限制,执行完构造函数后也没有增加存在对象的数目。所有这些现在都由Counted<Printer>的构造函数来处理,因为Counted<Printer>是Printer的基类,Counted<Printer>的构造函数总在Printer的前面被调用。如果建立过多的对象,Counted<Printer>的构造函数就会抛出异常,甚至都没有调用Printer的构造函数。



最后一个问题是应该初始化Counted<Printer>::maxObjects为多少呢?
简单的方法就是什么也不做。而是让此类的客户端提供合适的初始化。Printer的作者必须把这条语句加入到一个实现文件里:
const size_t Counted<Printer>::maxObjects = 10;
如果这些作者忘了对maxObjects进行初始化,连接时就会发生错误,因为maxObjects没有被定义。如果我们提供了充分的文档对Counted客户端说明了需求,他们会回去加上这个必须的初始化。



Item27. 要求或禁止对象分配在堆上:



要求在堆中建立对象

一种最直接的方法是让析构函数成为private,让构造函数成为public。通过引入一个专用的伪析构函数,用来访问真正的析构函数。客户端调用伪析构函数释放他们建立的对象。

class UPNumber {
public:
  UPNumber();
  UPNumber(int initValue);
  UPNumber(double initValue);
  UPNumber(const UPNumber& rhs);

  // 伪析构函数 (一个const 成员函数, 因为
  // 即使是const对象也能被释放。)
  void destroy() const { delete this; }
   ...
private:
  ~UPNumber();
};


然后客户端这样进行程序设计:

UPNumber n;                      // 错误! (在这里合法, 但是
                                     // 当它的析构函数被隐式地
                                     // 调用时,就不合法了)
UPNumber *p = new UPNumber;          //正确
...
delete p;                            // 错误! 试图调用
                                     // private 析构函数
p->destroy();                        // 正确


通过限制访问一个类的析构函数或它的构造函数来阻止建立非堆对象,但是在条款26已经说过,这种方法也禁止了继承和包含(containment)。

class UPNumber { ... };              // 声明析构函数或构造函数
                                     // 为private

class NonNegativeUPNumber:
  public UPNumber { ... };           // 错误! 析构函数或
                                     //构造函数不能编译

class Asset {
private:
  UPNumber value;
  ...                                // 错误! 析构函数或
                                     //构造函数不能编译
};


通过把UPNumber的析构函数声明为protected(同时它的构造函数还保持public)就可以解决继承的问题,需要包含UPNumber对象的类可以修改为包含指向UPNumber的指针:

class UPNumber { ... };              // 声明析构函数为protected

class NonNegativeUPNumber:
  public UPNumber { ... };           // 现在正确了; 派生类
                                             // 能够访问
                                            // protected 成员
class Asset {
public:
  Asset(int initValue);
  ~Asset();
  ...

private:
  UPNumber *value;
};

Asset::Asset(int initValue)
: value(new UPNumber(initValue))      // 正确
{ ... }

Asset::~Asset()
{ value->destroy(); }                 // 也正确




判断对象是否在堆中

如果想利用一个在很多系统上存在的事实,程序的地址空间被做为线性地址管理,程序的栈从地址空间的顶部向下扩展,堆则从底部向上扩展,来判断某个特定的地址是否在堆中:

// 不正确的尝试,来判断一个地址是否在堆中
bool onHeap(const void *address)
{
  char onTheStack;                   // 局部栈变量
  return address < &onTheStack;
}


到目前为止,这种逻辑很正确,但是不够深入。最根本的问题是对象可以被分配在三个地方,而不是两个。是的,栈和堆能够容纳对象,但是我们忘了静态对象。它们的位置是依据系统而定的,但是在很多栈和堆相向扩展的系统里,它们位于堆的底端。onHeap不能工作的原因立刻变得很清楚了,不能辨别堆对象与静态对象的区别。

令人伤心的是不仅没有一种可移植的方法来判断对象是否在堆上,而且连能在多数时间正常工作的“准可移植”的方法也没有。(作者后来确信基于签名的技巧是一种直接明了的方法:http://www.aristeia.com/BookErrata/M27Comments.html



其实研究对象是否在堆中这个问题,一个可能的原因是你想知道对象是否能在其上安全调用delete。幸运的是“判断是否能够删除一个指针”比“判断一个指针指向的事物是否在堆上”要容易一些。



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

抽象基类是不能被实例化的基类,也就是至少具有一个纯虚函数的基类。mixin(mix in)类提供某一特定的功能,并可以与其继承类提供的其它功能相兼容)。这种类几乎都是抽象类。因此我们能够使用抽象混合(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;
};


下面是其实现:

// 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是否被找到
}


代码还是很一目了然。只有一个地方可能让你感到困惑,就是这个语句(在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 操作符,这个技巧是完全可移植的。



使用这个类,即使是最初级的程序员也可以在类中加入跟踪堆中指针的功能。他们所需要做的就是让他们的类从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;
  }
}




禁止对象分配在堆上

通常对象的建立这样三种情况:对象被直接实例化;对象做为派生类的基类被实例化;对象被嵌入到其它对象内。以下分别说明:



①禁止用户直接实例化对象:
可以这样编写:

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

如果你也想禁止UPNumber堆对象数组,可以把operator new[]和operator delete[](参见条款8)也声明为private。



②把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




③同样,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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: