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

 
  
  1. #include <iostream> 
  2. using namespace std; 
  3. class Base 
  4. public
  5.       virtual void funtion(int arg = 1){cout<<arg<<endl;}       
  6. }; 
  7. class Derive : public Base 
  8. public
  9.       virtual void funtion(){cout<<"Derive"<<endl;} 
  10.       virtual void funtion(int arg){cout<<"Derive"<<arg<<endl;}       
  11. }; 
  12. int main(int argc, char *argv[]) 
  13.     Base* obj = new Derive(); 
  14.     obj->funtion(); 
  15.     system("pause"); 
  16.     return 0;     

 

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

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

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

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

例如:

 
  
  1. Base *pgo=   //pgo静态类型是Base * 
  2. new Derive; //动态类型是Derive * 
  3. Asterioid *pa = new Asterioid; //pa的静态类型是 Asterioid * 
  4. //动态类型也是 Asterioid * 
  5. pgo = pa; //pgo静态类型总指向Base * 
  6. //动态类型指向了 Asterioid * 
  7. Base &rgo = *pa; //rgo的静态类型是Base 
  8. //动态类型是 Asterioid 

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

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

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

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

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

 

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

 
  
  1. class D: public B { 
  2. public
  3.   void mf();                   // 隐藏了B::mf; 参见第33条 
  4. }; 
  5. pB->mf();                         // 调用B::mf 
  6. 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函数时可能表现出BD两种不同的行为,而且决定哪种行为的因素是指向mf的指针的类型,与对象本身没有任何关系。引用同指针一样会出现这种莫名其妙的行为。

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

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

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

 

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

 

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

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

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

铭记在心

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