原文请链接http://www.gotw.ca/gotw/005.htm
虚函数是非常基础的特性,如果你能回答上以下问题,你就能完全了解他们。
问题:
假设你正在浏览公司代码库的边角地带,你碰到了一段如下的代码。编写这段代码的人看上去像在实验C++的这些特性是如何工作的。程序员想要打印的结果是什么?实际结果是什么呢?
1#include <iostream> 2 #include <complex> 3 using namespace std; 4 5 class Base { 6 public: 7 virtual void f( int ) { 8 cout << "Base::f(int)" << endl; 9 } 10 11 virtual void f( double ) { 12 cout << "Base::f(double)" << endl; 13 } 14 15 virtual void g( int i = 10 ) { 16 cout << i << endl; 17 } 18 }; 19 20 class Derived: public Base { 21 public: 22 void f( complex<double> ) { 23 cout << "Derived::f(complex)" << endl; 24 } 25 26 void g( int i = 20 ) { 27 cout << "Derived::g() " << i << endl; 28 } 29 }; 30 31 void main() { 32 Base b; 33 Derived d; 34 Base* pb = new Derived; 35 36 b.f(1.0); 37 d.f(1.0); 38 pb->f(1.0); 39 40 b.g(); 41 d.g(); 42 pb->g(); 43 44 delete pb; 45 }
答案:
首先是一些风格问题:
- void main() ;
这不是main函数的合法用法,尽管一些编译器允许这样使用。要使用"int main()"或者"int main(int, char*[])"。然而,要注意到,return语句在main中并不是必须的。如果你不写,编译器会为你自动加上"return 0;"。
- delete pb;
这看上去没什么错误,前提是基类的编写者使用了虚拟析构函数。事实上,通过基类指针删除一个没有虚析构函数的实例就像魔鬼一样,你最好期望它能立即奔溃。
【规则】使基类的析构函数为虚
- Derived::f(complex<double>)
Derived 冰没有重载Base::f,而是隐藏了它。这非常重要,因为在Derived中,Base::f(int)和Base::f(double)在Derived中不可见。(注意,一些知名的编译器对此甚至连警告都没有)
【规则】当提供了一个与继承而来的函数同名的函数时,确保使用"using"指示符将继承而来的函数引入类作用域中,如果你不想隐藏他们。
- Derived::g(int i = 10)
除非你想要愚弄大家一下,否则不要试图改变继承而来的默认参数值。(总的来讲,尽量使用继承而来的默认参数值并不是个坏主意,不过这是它本身的问题)在C++中这是合法的,而且是良好定义的,但是,请不要这样使用。通过下面的代码来展示它是如何迷惑人们的。
【规则】永远不要修改继承而来的默认参数值。
现在我们清楚掉了主要的代码风格问题,让我们切回主题来看一下结果是否是编写者想要的。
1 void main() { 2 Base b; 3 Derived d; 4 Base* pb = new Derived; 5 6 b.f(1.0);
没问题,调用了Base::f(double)
d.f(1.0);
这调用了Derived::f(complex<double>).为什么呢?回想一下,Derived并没有声明"using Base::f()"。显然,Base::f(int)和Base::f(double)并不会被调用。
这调用了Derived::f(complex<double>),而编译器连警告都不会发出。这是因为"幸运的"在complex<double>有从"double"的隐式转换,所以编译器把它视作"Derived::f(complex<double>(1.0))"。
隐式转换是因为在当前的草案中,complex<double>的构造器并不是explicit。
pb->f(1.0);
这儿很有趣,它调用的是Base::f(double),尽管pb指向的是Derived类型。这是因为重载决议是在静态类型上完成的,而不是在动态类型上。
b.g();
这打印出了"10",这是因为它仅仅是调用了Base::g(int i = 10).没问题。
d.g();
这打印出了"20",这是因为它仅仅调用了Derived::g(int i = 20).依然没问题。
pb->g();
这打印出了"Derived::g() 10"。这会使你非常吃惊直到你意识到编译器这么做是多么的正确。这里要记住的是,像重载决议一样,默认参数来自于静态类型,而不是动态类型,因此是10.然而,这里恰巧是虚函数,因此调用的又是动态类型的方法。
【译者观点】关于虚函数中的默认参数,在effective c++中,作者提倡的原则是不要在虚函数中使用默认参数,比此文中的规则更加严格,至于使用那种规则,就要见仁见智了。在译者本人的项目组中,看到的已有代码会使用虚函数的默认参数,毕竟省时省力。现代的代码补全工具会在派生类中根据基类声明自动添加同样的默认参数值。很多时候为了兼容客户代码,即使要重构,也必须继续使用默认参数。因此使用或者不使用并不是简简单单的一条规则,it depends。
如果你理解了最后几个段落(哎呀,原来是这样啊!),那你应该完全理解了这个主题,恭喜。
1 delete pb; 2 }
对于delete语句,无论如何它会崩溃,析构掉部分内存。详见上文关于虚析构函数部分。