Guru of the Week 条款05:覆写虚拟函数
GotW #05 Overriding Virtual Functions
著者:Herb Sutter
翻译:kingofark
[声明]:本文内容取自www.gotw.ca网站上的Guru of the Week栏目,其著作权归原著者本人所有。译者kingofark在未经原著者本人同意的情况下翻译本文。本翻译内容仅供自学和参考用,请所有阅读过本文的人不要擅自转载、传播本翻译内容;下载本翻译内容的人请在阅读浏览后,立即删除其备份。译者kingofark对违反上述两条原则的人不负任何责任。特此声明。
Revision 1.0
Guru of the Week 条款05:覆写虚拟函数
难度:6 / 10
(虚拟函数(virtual function)真是一个招人喜欢的基本特性,对吗?好吧,如果你能回答下面这个问题,你就会发现她们有时真是冷若冰霜、寒气刺骨。)
[问题]
当你在一个布满灰尘的角落翻寻公司的存档代码时,你偶然中发现了无名氏编写的一段如下的程序。这位无名氏程序员好像曾经试图用这个程序做试验,看看某些C++特性的运作状况。你知道这个程序员希望程序运行后打印什么结果吗?你知道程序运行后的实际结果吗?
#include <iostream>
#include <complex>
using namespace std;
class Base {
public:
virtual void f( int ) {
cout << "Base::f(int)" << endl;
}
virtual void f( double ) {
cout << "Base::f(double)" << endl;
}
virtual void g( int i = 10 ) {
cout << i << endl;
}
};
class Derived: public Base {
public:
void f( complex<double> ) {
cout << "Derived::f(complex)" << endl;
}
void g( int i = 20 ) {
cout << "Derived::g() " << i << endl;
}
};
void main() {
Base b;
Derived d;
Base* pb = new Derived;
b.f(1.0);
d.f(1.0);
pb->f(1.0);
b.g();
d.g();
pb->g();
delete pb;
}
[解答]
首先,我们谈谈编码风格方面的问题:
1. void main()
这并不是一个合法的main声明,尽管许多编译器都允许这种写法。应该使用“int main()”或者“int main(int argc, char* argv[])”。
然而,其实你仍然不需要添加任何返回语句(虽然有时加上返回语句是为了形成一种向外部调用者报告错误的良好编码风格)。实际上,如果main没有返回语句,其效果等同于执行了“return 0;”。
2. delete pb;
这看起来好像既无公害又无伤大雅——当然,前提是Base的编写者提供了一个虚拟析构函数(virtual destructor)。然而,就像我们在这个程序中所看到的那样,在没有虚拟析构函数(virtual destructor)的情况下通过指向基类的指针进行删除操作,这简直就是邪恶的犯罪,同时也是幼稚和简单的——如此以来,崩溃就是你所能期待的最好的事情了。
[规则]:把基类的析构函数(virtual destructor)声明为virtual。
3. Derived::f(complex<double>)
Derived并没有重载(overload)Base::f,而是隐藏了它。这个细节非常重要,因为这意味着在Derived中,Base::f(int)和Base::f(double)是不可见的!(更何况某些流行的编译器对此种情况甚至都不给出一个警告信息。)
[规则]:当派生类中的函数与基类中的函数同名,而你又不想在派生类中隐藏这些基类函数的时候,请使用using声明来把它们定置到可用范围(scope)之内。
4. Derived::g(int i = 10)
除非你是故意要把别人搞糊涂,否则不要改变你所覆写(override)的继承函数(inherited function)的缺省参数。(一般来说,进行覆写而不使用参数缺省值并不是一个坏主意,但那是其本身的问题。)是的,这是一个合法合理的C++语句;不错,其结果也被很好的定义了;但是,不,请不要这样做。往下接着看,你会发现它到底是如何把人搞糊涂的。
[规则]:绝不要改变覆写的继承函数(overridden inherited function)
好了,让我们不要再谈那些编码风格的琐事了,现在我们就来看看主程序到底是不是按照那位无名氏程序员所期望的方式运作的。
void main() {
Base b;
Derived d;
Base* pb = new Derived;
* 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)都不能被调用。
无名氏先生也许原本是想要调用Base::f(double),但是此处他连一个编译错误信息也得不到,因为幸运的是(?),complex<double>包含有一个从double的隐式转换(*),因而编译器把它看成是Derived::f(complex<double>(1.0))。
(*)注:在现有的C++标准草案中,转换构造函数(conversion constructor)不是显式的(explicit)。
* pb->f(1.0);
有趣的事情在这里发生了:虽然Base* pb指向一个Derived对象,但这个语句还是调用Base::f(double),因为重载解析过程(overload resolution)会以静态类型(在这里是指Base)完成,而不是以动态类型(在这里是指Derived)完成。
* b.g();
这条语句只是简单的打印“10”,因为它引起对Base::g(int)的调用,其函数参数缺省值为10。这没什么好奇怪的。
* d.g();
这条语句打印“Derived::g()20”,因为它引起对Derived::g(int)的调用,其函数参数缺省值为20。这也没什么好奇怪的。
* pb->g();
这条语句打印“Derived::g()10”。这个结果也许会暂时死锁你的精神刹车,导致你声嘶力竭的进入一种精神停滞状态——直到你认识到,编译器的所作所为是再正常不过的了(虽然我们可怜的无名氏毫无疑问的应该被枪毙)。要记住,确定缺省参数和重载一样,是以对象的静态类型(在这里是指Base)完成的,因此选用了缺省值10。然而,函数又恰好是virtual的,所以实际被调用的函数取决于对象的动态类型(在这里是指Derived)。
最后,如果你能理解这剩下的几个语句(尽管你会因此说:“噢呕!”),那么你就终于可以理解我开头所说的“冷若冰霜、寒气刺骨”的意思了。祝贺你!
* delete pb;
}
删除操作,当然,会留下一些只被部分清除的东西,使内存变得不可捉摸……好好看看前面关于虚拟析构函数(virtual destructor)的叙述吧。