父类指针指向子类对象,通过父类指针访问子类的成员。在运行时,可以识别出真正的对象类型,调用对应子类中的函数,这种特性称之为”多态”。接下来从本质来看多态的本质。

为什么编译允许父类指针指向子类成员?因为它是安全的。

Person类型指针指向Student对象。由于是Person *类型的指针,编译器会认为指向的内存空间为4个字节,不会访问越界。

1
person->m_age = 10;

这句代码从汇编角度就是找到person指针变量存储的对象地址区前4个字节空间赋值10。如果反过来,Student*指向Person对象,就不一样了,就会访问越界

1
2
3
Student *stu = Person person();
stu->m_age = 17;
stu->m_height = 180;

为了防止访问越界,编译阶段报错。

C++多态的实现

在C++中父类指针指向子类对象,使用父类指针调用子类成员函数,调用的是父类的成员函数。

1
2
3
4
5
6
7
8
int main() {

Person *person = new Student();
person->run();

getchar();
return 0;
}

我们可以通过汇编看出原因。

由于C++语言是静态语言,在编译阶段后,根据指针类型编译,直接是调用父类的run方法。

C++多态是通过虚函数(virtual function)来实现,虚函数就是被virtual修饰的成员函数。只要在父类中声明为虚函数,子类中重写的函数也自动变成虚函数(也就是说子类中可以省略virtual关键字)。

虚函数的实现原理

虚函数的实现原理是虚表,虚表始终存储着最终要调用的虚函数地址,这个虚函数表也叫作 虚函数表

看下面的代码:

如果Cat没有virual修饰的成员函数,初始化一个Cat对象占用的存储空间为8个字节。而现在,通过sizeof输出确实12个字节。在X86环境中,如果一个类成员函数有虚函数,那么每个实例对象除成员变量占用的空间除外会额外分配4个字节,前4个字节用来存放指向虚表的地址。

执行下面代码:

会通过cat指针的地址找到Cat对象的内存空间,取出前四个字节的存储的指向虚表的地址,在通过虚表地址找到虚表,许表中存储着虚函数speak函数地址直接调用。

所有的Cat对象(不管在全局去、栈、堆)公用同一份虚表。

下图是汇编证明调用speak函数的汇编代码。

如果子类没有重写父类的虚函数,子类对象的虚表中存着的就是父类的虚函数地址。

子类重写

如果子类继承父类的成员函数,当父类的实现满足不了子类的需求时,并且子类想在父类的基础上增加自己的实现。

如果父类可以满足子类需求,子类不用重写父类函数,直接调用继承父类的成员函数。