跳转至

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

编译过程

  1. 预处理:将源文件中的头文件展开,处理宏定义,删除注释等;

对应命令:g++ -E main.cpp -o main.i

  1. 编译:进行词法分析、语法分析、语义分析,接着 生成汇编代码

  2. 词法分析:分析源代码中的关键字、标识符、常量等是否合法;

  3. 语法分析:分析源代码中的语法结构是否合法;

  4. 语义分析:分析源代码中的语义是否合法;

对应命令:g++ -S main.i -o main.s

  1. 汇编:将 汇编代码 转换为 目标文件 ,即将汇编代码翻译成机器指令;

对应命令:g++ -c main.s -o main.o

  1. 链接:将 目标文件库文件 链接成 可执行文件

  2. 静态链接:将库文件的代码和目标文件的代码合并成一个可执行文件;

  3. 动态链接:将库文件的代码和目标文件的代码分开,运行时再动态链接;

对应命令: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 and vptr

继承下

如果一个类继承了另一个类,在编译阶段,子类的 vtable 赋值流程如下:

  1. 先将父类的 vtable 复制到子类的 vtable 中。

  2. 再查找子类中 新的虚函数 ,如果有新的虚函数,就将新的虚函数地址 添加 到子类的 vtable 中。

  3. 最后查找子类中 重写的虚函数 ,如果有重写的虚函数,就将重写的虚函数地址 覆盖 父类的虚函数地址。

因此如果子类中没有新的虚函数和重写的虚函数,那么子类的 vtable 就和父类的 vtable 一样。

笔记