当你审查公司的旧代码时,可能偶然遇到不知哪个程序员编写的程序片段。写的人似乎对C++的特质有相当认识。那么这个程序员期望打印什么结果呢?实际结果如何呢?
#include <iostream>
#include <complex>
using namespace std;
class Base
{
public:
virtual void f(int);
virtual void f(double);
virtual void g(int i = 10);
};
void Base::f(int)
{
cout << "Base::f(int)" <<endl;
}
void Base::f(double)
{
cout << "Base::f(double)" <<endl;
}
void Base::g(int i)
{
cout << i <<endl;
}
class Derived : public Base
{
public:
void f(complex<double>);
void g(int i = 20);
};
void Derived::f(complex<double>)
{
cout << "Derived::f(complex<double>)" <<endl;
}
void Derived::g(int i)
{
cout << "Derived::g()" <<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()” 不是标准写法,因此没有太多移植性。
虽然void main()不是标准写法,但许多编译器接受它,不过即使你的编译器接受了void main(),并不达标你可以将它移植到另一个编译器上,因此你最好习惯使用以下两个标准写法之一:
int main();
int main(int argc,char* argv[]);
main函数没有写return 0语句,虽然标准的main()应该传回一个int,但是没写编译器默认返回的是return0。
2.“delete pb;”是不安全的。
这看起来无害,事实上也无害,前提是Base得提供虚析构函数。本例删除的是一个point-to-base,而该base class没有虚析构函数,这是一种有害行为。你所预期到只能是程序崩溃,因为错误的析构函数将被调用,而且还会错误使用对象大小来调用operator delete()。
设计准则:让base class的析构函数成为virtual(除非你确定不会有人通过基类指针来删除子类对象)。
下面三个很重要,用来区分三个常见的术语:
- 重载:函数名相同,作用域相同,参数类型不同的函数。
- 重写:在派生类中提供一个相同名字,相同参数函数,并且基类的函数有virtual。
- 隐藏:在派生类中提供一个相同名字,参数类型不同。
3.Derived::f 不是重载函数。
Derived::f并不会重载Base::f,而是隐藏了Base::f。这个区别非常重要,因为意味着Base::f(int)和Base::f(double)在Derived作用域内不可见。
如果Derived的编写者打算隐藏Base类的名为f的函数,这是没问题的。然而,通常情况下,这种屏蔽是无意为之的。将这些名字放进Derived作用域中正确的方法是"using Base::f"。
设计准则:当你提供一个函数,其名称和继承而来的函数同名,但参数不同,如果你不想因此遮掩了继承而来的函数,那么应该使用using声明来确保继承函数在作用域中。
4.Derived::g重写了Base::g,但是改变了默认参数。
void g(int i = 20);
这是对用户而言是不友好的写法,不要改变默认参数,除非你确实像迷惑别人。
设计准则:绝不要重写继承函数的默认参数。
现在我们已经谈论完了奇怪的风格问题,让我们回头看看代码,并看看它们的行为是否符合你的预期:
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>)。因为没有重写。
pb->f(1.0);
这里调用的是Base::f(double)。虽然Base*指向的是一个Derived对象,因为重载根据静态类型决定,而不是动态类型决定的。
同样的理由,如果你调用的pb->f(complex<double>),将无法通过编译,因为在Base接口中没有符合的函数。
b.g();
打印出的是10,简单调用的基类的g(int i = 10)。
d.g();
打印的是20,简单调用了派生类的g(int i = 20)。
pb->g();
打印的10,因为默认参数是静态类型的,不是动态类型的。实际调用的函数根据动态类型来决定的。
delete pb;
造成内存泄露,因为派生类的部分对象没删除,导致切割问题。