本文主要内容来自《CPU眼里的C/C++》
查看本节内容前,最好 首先阅读:
C++语法|虚函数与多态详细讲解(一)|再谈构造函数(构造函数与虚函数表)
C++语法|虚函数与多态详细讲解(二)|深入理解虚函数
系列汇总讲解,请移步:
C++语法|虚函数与多态详细讲解系列(包含多重继承内容)
首先我们需要明确一点,很多语言都支持多态,与其说多态是语法规则,不如说是一种设计技巧,多态不仅有代码复用的特性,它的精髓就在于虚函数的存在,使得我们能够完成函数的“覆盖”或者说“重写”。
我们首先要明确多态的基本规则:
多态,常会用基类指针指向派生类对象
多态,会利用派生类的结构特点复用基类的属性(变量/函数)
多态,会利用虚函数来扩展派生类的特性。
多态与指针
我们写一个简单的类A,再写一个简单的类B。我们用基类指针变量 p 指向派生类对象b:
class A {
public:
int x;
int y;
}
class B : public A {
public:
int z;
}
void func1(B& b) {
A* p = &b;
}
毫不意外,我们的编译能够顺利通过,现在我们写一个不符合多态原则的函数 func2,也就是用派生类指针 p 指向基类对象 a:
void fun2(A& a) {
//编译报错!
B *p = &a
}
好嘛,那我们来强制转换一下:
void fun2(A& a) {
B *p = (B*)&a;
}
现在我们能顺利通过了,但是这样安全吗?我们甚至能这样进行类型转换:
void func3(B& b) {
A* p = (int*)&b;
}
毫无疑问,这样绝对是不安全的。
其实只要我们做类型转换,编译器都会发出警告,唯独给多态(派生类向基类做指针转换)开了一个特例。
那么这样做就是安全的吗??
其实是安全的,解析来我们一起分析他们的内存分布。
首先对于类A的结构:
开始的4字节,分配给了变量x,随后分配给了变量 y 。
如果有虚函数,x, y 就需要同时上移4个字节,把起始的4字节,留给我们的 vptr(该内容移步文章:vptr的来源),用于存放类A的虚函数表地址,如下所示:
同理,我们来看看类B的内存分布:
所以说,我们只要不考虑使用派生类B的变量 z ,我们完全可以把派生类B降级为基类A来使用。这里也就是我们常说的代码复用了!
void func4(B& b) {
A *p = &b;
p->x = 1;
p->y = 2;
}
但是需要格外注意的就是,我们不能够把基类A升级为派生类B来使用,因为A的内存大小是小于派生类B的(因为派生类B实现了自己特有的属性),这样会造成非法的内存访问,程序崩溃。
多态与虚函数
如果我们这样定义基类A和派生类B呢?
class A {
public:
int x;
int y;
virtual int vfunc() {
return -1;
}
};
class B : public A {
int z;
virtual int vfunc() {
return z;
}
};
可以知道,我们类A的虚函数地址存储在A的虚函数表中;
B的虚函数地址存储在B的虚函数表中;
所以,根据动态绑定的实现原理:
无论指针 p 是什么类型,只要他指向的是类 A 的对象时,它就会调用类 A特有的虚函数;当指针 p 指向类 B 的对象时,它就会指向类 B 特有的虚函数。
这就是多态的精髓:调用相同名称的函数,会根据对象的实际类型,执行不同的函数版本。当软件代码变得越发复杂的时候,这种设计方法可以消灭大量的switch case语句。