多态
引言
多态(Polymorphism)是面向对象程序设计的核心特性,指同一接口,多种实现。C++中的多态主要分为两类:
-
编译时多态(静态多态)
- 通过函数重载 和模板实现
- 函数调用在编译期确定(早期绑定/静态联编)
- 基于参数类型或个数区分同名函数
-
运行时多态(动态多态)
- 通过继承 和虚函数实现
- 函数调用在运行期确定(晚绑定/动态联编)
- 根据对象的实际类型调用对应函数
静态多态
函数重载
函数重载允许在同一作用域 中声明多个同名函数 ,但这些函数的参数列表必须不同。
参数列表不同的具体体现:
- 参数个数不同
- 参数类型不同
- 参数顺序不同
注意: 不能仅通过返回值类型来区分重载函数。
实现原理
C++通过函数名修饰机制支持函数重载,而C语言不支持此特性。
编译过程概述:
- 预编译:将头文件中的函数声明拷贝到源文件中,避免编译时找不到函数定义。
- 编译 :进行语法分析,并符号汇总,生成符号表。
- 汇编 :生成函数名到函数地址的映射,便于后续调用时定位函数定义位置。
- 链接:将多个目标文件的符号表汇总合并。
函数名修饰的核心流程:
- 在编译和汇编阶段 ,编译器根据函数名、参数类型等信息对函数名进行修饰,生成唯一的符号名。
- 不同参数列表的同名函数会被修饰成不同的符号,从而在符号表中区分。
- 示例(GCC编译器):
int sum(int a, int b)→_Z3sumiidouble sum(double a, double b)→_Z3sumdd
- 修饰规则通常包含:
- 前缀(如
_Z) - 函数名长度 + 函数名
- 参数类型首字母(int→i, double→d, 类类型→类名等)
- 前缀(如
模板
模板(template)是 C++ 实现静态多态(编译时多态)的核心机制之一。
其本质是编译器根据模板参数在编译期生成具体代码,从而实现"同一套代码逻辑,适配多种不同类型"的多态行为。
核心原理
模板不是代码,而是代码生成器
- 函数模板 或 类模板 本身不会被编译成任何可直接执行的代码。
- 它只是一个蓝图 ,告诉编译器:当遇到对特定模板参数的使用时,请按此蓝图生成一份具体的代码。
模板实例化
- 当程序使用模板并提供具体类型参数 时,编译器会执行模板实例化。
- 实例化过程:
- 模板参数推导 :根据函数实参或显式指定的类型,确定模板参数
T的具体类型。 - 模板代码替换 :将模板定义中所有
T替换为实际类型,生成一份完整的普通函数或普通类。 - 编译生成的代码:对生成的普通代码执行常规编译。
- 模板参数推导 :根据函数实参或显式指定的类型,确定模板参数
编译期绑定
- 对同一模板函数 ,为不同模板参数实例化出的多个函数,拥有完全不同的函数签名和函数地址。
- 调用时,编译器通过重载决议 或模板参数匹配 ,静态地选择调用哪个实例化版本。
- 所有决策在编译期完成,无运行时开销。
示例
cpp
template<typename T>
T max(T a, T b) {
return a > b ? a : b;
}
int main() {
int i = max(3, 5); // (1) 实例化为 int max(int, int)
double d = max(3.14, 2.71); // (2) 实例化为 double max(double, double)
char c = max('a', 'z'); // (3) 实例化为 char max(char, char)
}
编译器实际生成的代码(概念性):
cpp
int max_int(int a, int b) { return a > b ? a : b; }
double max_double(double a, double b) { return a > b ? a : b; }
char max_char(char a, char b) { return a > b ? a : b; }
多态体现 :同一 max 名称,对三种不同类型生成三个不同函数,调用时根据参数类型静态绑定到对应版本。
运行时多态
虚函数相关
用关键字 virtual 修饰的非静态成员函数 称为虚函数。一旦基类将某个函数声明为 virtual,所有派生类中与该函数同名、同参数列表、同返回值 的函数自动成为虚函数。
派生类重新定义基类的虚函数称为重写(override) 。
作用 :通过基类指针或引用调用该函数时,实际执行的是对象所属派生类的版本。
纯虚函数 :在虚函数声明末尾添加 = 0,如:
cpp
virtual void breathe() = 0;
纯虚函数表示该函数在当前类中没有实现,要求派生类必须提供实现。
注:纯虚函数也可以提供函数体,但需要定义在类外;通常仅用于特殊场景(如析构函数)。
静态绑定与动态绑定
静态绑定(早绑定)
- 编译阶段就确定了函数调用的目标地址。
- 依据指针/引用的静态类型决定调用哪个函数。
- 例(无虚函数):
cpp
class Animal {
public:
void breathe() { cout << "animal breathe" << endl; }
};
class Fish : public Animal {
public:
void breathe() { cout << "fish bubble" << endl; }
};
Fish fh;
Animal* p = &fh; // 静态类型为 Animal*
p->breathe(); // 输出 "animal breathe"
原因 :p 被声明为 Animal*,编译器按此类型直接绑定 Animal::breathe()。
动态绑定(晚绑定)
- 运行阶段根据对象的实际类型动态决定调用的函数。
- 要求:通过基类指针或引用调用虚函数。
- 例(有虚函数):
cpp
class Animal {
public:
virtual void breathe() { cout << "animal breathe" << endl; }
};
class Fish : public Animal {
public:
void breathe() override { cout << "fish bubble" << endl; }
};
Fish fh;
Animal* p = &fh; // 静态类型 Animal*,实际指向 Fish 对象
p->breathe(); // 输出 "fish bubble"
动态绑定原理
虚函数表(vtable)与虚指针(vptr)
动态绑定的底层依赖虚函数表(Virtual Table) 和虚指针(Virtual Pointer)。
虚函数表(vtable)
- 每个包含虚函数的类(或从包含虚函数的类派生的类)都有一个虚函数表。
- 本质 :一个一维数组 ,按声明顺序存放该类所有虚函数的地址(包括继承自基类的虚函数和自身新增的虚函数)。
- 继承时的虚表内容 :
- 若派生类重写 了基类的虚函数,则虚表中对应位置的地址替换为派生类函数地址。
- 若派生类新增虚函数,则将其地址添加在虚表末尾。
- 若派生类未重写 基类虚函数,则虚表中保存基类该函数的地址。
虚指针(vptr)
- 每个含有虚函数的类的对象 中,编译器会隐式插入一个指针 ,称为
vptr。 vptr指向该对象所属类的虚函数表。- 同一类的所有对象共享同一个虚表,但每个对象拥有自己独立的
vptr(指向同一虚表)。
!NOTE\] \[\[为什么每个对象拥有自己独立的 vptr ?\]
参考文章: