More Effective C++----技巧 & (25)将构造函数和非成员函数虚拟化

技巧

本书涉及的大多数内容都是编程的指导准则。这些准则虽是重要的,但是程序员不能单靠准则生活。有一个很早以前的卡通片叫做“菲利猫”(Felix the Cat), 菲利猫无论何时遇到困难,它都会拿它的trick包。如果一个卡通角色都有一个trick包,那么C++程序员就更应该有了。把这一章想成你的trick包的启动器。

当设计C++软件时,总会再三地受到一些问题的困扰。你如何让构造函数和非成员函数具有虚拟函数的特点?你如何限制一个类的实例的数量?你如何防止在堆中建立对象呢?你如何又能确保把对象建立在堆中呢?其它一些类的成员函数无论何时被调用,你如何能建立一个对象并让它自动地完成一些操作?你如何能让不同的对象共享数据结构,而让每个使用者以为它们每一个都拥有自己的拷贝?你如何区分operator[]的读操作和写操作?你如何建立一个虚函数,其行为特性依赖于不同对象的动态类型?

所有这些问题(还有更多)都在本章得到解答,在本章里我叙述的都是C++程序员普遍遇到的问题,且解决方法也是已被证实了的。我把这些解决方法叫做技巧,不过当它们以程式化的风格(stylized fashion)被归档时,也被做为idiom和pattern。不管你把它称做什么,在你日复一日地从事软件开发工作时,下面这些信息都将使你受益。它会使你相信无论你做什么,总可以用C++来完成它。

Item M25:将构造函数和非成员函数虚拟化

从字面来看,谈论“虚拟构造函数”没有意义。当你有一个指针或引用,但是不知道其指向对象的真实类型是什么时,你可以调用虚拟函数来完成特定类型(type-specific)对象的行为。仅当你还没拥有一个对象但是你又确切地知道想要的对象的类型时,你才会调用构造函数。那么虚拟构造函数又从何谈起呢?

很简单。尽管虚拟构造函数看起来好像没有意义,其实它们有非常大的用处(如果你认为没有意义的想法就没有用处,那么你怎么解释现代物理学的成就呢?)(因为现代物理学的主要成就是狭义、广义相对论,量子力学,这些理论看起来都好象很荒谬,不好理解。译者注)。例如,假设你编写一个程序,用来进行新闻报道的工作,每一条新闻报道都由文字或图片组成。你可以这样管理它们:

class NLComponent {               //用于 newsletter components
public:                           // 的抽象基类
  ...                             //包含至少一个纯虚函数
};
class TextBlock: public NLComponent {
public:
  ...                             // 不包含纯虚函数
};
class Graphic: public NLComponent {
public:
  ...                             // 不包含纯虚函数
};
class NewsLetter {                // 一个 newsletter 对象
public:                           // 由NLComponent 对象
  ...                             // 的链表组成
private:
  list<NLComponent*> components;
};

类之间的关系图

在NewsLetter中使用的list类是一个标准模板类(STL),STL是标准C++类库的一部分(参见Effective C++条款49和条款M35)。list类型对象的行为特性有些象双向链表,尽管它没有以这种方法来实现。对象NewLetter不运行时就会存储在磁盘上。为了能够通过位于磁盘的替代物来建立Newsletter对象,让NewLetter的构造函数带有istream参数是一种很方便的方法。当构造函数需要一些核心的数据结构时,它就从流中读取信息:

class NewsLetter {
public:
  NewsLetter(istream& str);
  ...
};

此构造函数的伪代码是这样的:

NewsLetter::NewsLetter(istream& str)
{
  while (str) {
    从str读取下一个component对象;
    把对象加入到newsletter的 components对象的链表中去;
  }
}

或者,把这种技巧用于另一个独立出来的函数叫做readComponent,如下所示:

class NewsLetter {
public:
  ...
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。因为它能建立新对象,它的行为与构造函数相似,而且因为它能建立不同类型的对象,我们称它为虚拟构造函数虚拟构造函数是指能够根据输入给它的数据的不同而建立不同类型的对象。虚拟构造函数在很多场合下都有用处,从磁盘(或者通过网络连接,或者从磁带机上)读取对象信息只是其中的一个应用。(WQ加注:readComponent()的实现可详见《汤姆.斯旺C++编程秘诀》)

还有一种特殊种类的虚拟构造函数――虚拟拷贝构造函数――也有着广泛的用途。虚拟拷贝构造函数能返回一个指针,指向调用该函数的对象的新拷贝。因为这种行为特性,虚拟拷贝构造函数的名字一般都是copySelf,cloneSelf或者是象下面这样就叫做clone。很少会有函数能以这么直接的方式实现它:

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
  ...
};

正如我们看到的,类的虚拟拷贝构造函数只是调用它们真正的拷贝构造函数。因此“拷贝”的含义与真正的拷贝构造函数相同。如果真正的拷贝构造函数只做了简单的拷贝,那么虚拟拷贝构造函数也做简单的拷贝。如果真正的拷贝构造函数做了全面的拷贝,那么虚拟拷贝构造函数也做全面的拷贝。如果真正的拷贝构造函数做一些奇特的事情,象引用计数或copy-on-write(参见条款M29),那么虚拟构造函数也这么做。完全一致,太棒了。

注意上述代码的实现利用了最近才被采纳的较宽松的虚拟函数返回值类型规则。被派生类重定义的虚拟函数不用必须与基类的虚拟函数具有一样的返回类型。如果函数的返回类型是一个指向基类的指针(或一个引用),那么派生类的函数可以返回一个指向基类的派生类的指针(或引用)。这不是C++的类型检查上的漏洞,它使得有可能声明象虚拟构造函数这样的函数。这就是为什么TextBlock的clone函数能够返回TextBlock*和Graphic的clone能够返回Graphic*的原因,即使NLComponent的clone返回值类型为NLComponent*。

C++ Primer 第五版中指出:派生类中虚函数的返回类型必须也必须与基类函数相匹配,但是该规则存在一个例外,当类的虚函数返回类型是类本身的指针或者引用时,上述规则无效。就如上述例子所示。基类中的虚函数在派生类中隐含的也是一个虚函数,当派生类覆盖了某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配!

在NLComponent中的虚拟拷贝构造函数能让实现NewLetter的(正常的)拷贝构造函数变得很容易:

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

如果你对标准模板库(STL)不熟悉,这段代码可能有些令人费解,不过原理很简单:遍历被拷贝的NewsLetter对象中的整个component链表,调用链表内每个元素对象的虚拟(拷贝)构造函数。我们在这里需要一个虚拟构造函数,因为链表中包含指向NLComponent对象的指针,但是我们知道其实每一个指针不是指向TextBlock对象就是指向Graphic对象。无论它指向谁,我们都想进行正确的拷贝操作,虚拟构造函数能够为我们做到这点。

虚拟化非成员函数

就象构造函数不能真的成为虚拟函数一样,非成员函数也不能成为真正的虚拟函数(参见Effective C++ 条款19)。然而,既然一个函数能够构造出不同类型的新对象是可以理解的,那么同样也存在这样的非成员函数,可以根据参数的不同动态类型而其行为特性也不同。例如,假设你想为TextBlock和Graphic对象实现一个输出操作符。显而易见的方法是虚拟化这个输出操作符。但是输出操作符是operator<<,函数把ostream&做为它的左参数(left-hand
argument)(即把它放在函数参数列表的左边 译者注),这就不可能使该函数成为TextBlock 或 Graphic成员函数。

(这样做也可以,不过看一看会发生什么:

class NLComponent {
public:
  // 对输出操作符的不寻常的声明
  virtual ostream& operator<<(ostream& str) const = 0;
  ...
};
class TextBlock: public NLComponent {
public:
  // 虚拟输出操作符(同样不寻常)
  virtual ostream& operator<<(ostream& str) const;
};
class Graphic: public NLComponent {
public:
  // 虚拟输出操作符 (让就不寻常)
  virtual ostream& operator<<(ostream& str) const;
};
TextBlock t;
Graphic g;
...
t << cout;                                  // 通过virtual operator<<
                                            //把t打印到cout中。
                                            // 不寻常的语法
g << cout;                                  //通过virtual operator<<
                                            //把g打印到cout中。
                                            //不寻常的语法

类的使用者得把stream对象放到<<符号的右边,这与输出操作符一般的用法相反。为了能够回到正常的语法上来,我们必须把operator<<移出TextBlock 和 Graphic类,但是如果我们这样做,就不能再把它声明为虚拟了。)

另一种方法是为打印操作声明一个虚拟函数(例如print)把它定义在TextBlock 和 Graphic类里。但是如果这样,打印TextBlock 和 Graphic对象的语法就与使用operator<<做为输出操作符的其它类型的对象不一致了,这些解决方法都不很令人满意。我们想要的是一个称为operator<<的非成员函数,其具有象print虚拟函数的行为特性。有关我们想要什么的描述实际上已经很接近如何得到它的描述。我们定义operator<<
和print函数,让前者调用后者!

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);
}

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

现在你知道如何根据它们的一个参数让非成员函数虚拟化,你可能想知道是否可能让它们根据一个以上的参数虚拟化呢?可以,但是不是很容易。有多困难呢?参见条款M31;它将专门论述这个问题。

时间: 2024-11-07 21:11:37

More Effective C++----技巧 & (25)将构造函数和非成员函数虚拟化的相关文章

将构造函数以及非成员函数 “虚化”

虚构造函数,这似乎是很违反直观 的一件事情,因为当你拥有一个对象的指针或者引用的时候,你可以通过该对象的动态类型来调用虚函数,而在此处,你的对象还没有构造完成. 其实,"虚构造函数"并不是把你的构造函数声明为virtual,而是某一种函数,根据其输入数据,来决定此时构造什么类型的对象.比如你要从文件或者网络上读取一些数据,从该数据你来决定你要创建什么类型的对象. 比如: 下述的继承体系:[摘自More Effective C++ P124] class Base { public: .

Effective C++ Item 25 考虑写出一个不抛异常的swap函数

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 经验:当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常 示例: stl里的swap算法 namespace std{ template<typename T> void swap(T &a, T &b){ T temp(a); a = b; b = temp; } } //"pimpl手法"(pointer

构造函数与普通函数的比较

在js中,用new关键字来调用定义的构造函数.默认返回的是一个新的对象具有构造函数定义的变量和方法. 先来写个构造函数的例子: function Prince(name,age){   this.gender="male";   this.kind=true;   this.rich=true;   this.name=name;   this.age=age; } Prince.prototype.toFrog=function(){   console.log("Prin

C++语言基础(22)-转换构造函数和类型转换函数

一.转换构造函数 将其它类型转换为当前类类型需要借助转换构造函数(Conversion constructor).转换构造函数也是一种构造函数,它遵循构造函数的一般规则.转换构造函数只有一个参数. #include <iostream> using namespace std; //复数类 class Complex{ public: Complex(): m_real(0.0), m_imag(0.0){ } Complex(double real, double imag): m_real

java构造代码块,构造函数和普通函数的区别和调用时间

在这里我们谈论一下构造代码块,构造函数和普通函数的区别和调用时间.构造代码块:最早运行,比构造函数运行的时间好要提前,和构造函数一样,只在对象初始化的时候运行.构造函数:运行时间比构造代码块时间晚,也是在对象初始化的时候运行.没有返回值,构造函数名称和类名一致.普通函数:不能自动调用,需要对象来调用,例如a.add();如果只看代码运行先后顺序的话:构造代码块>构造函数>普通函数下面给一个程序 1 public class Test1 { 2 3 public static void main

拷贝构造函数和赋值函数的一些知识

/*******************拷贝构造函数和赋值运算符重载有以下两个不同之处***************************/ 1.拷贝构造函数生成新的类对象,而赋值运算符不能. 2.由于拷贝构造函数是直接构造一个新的类对象,所以在初始化这个对象之前不用检验源对象是否和新对象相同,而复制操作符需要这个操作,另外赋值运算符中如果原来对象中有内存分配,要先把内存释放掉. 下面是String类的一个实现的部分函数,可以看出二者的区别. 1 class String{ 2 public:

[概念] javascript构造函数和普通函数的

[概念] javascript构造函数和普通函数的 javascript构造函数和普通函数的区别是什么: 调用方式的区别: 构造函数需要使用new运算符调用,如果构造函数没有参数可以省略小括号,比如new Object. 普通函数的调用不需要new运算符,而且必须要有小括号. 关于new的作用可以参阅js的new运算符的作用简单介绍一章节. this的指向问题: 构造函数的this会被绑定到创建的对象实例上. 普通函数的this则属于此函数的调用者. 命名方式: 构造函数名称通常首字母要大些.

不要轻视拷贝构造函数与赋值函数

由于并非所有的对象都会使用拷贝构造函数和赋值函数,程序员可能对这两个函数 有些轻视.请先记住以下的警告,在阅读正文时就会多心: 本章开头讲过,如果不主动编写拷贝构造函数和赋值函数,编译器将以"位拷贝" 的方式自动生成缺省的函数.倘若类中含有指针变量,那么这两个缺省的函数就隐 含了错误.以类 String 的两个对象 a,b 为例,假设 a.m_data 的内容为"hello", b.m_data 的内容为"world". 现将 a 赋给 b,缺省

C++ 拷贝构造函数与赋值函数的区别(很严谨和全面)

这里我们用类String 来介绍这两个函数: 拷贝构造函数是一种特殊构造函数,具有单个形参,该形参(常用const修饰)是对该类类型的引用.当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式使用拷贝构造函数.为啥形参必须是对该类型的引用呢?试想一下,假如形参是该类的一个实例,由于是传值参数,我们把形参复制到实参会调用拷贝构造函数,如果允许拷贝构造函数传值,就会在拷贝构造函数内调用拷贝构造函数,从而形成无休止的递归调用导致栈溢出. 赋值函数,也是赋值操作符重载,因为赋值必须作为类成员,那