当前位置:首页 >> 编程开发 >> Visual C++ >> 内容

避免对派生的非虚函数进行重定义

时间:2015/5/19 21:32:42 作者:平凡之路 来源:xuhantao.com 浏览:

今天无意中发现一个关于C++基础的问题,当时愣是没理解是什么原因,现在搞明白了,就写下来了 。先看小程序,先实践再理论吧,要不大家就睡着了。

#include <iostream>
using namespace std;
class Base    
{    
public:    
      virtual void funtion(int arg = 1){cout<<arg<<endl;} 
};
class Derive : public Base
{
public:    
      virtual void funtion(){cout<<"Derive"<<endl;}
      virtual void funtion(int arg){cout<<"Derive"<<arg<<endl;}
};    
int main(int argc, char *argv[])
{    
    Base* obj = new Derive();
    obj->funtion();    
    system("pause");    
    return 0;        
}

上面的程序会出现什么结果呢?我想会有很多人看到这个地方就会怀疑我程序的正确性了, 大呼“你的程序是错的”,但真的错吗?我们可以先执行下程序看下结果。很明显,结果是调用了子类 的函数,并且子类中arg参数的值是父类中的值1,运行结果为“Derive 1”。

下面我就解释下 这种结果的原因,首先我先要说下对象的两种类型:动态类型和静态类型。

静态类型:指针或 者是引用声明时的类型。

动态类型:由他实际指向的类型确定。

例如:

Base 

*pgo=   //pgo静态类型是Base *    
new Derive; //动态类型是Derive *    
Asterioid *pa = new Asterioid; //pa的静态类型是 Asterioid *    
//动态类型也是 Asterioid *    
pgo = pa; //pgo静态类型总指向Base *    
//动态类型指向了 Asterioid *    
Base &rgo = *pa; //rgo的静态类型是Base    
//动态类型是 Asterioid

虚函数是动态绑定的,而默认参数值是静态绑定的。运行时效率。 如果默认参数值是动态绑定的话,那么编译器必须提供一整套方案,为运行时的虚函数参数确定恰当的 默认值。而这样做,比起C++当前使用的编译时决定机制而言,将会更复杂、更慢。鱼和熊掌不可兼得 ,C++将设计的中心倾向了速度和简洁,你在享受效率的快感的同时,如果你忽略本条目的建议,你就 会陷入困惑。

其实对于这个问题在[Effective C++第3版]中也有提到,其第36条:避免对派生 的非虚函数进行重定义。下面看下书中的描述:

现在考虑以下的层次结构:B是一个基类,D是 由B的公有继承类,B类中定义了一个公有成员函数mf,由于这里mf的参数和返回值不是讨论的重点,因 此假设mf是无参数无返回值的函数。也就是说:

class B {    
public:    
  void mf();    
};    
class D: public B {  };    
即使不知道B、D、mf的任何信息,让我们声明一个D的对象x:    
D x;                           // x 是D类型的对象    
B *pB = &x;                       // 指向x的指针    
pB->mf();                         // 通过指针调用mf函数    
D *pD = &x;                       // 指向x的指针    
pD->mf();                         // 通过指针调用mf函数

在这里,如果告诉你pD- >mf()与pB->mf()可能拥有不同的行为,你一定会感到意外。这也难怪:因为两次都是在调用x对 象的成员函数mf,因为两种情况下都是用了同一函数和同一对象,mf()理所应当应该有一致的行为。难 道不是吗?

你说得没错,的确“理所应当”。但这一点无法得到保证。在特殊情况下,如果mf 是非虚函数并且D类中对mf进行了重定义,那么问题就出现了:

class D: public B {    
public:    
  void mf();                   // 隐藏了B::mf; 参见第33条    
};    
pB->mf();                         // 调用B::mf    
pD->mf();                         // 调用D::mf

此类“双面行为”的出现,究其原因 ,是由于诸如B::mf和D::mf这样的非虚函数是静态绑定的(参见第37条)。这也就意味着:由于我们将 pB声明为指向B的指针,那么通过pB所调用的所有非虚函数都将调用B类中的版本,即使pB指向一个B的 派生类的对象也是如此,正如上文示例所示。

然而,对于虚函数而言,它们在编译期间采用动 态绑定(再次参见第37条),因此它们不会被这个问题困扰。如果mf是虚函数,那么无论通过pB还是pD 来调用mf都会是对D::mf的调用,这是因为pB和pD实际上指向同一对象,这个对象是D类型的。

如果你正在编写D类,并且你对由B类继承而来的mf函数进行了重定义,那么D类将会表现出不稳定的行 为。在特定情况下,任意给定的D对象在调用mf函数时可能表现出B或D两种不同的行为,而且决定哪种 行为的因素是指向mf的指针的类型,与对象本身没有任何关系。引用同指针一样会出现这种莫名其妙的 行为。

但是,本文的内容仅仅是从实际角度出发做出的分析,我知道,你真正需要的是对“避 免对派生的非虚函数进行重定义”这一命题的理论推导。我很乐意效劳。

第32条解释了公有继 承意味着A是一个B,第34条描述了为什么在类中声明一个非虚函数是对类本身设置的“个性化壁垒”。 将上述理论应用到类B、D和非虚你函数B::mf上,我们可以得到:

·对B生效的所有东西对D也生 效,这是因为所有的D对象都是B对象。

·继承自B的类必须同时继承mf的接口和实现,这是因 为mf是B类中的非虚函数。

现在,如果在D类中对mf进行了重定义,那么你的设计方案中就出现 了一个矛盾。如果D确实需要与B不同的mf实现方案,并且对于所有的B对象,无论这些对象多么个性化 ,它们都必须使用B实现版本的mf,于是我们可以很简单地的出以下的结论:并不是每个D都是一个B。 这种情况下,D并非公有继承自B。然而,如果我们确实需要D是B的公有继承类的话,并且D确实需要与B 不同的mf实现版本,那么mf对B的“个性化壁垒”作用就不复存在了。这种情况下,mf应该是虚函数。 最后,如果每个D确实是一个B,并且mf确实对B起到了“个性化壁垒”的作用,那么D中并不会真正的重 定义mf,它也不应该做出这样的尝试。

无论从哪个角度讲,我们都必须无条件地禁止对派生的 非虚函数进行重定义。

如果阅读本文给你一种似曾相识的感觉,那么你一定是对阅读过的第7条 还有印象,在那里,我们解释了为什么多态基类的析构函数必须为虚函数。如果你违背了第7条的思想 (比如,你在多态基类中声明了一个非虚析构函数),那么你也就同时违背了本条的思想。这是因为在 派生类中继承到的非虚函数一定会被重定义。即使派生类中不声明任何析构函数也是如此,这是因为, 对于一些特定的函数,即使你不自己生成它们,编译器也会自动为你生成它们(参见第5条)。从本质 上讲,第7条只不过是本条的一个特殊情况,只是因为它十分重要,我们才把它单列出一条来。

铭记在心

·避免在派生类中重定义非虚函数。

相关文章
  • 没有相关文章
共有评论 0相关评论
发表我的评论
  • 大名:
  • 内容:
  • 徐汉涛(www.xuhantao.com) © 2024 版权所有 All Rights Reserved.
  • 部分内容来自网络,如有侵权请联系站长尽快处理 站长QQ:965898558(广告及站内业务受理) 网站备案号:蒙ICP备15000590号-1