天天看点

More Effective C++ 25:将构造函数和非成员函数虚拟化

假设你编写一个程序,用来进行新闻报道的工作,每一条新闻报道都由文字

或图片组成。你可以这样管理它们:

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

类之间的关系图

More Effective C++ 25:将构造函数和非成员函数虚拟化

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 链表的最后,  
		components.push_back(readComponent(str)); 
	} 
}
           

readComponent

所做的工作:根据所读取的数据建立了一个新对象,或是

TextBlock

或是

Graphic

。因为它能建立新对象,它的行为与构造函数相似,而且因为它能建立不同类型的对象,我们称它为虚拟构造函数,虚拟构造函数是指能够根据输入给它的数据的不同而建立不同类型的对象。

还有一种特殊种类的虚拟构造函数――虚拟拷贝构造函数――也有着广泛的用途。虚拟拷贝构造函数能返回一个指针,指向调用该函数的对象的新拷贝。因为这种行为特性,虚拟拷贝构造函数的名字一般都是

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 { return new TextBlock(*this); } 	  // virtual copy  constructor 
	... 
}; 
class Graphic: public NLComponent 
{ 
public: 
	virtual Graphic * clone() const // virtual copy 
	{ return new Graphic(*this); } // constructor 
	... 
};
           

正如我们看到的,类的虚拟拷贝构造函数只是调用它们真正的拷贝构造函数。因此“拷贝”的含义与真正的拷贝构造函数相同。如果真正的拷贝构造函数只做了简单的拷贝,那么虚拟拷贝构造函数也做简单的拷贝。如果真正的拷贝构造函数做了全面的拷贝,那么虚拟拷贝构造函数也做全面的拷贝。如果真正的拷贝构造函数做一些奇特的事情,像引用计数或

copy-on-write

,那么虚拟构造函数也这么做。

NLComponent

中的虚拟拷贝构造函数能让实现

NewLetter

的(正常的)拷贝构造函数变得很容易:

class NewsLetter 
{ 
public: 
	NewsLetter(const NewsLetter& rhs); 
	... 
private: 
	list<NLComponent*> components; 
}; 
NewsLetter::NewsLetter(const NewsLetter& rhs) 
{ 
	 // 遍历整个 rhs 链表,使用每个元素的虚拟拷贝构造函数  把元素拷贝进这个对象的 component 链表。 
	 for (list<NLComponent*>::const_iterator it = rhs.components.begin(); it != rhs.components.end(); ++it) 
	 { 
		 components.push_back((*it)->clone()); 
	 } 
}
           

就像构造函数不能真的成为虚拟函数一样,非成员函数也不能成为真正的虚拟函数。

然而,既然一个函数能够构造出不同类型的新对象是可以理解

的,那么同样也存在这样的非成员函数,可以根据参数的不同动态类型而其行为特性也不同。例如,假设你想为

TextBlock

Graphic

对象实现一个输出操作符。显而易见的方法是虚拟化这个输出操作符。但是输出操作符是

operator<<

,函数把

ostream&

做为它的左参数(即把它放在函数参数列表的左边),这就不可能使该函数成为

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

类,但是如果我们这样做,就不能再把它声明为虚拟了。)

为了解决这个问题,我们可以定义

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

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

继续阅读