news 2026/3/26 12:34:20

C++:抽象类与多态原理深度解析,从纯虚函数到虚表机制(附高频面试题)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++:抽象类与多态原理深度解析,从纯虚函数到虚表机制(附高频面试题)

一. 纯虚函数与抽象类:强制接口规范的“契约”

在实际开发中,我们经常需要定义一个“只规定行为,不提供具体实现”的类。C++ 通过纯虚函数抽象类实现这种 “接口契约”。

本文代码示例所需头文件

#include<iostream> using namespace std;

1.1 纯虚函数:没有实现的 “接口声明”

在虚函数的声明后加=0,该函数即为纯虚函数。纯虚函数无需在基类中实现(语法上允许实现,但无实际意义,因为会被派生类重写),其核心作用是“强制派生类必须重写该函数”

1.2 抽象类:包含纯虚函数的 “不可实例化类”

包含纯虚函数的类称为抽象类,它有两个关键特性:

  • 无法直接实例化对象(编译器会报错);
  • 派生类若未重写基类的所有纯虚函数,自身也会成为抽象类,同样无法实例化。

有了上面的知识储备,我们来看下代码示例吧:

// 抽象类:包含纯虚函数Drive() class Car { public: // 纯虚函数:只声明接口,不提供实现 virtual void Drive() = 0; }; // 派生类Benz:重写纯虚函数,成为“具体类” class Benz :public Car { public: // 必须重写Drive(),否则Benz也是抽象类 virtual void Drive() { cout << "Benz-舒适" << endl; } }; // 派生类BMW:重写纯虚函数,成为“具体类” class BMW :public Car { public: virtual void Drive() { cout << "BMW-操控" << endl; } }; int main() { // 抽象类无法实例化对象 // Car car; // 用抽象类指针指向派生类对象(多态核心用法) Car* pBenz = new Benz; pBenz->Drive(); // 多态调用:输出“Benz-舒适” Car* pBMW = new BMW; pBMW->Drive(); // 多态调用:输出“BMW-操控” return 0; }

二. 多态的底层原理:虚表指针与虚函数表

当我们用基类指针调用派生类的虚函数时,编译器如何 “知道” 该调用哪个类的函数?答案藏在虚表指针(vfptr)虚函数表(vtable)中 —— 这是 C++ 实现动态绑定(运行时多态)的核心机制。

2.1 虚表指针(vfptr):对象中的 “导航器”

首先通过下面这个题目来验证一下虚表指针的存在
下面编译为32位程序的运行结果是什么(D
A. 编译报错 B. 运行报错 C. 8 D. 12

class Base { public: // 虚函数:触发编译器生成虚表指针 virtual void Func1() { cout << "Func1()" << endl; } virtual void Func2() { cout << "Func2()" << endl; } // 普通函数 void Func3() { cout << "Func3()" << endl; } protected: int _b = 1; char _ch = 'x'; }; int main() { Base b; //除了我们能看到的_b和_ch,其实有虚函数的类就会有一个虚函数表指针(32位下4字节,64位下8字节) //因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。 cout << sizeof(b) << endl;//32位:4+4+1->12 // 输出结果:32位环境下为12字节,64位环境下为16字节 return 0; }

关键结论

  • 只要类中包含虚函数(或继承自含虚函数的类),该类的对象就会额外存储一个虚表指针
  • 虚表指针通常位于对象内存的最前端(不同编译器可能有差异),其作用是 “指向该类的虚函数表”;
  • 同类型的对象共用一张虚函数表,但每个对象都有独立的虚表指针(指向同一张虚表)。

2.2 多态的实现原理

从底层的角度Func函数中ptr->BuyTicket(),是如何作为ptr指向Person对象调用Person::BuyTicket,ptr指向Student对象调用Student::BuyTicket的呢?通过下图我们可以看到,满足多态条件后,底层不再是编译时通过调用对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。第⼀张图,ptr指向的Person对象,调用的是Person的虚函数;第二张图,ptr指向的Student对象,调用的是Student的虚函数。

class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl; } private: string _name; }; class Student : public Person { public: virtual void BuyTicket() { cout << "买票-打折" << endl; } private: string _id; }; void Func(Person ptr) { // 这里可以看到虽然都是Person指针Ptr在调用BuyTicket // 但是跟ptr没关系,而是由ptr指向的对象决定的。 ptr.BuyTicket(); } int main() { // 其次多态不仅仅发⽣在派⽣类对象之间,多个派⽣类继承基类,重写虚函数后 // 多态也会发⽣在多个派⽣类之间。 Person ps; Student st; Func(ps); Func(st); ////这三个的虚函数表是一样的,同类型的对象共用一虚表 //Person p1; //Person p2; //Person p3; return 0; }

正确处理:

//方案 1:用「基类引用」传参(推荐,更简洁) // 关键修改:形参改为 Person&(基类引用) void Func(Person& ptr) { ptr.BuyTicket(); // 触发多态:由引用指向的实际对象类型决定调用版本 } int main() { Person ps; Student st; Func(ps); // 引用指向Person对象 → 调用Person::BuyTicket Func(st); // 引用指向Student对象 → 调用Student::BuyTicket return 0; } //方案 2:用「基类指针」传参(经典多态写法) // 关键修改:形参改为 Person*(基类指针) void Func(Person* ptr) { ptr->BuyTicket(); // 触发多态:由指针指向的实际对象类型决定调用版本 } int main() { Person ps; Student st; Func(&ps); // 指针指向Person对象 → 调用Person::BuyTicket Func(&st); // 指针指向Student对象 → 调用Student::BuyTicket return 0; }

2.3 虚函数表(vtable):存储虚函数地址的 “数组”

虚函数表(简称 “虚表”)是编译器为每个含虚函数的类生成的一张 “虚函数指针数组”,数组中存储的是该类所有虚函数的地址。其结构与生成规则如下:

  • 基类虚表:存储基类所有虚函数的地址(如Base类的虚表存储Func1Func2的地址);

  • 派生类虚表

  • 首先继承基类虚表的所有内容;
  • 若派生类重写了基类的虚函数,会用派生类自身的虚函数地址 “覆盖” 基表中 对应的位置;
  • 派生类新增的虚函数,其地址会追加到虚表的末尾;
  • 虚表结尾标记:部分编译器(如 VS)会在虚表末尾添加0x00000000作为结束标记(g++ 无此标记,C++ 标准未强制规定)。

  • 注意:同类型的对象共用同一张虚表,不同类型的对象都有各自独立的虚表。

class Base { public: virtual void func1() { cout << "Base::func1" << endl; } virtual void func2() { cout << "Base::func2" << endl; } // 普通函数:不存入虚表 void func5() { cout << "Base::func5" << endl; } protected: int a = 1; }; class Derive : public Base { public: // // 重写基类的func1:会覆盖虚表中func1的地址 virtual void func1() { cout << "Derive::func1" << endl; } // 派生类新增虚函数:会追加到虚表末尾 virtual void func3() { cout << "Derive::func1" << endl; } // 普通函数:不存入虚表 void func4() { cout << "Derive::func4" << endl; } protected: int b = 2; }; int main() { Base b; Derive d; return 0; }

2.4 动态绑定与静态绑定

  • 对不满足多态条件(指针或者引用+调用虚函数)的函数调用在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定。
  • 满足多态条件的函数调用是在运行时绑定,也就是运行时到指定对象的虚函数表中找到调用函数的地址,也叫做动态绑定。

当用基类指针调用虚函数时,编译器会按以下步骤完成 “动态绑定”(运行时确定调用的函数):

  • 获取虚表指针:从基类指针指向的对象中,取出虚表指针(vfptr);
  • 查找虚表:通过虚表指针找到该对象所属类的虚函数表(vtable);
  • 定位函数地址:在虚表中找到目标虚函数对应的地址(若派生类重写过,此处就是派生类函数地址);
  • 调用函数:通过找到的函数地址,调用对应的虚函数。

以之前的 “买票” 场景为例,流程如下:

// 基类指针指向派生类对象 Person* ptr = new Student; // 动态绑定流程: 1. 从ptr指向的Student对象中,取出vfptr; 2. 通过vfptr找到Student类的虚表; 3. 在虚表中找到BuyTicket对应的地址(Student::BuyTicket的地址); 4. 调用该地址对应的函数,输出“买票-打折”。

三. 关键问题辨析与总结

3.1 虚函数存在哪里?虚表又存在哪里?

四. 多态考察的一些常见问题(重点,面试高频题)

1. 面向对象的三大特性(这里重点讲讲什么是多态)

2. 什么是重载,重写(覆盖),重定义(隐藏)?

特性定义示例
重载同一类中,方法名相同,参数列表(参数类型、个数、顺序)不同,与返回值类型无关类中add(int a, int b)add(double a, double b)
重写(覆盖)子类继承父类后,对父类的虚函数进行重新实现,方法名、参数列表、返回值类型(协变情况除外)完全相同父类Animal的虚函数makeSound(),子类Dog重写为void makeSound() { cout << "汪汪" << endl; }
重定义(隐藏)子类中定义了与父类同名的非虚函数,隐藏父类的该函数父类有func(),子类也定义func(),子类对象调用func()时执行子类的,父类对象调用执行父类的

3. 多态的实现原理?

答:多态通过 虚函数表(vtable)和虚函数指针(vptr) 实现。每个包含虚函数的类都有一个虚函数表,表中存储着该类所有虚函数的地址。每个对象都有一个虚函数指针,指向所属类的虚函数表(相同类型的对象指向同一张虚函数表)。当通过父类指针或者引用调用虚函数时,程序会根据指向的实际对象类型,通过其虚函数指针找到虚函数表,再找到对应的虚函数地址并调用,从而实现运行时的多态。

4. inline函数可以是虚函数吗?inline属性和虚函数属性能同时存在吗?

:可以是虚函数,从语法上看,inline函数可以声明为虚函数,但实际上编译器会忽略inline属性(inline一般展开是不需要地址的),将其当作普通虚函数处理。因为虚函数要放在虚表中去,两者机制冲突,也就是说inline属性和虚函数属性是不同时存在的。

5. 静态成员可以是虚函数吗?
:不能,静态成员函数属于类,不属于某个对象,没有this指针,而虚函数的调用需要通过对象的虚函数指针来实现,所以静态成员函数不能是虚函数。

6. 构造函数可以是虚函数吗?
:不可以。因为对象中的虚函数指针是在构造函数初始化列表阶段才初始化的,所以构造函数不能是虚函数。

7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

8. 对象访问普通函数快还是虚函数更快?
:首先如果是普通对象调用的话两者是一样快的,但如果是基类的指针或者引用去调用,且构成了多态调用,则调用的普通函数更快,运行时调用虚函数需要到虚函数表中去查找,有一定开销。

使用场景普通函数调用机制虚函数调用机制性能差异根源
普通对象调用编译时直接绑定函数地址,直接跳转执行编译时直接绑定函数地址,直接跳转执行无差异
基类指针/引用多态调用编译时直接绑定函数地址,直接跳转执行运行时通过vptr找vtable,再找函数地址执行虚函数多了查表的运行时开销

9. 虚函数表是在什么阶段生成的,存在哪里的?
:虚函数表是在编译阶段生成的,一般情况下是存在代码段(常量区)的。

10. C++菱形继承的问题?虚继承的原理?

:菱形继承会导致数据冗余和二义性的问题,虚继承则是通过虚基类指针和虚基表(不要把虚函数表,虚函数指针和虚基表,虚基类指针搞混了)实现的中间基类在继承时顶层基类时声明为虚继承,这样可以保证顶层基类的成员只会有一份,解决了数据冗余和二义性的问题。

11. 什么是抽象类?抽象类的作用?
: 包含纯虚函数(形如virtual void func() = 0;)的类,无法实例化对象。抽象类的作用是作为接口规范,强制子类必须重写实现纯虚函数

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/25 13:56:15

为什么map函数比for循环快?性能对比实测

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个性能对比测试&#xff1a;1) 用for循环和map分别处理100万个数据的平方运算 2) 使用timeit模块测量执行时间 3) 分析内存使用差异。要求生成可视化对比图表&#xff0c;并解…

作者头像 李华
网站建设 2026/3/26 3:23:10

如何在Android中使用StateFlow和MutableStateFlow?

在 Android 中,StateFlow + MutableStateFlow 是 MVVM 架构下UI 状态管理的首选方案,核心遵循「内部可变、外部只读」的封装原则,结合 ViewModel 存放状态、Lifecycle 管理订阅生命周期,确保状态安全且无内存泄漏。以下是完整的使用步骤和最佳实践: 一、前置准备(依赖)…

作者头像 李华
网站建设 2026/3/26 0:58:41

OpenProject服务的备份与恢复

1. 参考 Backing up your OpenProject installationRestoring an OpenProject backupLinux安装OpenProject 2. 环境 Docker compose部署的OpenProject服务服务器192.168.7.28 作为主服务器&#xff0c;预装OpenProject服务&#xff0c;提供生产环境服务器192.168.15.96 作为…

作者头像 李华
网站建设 2026/3/25 17:56:03

用于氧化石墨烯的多模态表征与激光还原图案化的共聚焦显微技术

氧化石墨烯&#xff08;GO&#xff09;是制备导电还原氧化石墨烯&#xff08;rGO&#xff09;的重要前驱体&#xff0c;在柔性电子、储能等领域应用广泛。激光还原因无掩模、局部精准的优势成为 GO 图案化关键技术&#xff0c;但传统方法难以实时观察还原过程&#xff0c;制约机…

作者头像 李华