引言:
多态对于C++这种面向对象的语言来讲,其重要性是不言而喻的,用了足足半天的时间来把我所理解的多态表达出来,其中还有很多细节需要以后补充。(一个字一个字写,还要画图,太累了)
面试中遇到最多的问题就是多态的概念,以及多态的实现依据,首先多态发生的条件:
(1)必须要有子类继承父类
(2)父类中存在虚函数,并且子类重写该虚函数
(3)父类的指针或者引用指向子类对象
这三个条件缺一不可。那么多态究竟是怎么实现的呢?那就不得不提到虚函数表,在每一个实例化的子类对象的头部,首先是一个指向虚函数表的指针,那么虚函数表中依次存放着父类的方法和子类特有的方法,如果子类重写父类的方法,那么就会替换父类的方法,否则的话就照抄。下面给出一个示意图。
这里,Base是一个基类,里面有三个虚函数f,g,h。子类son继承Base,实例化一个子类对象b,因为我们知道,虚函数指针(也就是指向虚函数表的指针),是存放在类对象的头部的,所以对b取地址,其实得到的就是虚函数指针的地址,虚函数指针又指向虚函数表,那么你对&b解引用,自然也就得到了虚函数表的首地址(也就是第一个虚函数的地址),虚函数表里依次存放了虚函数的地址,其中,存放的顺序:父类中的函数(如果有重写,就直接覆盖)、子类中特有的函数。
这里面需要注意的是,虚函数表并不是共享的,父类和子类都会有自己的虚函数表,可以通过分别取各自的虚函数表地址来验证。
我们可以进一步来验证一下每个类对象的头部存放的都是指向虚函数表地址的指针这一事实。
首先给出父类和子类
class Base
{
public:virtual void f() { cout << "调用base::f" << endl; }virtual void g() { cout << "调用base::g" << endl; }virtual void h() { cout << "调用base::h" << endl; }};
class son:public Base
{
public:void f() { cout << "调用son::f" << endl; }void f1() { cout << "调用son::f1" << endl; }
};
实例化一个子类对象,并且获得其虚函数表的地址。
typedef void(*Fun)(void);Base b;Fun pFun = NULL;cout << "虚函数指针的地址:" << (int*)*(int*)(&b) << endl;
进一步,获得虚函数表的地址
cout << "虚函数表 —(虚函数第一个函数地址:)" << (int*)*(int*)*(int*)(&b) << endl;pFun = (Fun)*((int*)*(int*)(&b));pFun();
如果是一个类继承了多个基类,那么它的虚函数表应是如下情况
下面有几个问题 需要解决一下:
Q1: 为什么父类指针可以指向子类对象?
可以通俗的理解,子类可能含有一些父类没有的成员变量或者方法函数,但是子类肯定继承了父类所有的成员变量和方法函数。所以用父类指针指向子类时,没有问题,因为父类有的,子类都有,不会出现非法访问问题。但是如果用子类指针指向父类的话,一旦访问子类特有的方法函数或者成员变量,就会出现非法。虽然父类指针可以指向子类,但是其访问范围还是仅仅局限于父类本身有的数据,那些子类的数据,父类指针是无法访问的。
Q2: 为什么非要用父类指针指向子类对象,才能调用子类对应的虚函数?
首先,因为子类包含有父类,把子指针赋给父指针时其实只是把子类中父类部分的地址赋给了父类指针而已,(而父类里没有包含子类,自然不能进行复制。)这个时候再用这个指针去调用父类和子类共有的函数,其实调用的是子类重写之后的虚函数) //这个理解真的很棒,参考别人的。
如果一个类继承了多个类,而且同时重写了各个类的虚函数,那么此时如果你只是单纯的想要用子类去调用虚函数,那么到底是调用的哪个父类的虚函数呢?这个时候用类的指针去指向子类对象就显得尤为重要了,此时才会调用到一个确定的父类的虚函数。
但是父类指针指向子类对象时不能调用子类特有的方法。
son s;Base* b = &s;b->f();b->f1(); //f1为子类特有方法,此时报错