深入理解C++多态
在之前发的一篇文章《虚函数表分析-C++多态的实现》中,已经分析过C++多态的实现原理。这篇文章来看一个具体的例子,这个例子来源于一道经典的C++面试题,看起来蛮简单,但是相当多的人都在这里翻车了。这篇文章希望能把这道题阐述清楚,给大家带来一些帮助。题目给了下面这样一段代码(代码我略有更改),问程序的输出是什么,并解释输出结果。
class A {
public:
int a;
virtual void x() {
cout << "A::x()" << endl;
}
void y() {
x();
cout << "A::y()" << endl;
}
};
class B:public A {
public:
int b;
virtual void x() {
cout << "B::x()" << endl;
}
virtual void y() {
x();
cout << "B::y()" << endl;
}
};
int main()
{
A* p = new B;
p->y();
return 0;
}
大家可以先自己想一想这段代码的执行结果是什么,再看后面的内容。
这段代码的打印结果是:
B::x()
A::y()
如何解释这个执行结果呢?我们先来分析一下A,B两个对象的内存布局是什么样子。A定义了一个int型的成员变量a,一个虚函数x(),一个普通成员函数y()。那么根据我们之前的分析可以知道,A对象的内存中应该包含一个虚函数指针和一个int类型的变量a。虚函数表指针指向一张虚函数表,虚函数表中保存着A::x()的函数地址。由于A::y()不是虚函数,因此y()的地址不会写入虚函数表,调用y()函数是编译器静态联编实现的。如果在32位系统上,那么一个A对象的实例应该占用8个字节(一个指针占用4字节,一个int变量占用4个字节)。其内存布局如图所示:
B对象公有继承了A对象,那么B对象的内存中同样有一个虚函数表指针,一个int型变量a。B中的虚函数表的内容和A的虚函数表内容是一模一样。但是要注意由于B重写了A的x()函数,那么B的虚函数表中,本来存放A::x()函数地址的地方现在被替换为B::x()函数地址。同时因为B::y()函数也是一个虚函数,因此B::y()的函数地址被追加到B的虚函数表的末尾。B继承了A的a变量,同时自己也定义了一个int型的变量b。如果是在一个32位系统上,那么一个B对象的实例将占用12个字节(一个指针,两个int类型变量)。B的内存布局如下图所示:
现在回到代码上来,为什么执行p->y()打印出来的结果是调用了B的x函数,A的y函数呢?在main程序中,我们new了一个B对象,用一个A指针来指向它。那么A指针在B的内存当中可访问的范围如下图:
因此通过p指针能访问的函数只有B::x()和A::y(),通过p能访问的成员变量只有a。调用p->y()函数的时候,由于p是一个A*指针,此时调用的是A的普通成员函数A::y(),这个函数调用是在编译器编译的时候已经确定的。我们知道当一个成员函数被调用时,编译程序会向它传递一个隐含的参数,这个参数是一个指向这个成员函数所在的对象的指针,这个指针的值会被赋值给this指针。
在此例当中就是说执行p->y()时,会向y函数传递p指针赋值给this指针。在A::y()中调用x()函数,相当于调用this->x(),调用this->x()也就是调用p->x()。到这里就很清楚了,调用p->x()的时候,编译器从p的虚函数表中寻找x()函数的地址执行,显然找到的是B::x()的地址。因此,这里执行B的x()函数,打印出B::x(),随后接着执行A的y()函数,打印出A::y()。