C++ 多态学习笔记
C++ 的多态性是面向对象程序设计的三大特性之一(封装、继承、多态),它允许将子类对象赋值给父类对象,从而实现基类指针指向子类对象,实现基类指针调用子类对象的成员函数。
C++ 的多态性主要有两种实现方式:静态多态和动态多态。
-
静态多态:通过函数重载和模板实现。
-
动态多态:通过虚函数实现。
静态多态
函数重载
函数重载是指在同一个作用域内,可以定义 多个名称相同 但 参数列表不同 的函数。注意,不能用 返回值类型 来区分重载函数。
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
double add(double a, double b, double c) {
return a + b + c;
}
Note
编译过程
- 预处理:将源文件中的头文件展开,处理宏定义,删除注释等;
对应命令:g++ -E main.cpp -o main.i
;
-
编译:进行词法分析、语法分析、语义分析,接着 生成汇编代码 ;
-
词法分析:分析源代码中的关键字、标识符、常量等是否合法;
-
语法分析:分析源代码中的语法结构是否合法;
-
语义分析:分析源代码中的语义是否合法;
对应命令:g++ -S main.i -o main.s
;
- 汇编:将 汇编代码 转换为 目标文件 ,即将汇编代码翻译成机器指令;
对应命令:g++ -c main.s -o main.o
;
-
链接:将 目标文件 和 库文件 链接成 可执行文件 ;
-
静态链接:将库文件的代码和目标文件的代码合并成一个可执行文件;
-
动态链接:将库文件的代码和目标文件的代码分开,运行时再动态链接;
对应命令:g++ main.o -o main
。
函数重载的原理是通过 函数名修饰 实现的,即在编译阶段,编译器会根据函数名和参数列表生成一个唯一的函数名。
可以通过 objdump -t main.o
命令查看函数名修饰后的函数名。一般函数名格式为 _ZN + 类名长度 + 类名 + 函数名长度 + 函数名 + E + 参数类型首字母
。
class A {
public:
void func(int a) { // 函数名修饰后:_ZN1A4funcEi
std::cout << "int: " << a << std::endl;
}
void func(double a, double b) { // 函数名修饰后:_ZN1A4funcEdd
std::cout << "double: " << a << " " << b << std::endl;
}
};
int main() {
// ...
}
模板
模板也是一种静态多态。
动态多态
虚函数
C++ 的多态性是通过虚函数实现的,虚函数是在基类中声明的,子类可以重写基类的虚函数,从而实现 基类指针指向子类对象 ,调用子类对象的成员函数。
虚函数表
虚函数表(vtable)是一个存储 虚函数地址 的一维数组,每个类都有一个虚函数表,虚函数表中存储的是虚函数的地址。
虚函数表的创建时机是在 编译阶段 ,存放于 只读数据段(.rodata) 中,是一个全局变量,每个类的虚函数表都是唯一的,这个类的所有实例都会共享这个虚函数表。
虚函数表指针
虚函数表指针(vptr)是一个指向虚函数表的指针,是一个 隐藏的成员变量 ,存在于每个类的对象中,指向这个类的虚函数表。
虚函数表指针是在构造函数中初始化的,指向这个类的虚函数表,当调用虚函数时,会通过这个虚函数表指针找到虚函数表,再找到虚函数的地址,最后调用虚函数。
编译器任务
如果一个类中有虚函数(用 virtual
关键字修饰的函数):
-
那么编译器就会为这个类生成一个
vtable
,存放这个类的虚函数地址。 -
编译器还会在这个类的对象中添加一个
vptr
,用于指向这个类的vtable
。 -
编译器还会在这个类的 构造函数 插入初始化
vptr
的代码,指向这个类的vtable
。 -
编译器还会在这个类的 析构函数 插入清空
vptr
的代码。
对象实例的内存布局如下:
继承下
如果一个类继承了另一个类,在编译阶段,子类的 vtable
赋值流程如下:
-
先将父类的
vtable
复制到子类的vtable
中。 -
再查找子类中 新的虚函数 ,如果有新的虚函数,就将新的虚函数地址 添加 到子类的
vtable
中。 -
最后查找子类中 重写的虚函数 ,如果有重写的虚函数,就将重写的虚函数地址 覆盖 父类的虚函数地址。
因此如果子类中没有新的虚函数和重写的虚函数,那么子类的 vtable
就和父类的 vtable
一样。