虚函数的困惑
作者:gang2k
关键字: C++, 单继承,虚函数
这篇文章是要解释C++虚函数的一些令人困惑的问题,这次我只说单继承的事,不涉及多继承和虚拟继承的问题。
先来看一段程序:
#include<iostream>
using namespace std;
class base
{
public:
virtual void a()
{
cout<<"base class function a"<<endl;
}
void b()
{
cout<<"base class function b"<<endl;
}
};
class derived : public base
{
public:
void a()
{
cout<<"derived class function a"<<endl;
}
virtual void b()
{
cout<<"derived class function b"<<endl;
}
};
class derived1 : public derived
{
public:
virtual void a()
{
cout<<"derived1 class function a"<<endl;
}
void b()
{
cout<<"derived1 class function b"<<endl;
}
};
int main()
{
base *x = new derived;
base *y = new derived1;
derived *z= dynamic_cast<derived*>(y);
x->a();
x->b();
y->a()
y->b();
z->a();
z->b();
}
也许很容易就能猜出这个程序的结果,不过在你把它编译执行以后你会发现输出可能和你的想象不太一样。
下面是用g++在linux上编译执行的结果:
L1:derived class function a
L2:base class function b
L3:derived1 class function a
L4:base class function b
L5:derived1 class function a
L6:derived1 class function b
让人不能理解的可能主要是L4和L6。
从程序中可以看出,指针y和指针z指向的是同一个对象,它们调用的似乎也是同一个函数b(), 为什么结果会不一样? 函数b()到底是不是虚函数?
解释这个问题其实也不难,但是要明白几个原则。
1. 对于同名函数,如果在父类中使用了virtual关键子,继承它的子类,子类的子类…,中所有的同名函数都相当于使用了virtual关键字,不管有没有virtual关键字存在。上句中所说的同名函数,指的是声明完全相同的函数,就是说返回类型,函数名,参数都相同,甚至连有没有const都要相同的函数,如果有一点不同,他们就不是同名函数。前面之所以使用父类和子类,而不使用基类和派生类这样的词,是因为有人常常用基类来指继承树顶端的那个类,下面的类都是派生类;我所指的不是这种关系,所以我用父类和子类这样的词,指名两个类(类声明中,冒号前后的两个类)的关系。
2. 对于虚函数实际上是保存在一个叫virtual table的数据结构中这个事实, 很多C++书籍都有描述。那么一个类中的virtual table是什么样子的? 可以用以下3个简单的步骤来推测:
a. 首先把父类的virtual table完全继承下来。
b. 如果类中有成员函数和virtual table中的函数同名(指完全同名),那么覆盖virtual table中的函数
c. 如果类中有virtual table中没有的新的virtual 函数,这把这个函数加入表中。
3. 对于一个指针,它可能有两个类型,静态类型和动态类型
举例说明:
base *y = new derived1;
指针y的静态类型是等号左边的类型base,而它的动态类型则是等号右边的derived1。
当通过指针调用一个成员函数时,首先要通过指针的静态类型来决定该函数是否是虚函数。如果不是虚函数则调用它的普通成员函数,否则通过动态类型的virtual table 调用虚函数。
知道了这3条,就可以解释上面这段程序的输出结果了。
首先来看一下3个类的virtual table是什么样的。
base类没有父类,所以它的virtual table本来是空的,它自己内部有一个带virtual关键字的函数a(); 把这个新virtual函数加入表中, 所以base类的virtual table是这样的。
base class’ virtual table:
base::a() //只有一个函数 base::a()
derived类首先继承base类的virtual table,而它自己有一个和virtual table中同名的函数 a(); 所以它会覆盖这个函数,同时它还有一个新的virtual 函数b(),于是b()也被添加到virtual table中,于是derived class的virtual talbe是这样的:
derived class’ virtual table:
derived::a(); //覆盖了原来的base::a()
derived::b(); //新的虚函数derived::b()
再来看看derived1类,它的情况其实比较简单,a()和b()和继承下来的virtual table中的两个函数同名,所以覆盖掉原来的函数,于是derived1的virtual table是这样的:
derived1 class’ virtual table:
derived1::a(); //覆盖掉继承下来的 derived::a()
derived1::b(); //覆盖掉继承下来的 derived::b()
现在我们可以来解释L4和L6的输出结果了。
先看L4。 这一行实际上是由main函数中的 y->b();表达式产生的,y是一个指针,我们来看一下它的定义:
base *y = new derived1;
从这行代码可以看出,y的静态类型是base,而动态类型是derived1,当我们通过指针y调用函数b()的时候,编译器首先看出y的静态类型是base,而在base类的定义中,函数b()并不是一个虚函数,所以编译器当它是普通的成员函数,直接调用base::b(), 这就是为什么y指针实际指向的是一个derived1类,但是没有调用derived1的b()函数的原因。
结果就是L4显示的那样。
再看看L6,L6的结果是由z->b();这句产生的,z的定义是:
derived *z= dynamic_cast<derived*>(y);
可以看出:指针z和指针y指向的是同一个对象,所以z的动态类型和y一样都是derived1, 而静态类型却变成了derived。
好了,我们来看看z->b()这句话做了什么。首先编译器看出z的静态类型是derived, 而在derived类中,b()是个虚函数,那么就要执行其动态类型的virtual table中的b()函数。指针z的动态类型是derived1, 在derived1的virtual table中b函数实际上是derived1::b(), 这就是为什么我们看到L6的结果。
我们也可以根据上面说的3条原则,试着看看L2,L3,L5的输出是不是这么回事。
PS:
为了学习C++, 我写了20多个向上面那样小程序段,去证明这种语言的一些细节。 越来越觉得C++是一门难学难用的语言,那么多隐藏在背后的细节,那么多意想不到的结果。无法浅显易懂,但细细研究下去又觉得有道理。 这倒是给面试官们很多发挥的机会。也许实际上很多编程老手都避免在自己的程序中用到这些诡异的细节。但是如果真的不小心写出一两个,要是不理解原理,还真的不好找原因。所以我们也不能抱怨面试官和笔试题目太变态。在说东西太容易的话那不都学会了。
我在一次面试中,被问道virtual table的问题,我跟对方讲了大致推测出virtual table的3个步骤,也就是上面说的原则2。但是我没有说原则1和3,因为我以为说出2可能就已经能表达出我对这个问题是理解的了。 但是对方只是疑惑的“嗯”了一声。 面试后想想,如果对方本来对这个问题就不慎理解的话,我光说出2,其实是不够的,因为有很多人都以为一个函数是不是virtual的,要看继承树的最顶端,如果在最顶端的那个基类里,一个函数有virtual关键字,那么下面的派生类中的同名函数,无论有没有写virtual这个关键字,其实都是virtual的,反之亦然。 这当然是不对的,我在网上发现很多人是这样的认识,而且都自称做过试验,其实他们确实做过试验,只不过他们的继承树只有两层,如果在加一层继承,就能看到不一样的结果。