
本文深入解析 C++ 多态的底层逻辑,聚焦运行时多态,从概念、实现条件、虚函数机制、虚表原理到抽象类、菱形继承等场景,结合代码与内存图解,全面梳理多态的核心知识与面试考点。
多态(polymorphism)的概念:通俗来说,就是多种形态。多态分为编译时多态(静态多态)和运⾏时多态(动态多态)。
编译时多态(静态多态)主要就是我们前⾯讲的函数重载和函数模板,他们传不同类型的参数就可以调⽤不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的。运⾏时多态,具体点就是去完成某个⾏为(函数),可以传不同的对象就会完成不同的⾏为,就达到多种形态。⽐如买票这个⾏为,当普通⼈买票时,是全价买票;学⽣买票时,是优惠买票(5折或75折);军⼈买票时是优先买票。再⽐如,同样是动物叫的⼀个⾏为(函数),传猫对象过去,就是”(>ω<)喵“,传狗对象过去,就是"汪汪"。多态通俗来讲就是面对同一个任务,不同的对象去完成会有不同的状态。就排队这个任务来说,军人有军人优先通道,残疾人士有残疾人专属通道,普通人有普通人通道等等…

这篇文章重点讲解运行时多态。
多态是在不同继承关系的类对象,去调用同一函数产生了不同行为。例如以买票为例,Student对象继承了Person,Student对象买票半价,Person对象买票全价。

//多态
#include<iostream>
using namespace std;
class Person //基类
{
public:
virtual void BuyTicket()
{
cout << "买票全价" << endl;
}
};
class Student :public Person
{
public:
virtual void BuyTicket()
{
cout << "买票半价" << endl;
}
};
void Func(Person& ps)
{
ps.BuyTicket();
}
void test()
{
Person dh;
Func(dh);
Student Daitou;
Func(Daitou);
}
int main()
{
test();
return 0;
}类成员函数前⾯加
virtual修饰,那么这个成员函数被称为虚函数。 注意:
C++ 的运行时多态,在原理层面上被称为“覆盖”(Overriding)。它的核心机制可以理解为:派生类继承了父类定义的虚函数的接口或声明,但重写了函数的具体的实现。
派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派⽣类的虚函数重写了基类的虚函数。 注意: 在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使⽤,不过在考试选择题中,经常会故意买这个坑,让你判断是否构成多态。
协变是对派生类重写基类的虚函数中必要条件之一 返回值类型相同的唯一松绑;它允许派生类的虚函数返回一个更具体(更窄)的类型。
协变的规则:
假如我们是一家大型产品制造集团的总裁,我们的集团总部(基类 FactoryBase)统一规定所有工厂的 createProduct() 方法必须承诺返回一个通用的 【基础产品】(ProductBase*),集团下有一个专业的子工厂(派生类 SpecificFactory),它在实现时,可以行使协变特权,返回一个更精确 的 【特定产品】 (SpecificProduct*)。
这种做法是安全的,因为 【特定产品】本质上也是【基础产品】的一种,调用者(程序)通过基类指针接收时可以安全地向上转型进行处理,同时又为后续需要特定功能的代码提供了便利,从而在保持多态统一接口的前提下实现了精准的出货(类型)。
// --- 1. 产品层次结构 ---
class ProductBase {
public:
virtual void info() { cout << " -> 通用产品" << endl; }
};
class SpecificProduct : public ProductBase {
public:
// 可选:重写以展示差异
void info() { cout << " -> 特定产品" << endl; }
};
// --- 2. 工厂层次结构(协变发生在这里)---
class FactoryBase {
public:
// 【基类承诺】 返回 ProductBase*
virtual ProductBase* createProduct() const
{
cout << "工厂基类生产:";
return new ProductBase();
}
// 必须有虚析构函数,保证安全,但为了简化,可以省略(实际项目不能省!)
virtual ~FactoryBase() {}
};
class SpecificFactory : public FactoryBase {
public:
// 【协变】
// 派生类重写,返回 ProductBase 的派生类型 SpecificProduct*
SpecificProduct* createProduct() const
{
cout << "特定工厂生产:";
return new SpecificProduct();
}
};
void testCovariance() {
// 使用基类指针指向派生类工厂
FactoryBase* factory = new SpecificFactory();
// 多态调用:
// 尽管 factory 是基类指针,但它调用了 SpecificFactory 的 createProduct()
ProductBase* product = factory->createProduct();
// 验证实际类型
product->info();
// 释放内存
delete product;
delete factory;
}
int main() {
testCovariance();
return 0;
}
不使用协变的代码在功能上是完全正确的,它实现了多态。协变只是一个“语法糖”,让派生类能返回更精确的类型,以提高代码的清晰度和类型安全性。
当我们在多态场景下,使用基类指针(如
Person*)指向一个派生类对象(如new Student)时,我们期望在调用delete时,系统能根据指针实际指向的类型来调用正确的析构函数。如果基类的析构函数不是虚函数,delete过程就会执行静态绑定,只根据指针的静态类型(Person*)调用~Person(),从而导致子类的析构函数(~Student())被跳过,造成子类资源的内存泄漏。因此,我们必须将基类的析构函数声明为virtual,强制启用动态绑定,确保在运行时能正确地从派生类(~Student())开始,完整地执行整个析构链,从而安全地清理所有资源。


当我们在多态场景下使用虚析构函数时,~Person() 析构函数会被调用两次,这是因为程序销毁了两个不同的对象。第一次调用发生在执行 delete st; 时:由于 st 指向的是 Student 对象,多态机制首先调用 ~Student() 清理派生类成员,然后编译器会自动、隐式地调用一次基类析构函数 ~Person(),完成对 Student 对象中继承自 Person 部分的清理。第二次调用则完全独立,它发生在 delete ps; 时,是为了销毁 ps 指向的那个纯粹的 Person 对象本身,从而确保了所有动态分配的内存都得到了正确的释放。
C++ 析构函数(Destructor)的重写机制是一个特殊的规则,它看似违反了虚函数重写中“函数名称必须相同”的必要条件,但实际上,这是语言标准为了保证多态的完整性而设立的例外。
其原理在于:
Person)的析构函数被声明为 virtual,那么所有派生类(Student)的析构函数将自动被视为虚函数,并自动重写基类的虚析构函数,即便它们的符号名必须遵循各自的类命名规则(即 ~Person 和 ~Student)。delete 时,程序依赖于对象的虚指针(vptr)查找到 VTable 中这个固定的析构函数槽位,从而获得实际对象类型的析构函数地址。这确保了在运行时能够正确地调用派生类的析构函数,保证了完整的、从派生类到基类的链式清理,是解决多态场景下内存泄漏问题的核心机制。因此,析构函数名称的不同是 C++ 语法要求,而其重写是 C++ 运行时多态机制强制实现的语言特性。
override 用于检查派生类虚函数是否重写了基类的某个虚函数,如果没有则报错 注意:override要放在被检查的派生类的虚函数的声明的最后
// error C3668: “Benz::Drive”: 包含重写说明符“override”的⽅法没有重写任何基类⽅法
//函数名的不同
class Car {
public:
virtual void Dirve() //D i r v e
{
}
};
class Benz :public Car {
public:
virtual void Drive() override { cout << "Benz-舒适" << endl; }
//D r i v e
};
int main()
{
return 0;
}final用于修饰虚函数,表明这个虚函数不能被重写 注意:final同时还可以修饰类,说明这个类不能被继承
//error C3248: “Car::Drive”: 声明为“final”的函数⽆法被“Benz::Drive”重写
class Car
{
public :
virtual void Drive() final {}
};
class Benz :public Car
{
public :
virtual void Drive() { cout << "Benz-舒适" << endl; }
};
int main()
{
return 0;
}
// 1. 重载版本 A (无参数)
void eat() const
{
cout << "Animal: 默认进食方式。" << endl;
}
// 2. 重载版本 B (带参数:食物)
void eat(const string& food) const
{
cout << "Animal: 正在吃 " << food << endl;
}
// 3. 重载版本 C (带参数:数量)
void eat(int amount) const
{
cout << "Animal: 吃了 " << amount << " 份食物。" << endl;
}
};
int main()
{
Animal a;
a.eat(); // 调用版本 A
a.eat("肉"); // 调用版本 B
a.eat(3); // 调用版本 C
return 0;
}
class Animal {
public:
// 1. 基类虚函数 (必须是虚函数)
virtual void move() const
{
cout << "Animal: 四肢移动。" << endl;
}
};
class Bird : public Animal {
public:
// 2. 派生类重写 (签名必须一致)
void move() const override
{ // 使用 override 确保是重写
cout << "Bird: 飞行移动。" << endl;
}
};
int main()
{
Animal* pAnimal = new Bird(); // 基类指针指向派生类对象
// 多态:通过基类指针调用虚函数
pAnimal->move(); // 运行时调用 Bird::move()
delete pAnimal;
return 0;
}
class Animal {
public:
// 1. 被隐藏的基类函数 A (无参数)
void sleep() const
{
cout << "Animal: 睡 8 小时。" << endl;
}
// 2. 被隐藏的基类函数 B (带参数)
void sleep(int hours) const
{
cout << "Animal: 睡 " << hours << " 小时。" << endl;
}
};
class Dog : public Animal {
public:
// 3. 隐藏者:函数名相同,但参数列表不同!
void sleep(const string& position) const
{
cout << "Dog: 趴着睡。" << endl;
}
};
int main()
{
Dog d;
// d.sleep(); // 编译错误!基类的无参版本被隐藏了。
// d.sleep(5); // 编译错误!基类的带参版本也被隐藏了。
// 只能调用自己的版本:
d.sleep("窝里"); // 输出: Dog: 趴着睡。
// 必须使用作用域解析符来调用被隐藏的基类函数:
d.Animal::sleep(); // 输出: Animal: 睡 8 小时。
}
在虚函数的后⾯写上 =0 ,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派⽣类重写,但是语法上可以实现),只要声明即可。 包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写实例化不出对象。
// --- 抽象类(只定义接口)---
class BaseInterface {
public:
// 纯虚函数:强制派生类必须实现此功能
virtual void mustImplement() const = 0;
// 必须要有虚析构函数
virtual ~BaseInterface() {}
};
// --- 派生类(提供实现)---
class ConcreteDerived : public BaseInterface {
public:
// 实现了纯虚函数,因此可以被实例化
void mustImplement() const override
{
cout << "实现完成!" << endl;
}
};
int main()
{
// BaseInterface bi; // 编译错误:抽象类不能实例化
// 必须通过指针或引用使用,实现多态
BaseInterface* obj = new ConcreteDerived();
obj->mustImplement();
delete obj;
return 0;
}
普通函数继承本质上是一种实现继承,它关注的是代码的复用:派生类直接继承并使用基类提供的具体函数实现。与此相对,虚函数继承本质上是一种接口继承,它关注的是规范:派生类继承了基类函数的接口签名,目的是重写这个实现,从而在运行时能够根据对象的实际类型调用正确的版本,实现多态性。因此,遵循最小开销原则,如果一个函数不打算在继承体系中被多态调用,就不应该将其定义为虚函数,以避免引入不必要的运行时开销和内存负担。
普通的函数调用他不符合多态,编译器在编译的时候就以及确定了要确定函数的地址(静态绑定);而多态依赖于虚函数和动态绑定,函数地址的确定被推迟到程序运行的阶段。 在x86环境下指针的大小为四字节,并且在虚函数表的最后放一个nullptr的指针,作为vs2022打印虚表的依据

我们发现a中有一个_vfptr和一个_a,_vfptr是一个一级指针,它指向了虚函数表的地址(虚函数表的本质是一个数组,存放着虚函数的指针)。

当我们新增了两个函数我们发现对象a中还是一个指针_vfptr和一个变量_a,但是指针中存放了两个虚函数的指针(因为func3()并不是虚函数所以没用被放入_vfptr中) _vfptr是虚函数表指针,在x86环境下是4个字节,在x64环境下是8个字节。
class A
{
public:
virtual void func() { cout << "func()" << endl; }
virtual void func1() { cout << "func1()" << endl; }
void func2() { cout << "func2()" << endl; }
private:
int _a;
};
class B : public A
{
public:
virtual void func() { cout << "Bfunc()" << endl; }
private:
int _b;
};
int main()
{
A a;
B b;
return 0;
}

派生类虚表的生成规则:

class Daitou
{
public:
virtual void func() { cout << "wobushidaitou!"; }
};
int main()
{
int a = 0;
cout << "a是普通变量,存储在栈上" << endl;
cout << "栈的地址为:" << &a << endl;
int* ptr = new int;
cout << "ptr是动态开辟的指针,存储在堆上" << endl;
cout << "堆的地址为:" << ptr << endl;
static int b = 0;
cout << "b是静态变量,存储在数据段(静态区)" << endl;
cout << "数据段的地址为:" << &b << endl;
const char* str = "daitou";
cout << "str是常量字符串,常量区" << endl;
cout << "代码段的地址为:" << (void*)str << endl;
Daitou d;
cout << "a是普通变量,存储在栈上" << endl;
cout << "虚表的地址为:" << (void*)*((int*)&d) << endl;
return 0;
}str字符串指向的首个字符的地址,但是使用cout的形式打印会被识别为字符串打印,使用void*强制转换指针类型就可以打印出常量字符串的地址。

虚表很有可能储存在代码段。
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票全价" << endl;
}
};
class Student :public Person
{
public:
virtual void BuyTicket()
{
cout << "买票半价" << endl;
}
};
void func(Person& ps)
{
ps.BuyTicket();
}
int main()
{
Person ps;
Student st;
func(ps);
func(st);
return 0;
}
在反汇编中,
call和eax是 x86 汇编中非常基础且关键的指令/寄存器,结合虚函数表场景,具体含义如下:call的作用是跳转到指定地址执行函数,同时会自动将当前指令的下一条地址(“返回地址”)压入栈中,方便函数执行完后能回到原位置继续运行。eax是 x86 架构中的一个 32位通用寄存器,用途广泛:
int、指针等)通常会放在 eax 中,调用者通过读取 eax 获取结果。这里:
eax 临时存储对象地址(虚函数指针),用于后续取虚表指针;call 负责跳转到虚表中存储的函数地址,完成动态绑定调用。如果基类有虚函数,那么每个对象内部都有一个指向虚函数表的指针。
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() {
cout << "Base::show()" << endl;
}
int base_data = 10;
};
class Derived : public Base {
public:
virtual void show() override {
cout << "Derived::show()" << endl;
}
int derived_data = 20;
};
int main() {
Derived derived_obj;
// 情况1:基类指针指向派生类对象 - 多态正常工作
Base* base_ptr = &derived_obj;
cout << "基类指针调用: ";
base_ptr->show(); // 输出: Derived::show()
// 情况2:基类引用指向派生类对象 - 多态正常工作
Base& base_ref = derived_obj;
cout << "基类引用调用: ";
base_ref.show(); // 输出: Derived::show()
// 情况3:对象切片 - 多态失效!
Base base_obj = derived_obj; // 对象切片发生
cout << "基类对象调用: ";
base_obj.show(); // 输出: Base::show() ← 不是我们期望的派生类版本!
// 验证切片效果
cout << "base_data: " << base_obj.base_data << endl; // 正常复制
// cout << base_obj.derived_data << endl; // 错误!派生类数据被切掉了
return 0;
}在类中同类型的对象共用一张虚表的时候,他们的虚表指针,虚表内容完全相同。
//基类
class Base
{
public:
virtual void func1() { cout << "Base::func1()" << endl; }
virtual void func2() { cout << "Base::func2()" << endl; }
private:
int _a;
};
//派生类
class Derive : public Base
{
public:
virtual void func1() { cout << "Derive::func1()" << endl; }
virtual void func3() { cout << "Derive::func3()" << endl; }
virtual void func4() { cout << "Derive::func4()" << endl; }
private:
int _b;
};


我们可以通过查看一下派生类虚表指针对应的虚表的内存窗口的情况

想要手动通过代码查看的话
typedef void(*VFPTR)(); //虚函数指针类型重命名
//打印虚表地址及其内容
void PrintVFT(VFPTR* ptr)
{
printf("虚表地址:%p\n", ptr);
for (int i = 0; ptr[i] != nullptr; i++)
{
printf("ptr[%d]:%p-->", i, ptr[i]); //打印虚表当中的虚函数地址
ptr[i](); //使用虚函数地址调用虚函数
}
printf("\n");
}
int main()
{
Base b;
PrintVFT((VFPTR*)(*(int*)&b)); //打印基类对象b的虚表地址及其内容
Derive d;
PrintVFT((VFPTR*)(*(int*)&d)); //打印派生类对象d的虚表地址及其内容
return 0;
}//基类1
class Base1
{
public:
virtual void func1() { cout << "Base1::func1()" << endl; }
virtual void func2() { cout << "Base1::func2()" << endl; }
private:
int _b1;
};
//基类2
class Base2
{
public:
virtual void func1() { cout << "Base2::func1()" << endl; }
virtual void func2() { cout << "Base2::func2()" << endl; }
private:
int _b2;
};
//多继承派生类
class Derive : public Base1, public Base2
{
public:
virtual void func1() { cout << "Derive::func1()" << endl; }
virtual void func3() { cout << "Derive::func3()" << endl; }
private:
int _d1;
};

使用代码查看
typedef void(*VFPTR)(); //虚函数指针类型重命名
//打印虚表地址及其内容
void PrintVFT(VFPTR* ptr)
{
printf("虚表地址:%p\n", ptr);
for (int i = 0; ptr[i] != nullptr; i++)
{
printf("ptr[%d]:%p-->", i, ptr[i]); //打印虚表当中的虚函数地址
ptr[i](); //使用虚函数地址调用虚函数
}
printf("\n");
}
int main()
{
Base1 b1;
Base2 b2;
PrintVFT((VFPTR*)(*(int*)&b1)); //打印基类对象b1的虚表地址及其内容
PrintVFT((VFPTR*)(*(int*)&b2)); //打印基类对象b2的虚表地址及其内容
Derive d;
PrintVFT((VFPTR*)(*(int*)&d)); //打印派生类对象d的第一个虚表地址及其内容
PrintVFT((VFPTR*)(*(int*)((char*)&d + sizeof(Base1)))); //打印派生类对象d的第二个虚表地址及其内容
return 0;
}
这里我们可以发现在多继承的虚函数重写后,它们在对应的两张虚表中的地址不同,这是为什么?
Derive对象d的内存布局是:[Base1子对象(vptr1 + _b1)][Base2子对象(vptr2 + _b2)][_d1]。
Base1& a = d(a是Base1类型引用)时:a指向d的开头(即Base1子对象的开头),vptr1(Base1的虚表指针)中func1的地址是Derive::func1的真实地址。a.func1()时,this指针直接指向d的开头(和Base1子对象地址一致),无需调整,直接调用Derive::func1,正确访问d的成员。Base2& b = d(b是Base2类型引用)时:b指向d中Base2子对象的开头(比d的整体地址偏移sizeof(Base1)(8字节)),vptr2(Base2的虚表指针)中func1的地址是编译器特殊处理后的“跳板地址”。b.func1()时,this原本指向Base2子对象的开头(非d的开头),但通过“跳板地址”的逻辑,this会被减去Base1的大小(8字节),调整到d的开头,最终正确调用Derive::func1,保证访问d成员时地址正确。这就是多继承中,Derive::func1在Base1和Base2的虚表中地址不同的原因——Base2的虚表需要通过“跳板”调整this指针,而Base1的虚表无需调整,直接使用真实地址。
#include <iostream>
using namespace std;
class A
{
public:
A(char* s) { cout << s << endl; }
~A() {};
};
class B : virtual public A
{
public:
B(char* s1, char* s2)
:A(s1)
{
cout << s2 << endl;
}
};
class C : virtual public A
{
public:
C(char* s1, char* s2)
:A(s1)
{
cout << s2 << endl;
}
};
class D : public B, public C
{
public:
D(char* s1, char* s2, char* s3, char* s4)
:B(s1, s2)
, C(s1, s3)
, A(s1)
{
cout << s4 << endl;
}
};
int main()
{
D* p = new D("class A", "class B", "class C", "class D");
delete p;
return 0;
}A.class A class B class C class D B.class D class B class C class A C.class D class C class B class A D.class A class C class C class D
class Base1
{
public:
int _b1;
};
class Base2
{
public:
int _b2;
};
class Derive : public Base1, public Base2
{
public:
int _d;
};
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}A.p1 == p2 == p3 B.p1 < p2 < p3 C.p1 == p3 != p2 D.p1 != p2 != p3
#include <iostream>
using namespace std;
class A
{
public:
virtual void func(int val = 1)
{
cout << "A->" << val << endl;
}
virtual void test()
{
func();
}
};
class B : public A
{
public:
void func(int val = 0)
{
cout << "B->" << val << endl;
}
};
int main()
{
B* p = new B;
p->test();
return 0;
}A.A->0 B.B->1 C.A->1 D.B->0 E.编译错误 F.以上都不正确
参考答案
题号 | 答案 | 题号 | 答案 |
|---|---|---|---|
1 | A | 6 | D |
2 | D | 7 | D |
3 | C | 8 | A |
4 | A | 9 | C |
5 | B | 10 | B |
特性 | 重载(Overload) | 重写(覆盖,Override) | 重定义(隐藏,Hide) |
|---|---|---|---|
作用域 | 同一作用域(如同一类) | 不同作用域(基类+派生类) | 不同作用域(基类+派生类) |
核心条件 | 函数名相同,参数列表不同(个数/类型/顺序) | 虚函数 + 函数名、参数、返回值(协变除外)完全相同 | 函数名相同(无其他限制) |
inline属性(虚函数需入虚表,而inline函数无地址)。
this指针,而虚函数调用需通过this访问虚表。
delete派生类对象时,需虚析构保证派生类析构被调用(如A* a = new B; delete a;场景)。
virtual void func() = 0;)的类。C++ 多态是面向对象的核心特性,静态多态依赖编译期函数重载,动态多态由虚函数表与动态绑定实现。虚函数重写、基类指针 / 引用调用是动态多态的关键条件,虚表在编译期生成并存储于代码段,抽象类强制接口继承。理解这些机制,能帮助我们在继承体系中高效实现 “同一接口,不同行为” 的设计,同时规避内存泄漏、二义性等问题,是掌握 C++ 面向对象编程的关键。