虚析构函数、纯虚析构函数、虚构造函数

一. 虚析构函数

我们知道,为了能够正确的调用对象的析构函数,一般要求具有层次结构的顶级类定义其析构函数为虚函数。因为在delete一个抽象类指针时候,必须要通过虚函数找到真正的析构函数。

如:

class
 Base
{

public
:
   Base(){}
   

virtual
 
~
Base(){}
};


class
 Derived: 
public
 Base
{

public
:
   Derived(){ 《虚析构函数、纯虚析构函数、虚构造函数》};
   

~
Derived(){ 《虚析构函数、纯虚析构函数、虚构造函数》};
}


void
 foo()
{
   Base 

*
pb;
   pb 

=
 
new
 Derived;
   delete pb;
这是正确的用法,会发生动态绑定,它会先调用Derived的析构函数,然后是Base的析构函数

如果析构函数不加virtual,delete pb只会执行Base的析构函数,而不是真正的Derived析构函数。

因为不是virtual函数,所以调用的函数依赖于指向静态类型,即Base

二. 纯虚析构函数

现在的问题是,我们想把Base做出抽象类,不能直接构造对象,需要在其中定义一个纯虚函数。如果其中没有其他合适的函数,可以把析构函数定义为纯虚的,即将前面的CObject定义改成:

class
 Base
{

public
:
   Base(){}
   

virtual
 
~
Base()
=
 
0

};
可是,这段代码不能通过编译,通常是link错误,不能找到~Base()的引用(gcc的错误报告)。这是因为,析构函数、构造函数和其他内部函数不一样,在调用时,编译器需要产生一个调用链。也就是,Derived的析构函数里面隐含调用了Base的析构函数。而刚才的代码中,缺少~Base()的函数体,当然会出现错误。

这里面有一个误区,有人认为,virtual f()=0这种纯虚函数语法就是没有定义体的语义。

其实,这是不对的。这种语法只是表明这个函数是一个纯虚函数,因此这个类变成了抽象类,不能产生对象。我们
完全可以为纯虚函数指定函数体 (http://www.research.att.com/~bs/bs_faq2.html#pure-virtual)。通常的纯虚函数不需要函数体,是因为我们一般不会调用抽象类的这个函数,只会调用派生类的对应函数。这样,我们就有了一个纯虚析构函数的函数体,上面的代码需要改成:

class
 Base
{

public
:
   Base()
   {
   }
   

virtual
 
~
Base() 
=
 
0

//
pure virtual


};

Base::
~Base
()
//
function body


{

从语法角度来说,不可以将上面的析构函数直接写入类声明中(内联函数的写法)。这或许是一个不正交化的地方。但是这样做的确显得有点累赘

这个问题看起来有些学术化,因为一般我们完全可以在Base中找到一个更加适合的函数,通过将其定义为没有实现体的纯虚函数,而将整个类定义为抽象类。但这种技术也有一些应用,如这个例子:

class
 Base  
//
abstract class


{

public
:
   

virtual
 
~
Base(){};
//
virtual, not pure


   
virtual
 
void
 Hiberarchy() 
const
 
=
 
0
;
//
pure virtual


};


void
 Base::Hiberarchy() 
const
 
//
pure virtual also can have function body


{
   std::cout 

<<

Base::Hiberarchy

;
}


class
 Derived : 
public
 Base
{

public
:
   Derived(){}
   

virtual
 
void
 Hiberarchy() 
const

   {
       CB::Hiberarchy();
       std::cout 

<<

Derived::Hiberarchy

;
   }
   

virtual
 
void
 foo(){}
};


int
 main(){
   Base

*
 pb
=
new
 Derived();
   pb

->
Hiberarchy();
   pb

->
Base::Hiberarchy();
   

return
 
0
;

在这个例子中,我们试图打印出类的继承关系。在根基类中定义了虚函数Hiberarchy,然后在每个派生类中都重载此函数。我们再一次看到,由于想把Base做成个抽象类,而这个类中没有其他合适的方法成员可以定义为纯虚的,我们还是只好将Hiberarchy定义为纯虚的。(当然,完全可以定义~Base函数,这就和上面的讨论一样了。^_^)

另外,可以看到,在main中有两种调用方法,第一种是普通的方式,进行动态链接,执行虚函数,得到结果”Derived::Hiberarchy”;第二种是指定类的方式,就不再执行虚函数的动态链接过程了,结果是”Base::Hiberarchy”。

通过上面的分析可以看出,
定义纯虚函数的真正目的是为了定义抽象类,而并不是函数本身。与之对比,在java中,定义抽象类的语法是 abstract class,也就是在类的一级作指定(当然虚函数还是也要加上abstract关键字)。是不是这种方式更好一些呢?在Stroustrup的《C++语言的设计与演化》中我找到这样一段话:

“我选择的是将个别的函数描述为纯虚的方式,没有采用将完整的类声明定义为抽象的形式,这是因为纯虚函数的概念更加灵活一些。我很看重能够分阶段定义类的能力;也就是说,我发现预先定义一些纯虚函数,并把另外一些留给进一步的派生类去定义也是很有用的”。

我还没有完全理解后一句话,我想从另外一个角度来阐述这个概念。那就是,在一个多层复杂类结构中,中间层次的类应该具体化一些抽象函数,但很可能并不是所有的。中间类没必要知道是否具体化了所有的虚函数,以及其祖先已经具体化了哪些函数,只要关注自己的职责就可以了。也就是说,中间类没必要知道自己是否是一个真正的抽象类,设计者也就不用考虑是否需要在这个中间类的类级别上加上类似abstract的说明了。

当然,一个语言的设计有多种因素,好坏都是各个方面的。这只是一个解释而已。

最后,总结一下关于虚函数的一些常见问题:

1) 虚函数是动态绑定的,也就是说,使用虚函数的指针和引用能够正确找到实际类的对应函数,而不是执行定义类的函数。这是虚函数的基本功能,就不再解释了。

2)
构造函数不能是虚函数。而且,
在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还没有构造好, 多态是被disable的。

3)
析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。

 

4) 将一个函数定义为纯虚函数,实际上是将这个类定义为抽象类,不能实例化对象。

5)
纯虚函数通常没有定义体,但也完全可以拥有

6)  析构函数可以是纯虚的,但
纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。

7) 非纯的虚函数必须有定义体,不然是一个错误。

8) 派生类的override虚函数定义必须和父类完全一致。
除了一个特例,如果父类中返回值是一个指针或引用,子类override时可以返回这个指针(或引用)的派生。例如,在上面的例子中,在Base中定义了 virtual Base* clone(); 在Derived中可以定义为 virtual Derived* clone()。可以看到,这种放松对于Clone模式是非常有用的。

假“虚拟构造函数”的用处

从字面来看,谈论“虚拟构造函数”没有意义。当有一个指针或引用,但是不知道其指向对象的真实类型是什么时,可以调用虚拟函数来完成特定类型(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)。list类型对象的行为特性有些象双向链表,尽管它没有以这种方法来实现。对象NewLetter不运行时就会存储在磁盘上。为了能够通过位于磁盘的替代物来建立Newsletter对象,让NewLetter的构造函数带有istream参数是一种很方便的方法。当构造函数需要一些核心的数据结构时,它就从流中读取信息:
class NewsLetter {
public:
NewsLetter(istream& str);

};
此构造函数的伪代码是这样的:
NewsLetter::NewsLetter(istream& str)
{
while (str) { [Page]
从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。因为它能建立新对象,它的行为与构造函数相似,而且因为它能建立不同类型的对象,我们称它为虚拟构造函数。虚拟构造函数是指能够根据输入给它的数据的不同而建立不同类型的对象。虚拟构造函数在很多场合下都有用处,从磁盘(或者通过网络连接,或者从磁带机上)读取对象信息只是其中的一个应用。

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

};

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

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

在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) {
// \”it\” 指向rhs.components的当前元素,调用元素的clone函数,
// 得到该元素的一个拷贝,并把该拷贝放到
// 这个对象的component链表的尾端。
components.push_back((*it)->clone());
}
}
遍历被拷贝的NewsLetter对象中的整个component链表,调用链表内每个元素对象的虚拟构造函数。我们在这里需要一个虚拟构造函数,因为链表中包含指向NLComponent对象的指针,但是我们知道其实每一个指针不是指向TextBlock对象就是指向Graphic对象。无论它指向谁,我们都想进行正确的拷贝操作,虚拟构造函数能够为我们做到这点。

虚拟化非成员函数
就象构造函数不能真的成为虚拟函数一样,非成员函数也不能成为真正的虚拟函数。然而,既然一个函数能够构造出不同类型的新对象是可以理解的,那么同样也存在这样的非成员函数,可以根据参数的不同动态类型而其行为特性也不同。例如,假设你想为TextBlock和Graphic对象实现一个输出操作符。显而易见的方法是虚拟化这个输出操作符。但是输出操作符是operator<<,函数把ostream&做为它的左参数(left-hand argument)(即把它放在函数参数列表的左边),这就不可能使该函数成为TextBlock 或 Graphic成员函数。 [Page]
(这样做也可以,不过看一看会发生什么:
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函数,让前者调用后者! [Page]
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);
}

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

现在你知道如何根据它们的一个参数让非成员函数虚拟化,你可能想知道是否可能让它们根据一个以上的参数虚拟化呢?可以,但是不是很容易。

    原文作者:Hsuxu
    原文地址: https://blog.csdn.net/Hsuxu/article/details/7445597
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞