
文章目录
-
-
- [🟢 第一部分:多态的初探与核心概念](#🟢 第一部分:多态的初探与核心概念)
-
- [1. 简短的前置知识准备](#1. 简短的前置知识准备)
- [2. 什么是多态?(Polymorphism)](#2. 什么是多态?(Polymorphism))
- [3. 核心比喻:生活场景与游戏架构](#3. 核心比喻:生活场景与游戏架构)
- [📝 第一部分:高频面试题与解析](#📝 第一部分:高频面试题与解析)
- [🟢 第二部分:多态的构成条件与代码实现](#🟢 第二部分:多态的构成条件与代码实现)
-
- [1. 前提引入:万能遥控器与隐藏身份](#1. 前提引入:万能遥控器与隐藏身份)
- [2. 构成多态的两个"硬性条件"](#2. 构成多态的两个“硬性条件”)
- [3. 什么是虚函数与重写?](#3. 什么是虚函数与重写?)
- [4. C++ 核心 Demo 演示](#4. C++ 核心 Demo 演示)
- [📝 第二部分:高频面试题与解析](#📝 第二部分:高频面试题与解析)
- [🟢 第三部分:虚函数重写(覆盖)的深水区](#🟢 第三部分:虚函数重写(覆盖)的深水区)
-
- [1. 虚函数重写的特例:协变(了解即可)](#1. 虚函数重写的特例:协变(了解即可))
- [2. 最危险的陷阱:析构函数的重写](#2. 最危险的陷阱:析构函数的重写)
- [3. 语法护盾:override 和 final 关键字](#3. 语法护盾:override 和 final 关键字)
- [📝 第三部分:高频面试题与解析](#📝 第三部分:高频面试题与解析)
- [🟢 第四部分:重载、重写与隐藏的终极辨析 & 抽象类](#🟢 第四部分:重载、重写与隐藏的终极辨析 & 抽象类)
-
- [1. 概念大乱斗:重载 vs 重写 vs 隐藏](#1. 概念大乱斗:重载 vs 重写 vs 隐藏)
- [2. 纯虚函数与抽象类:强制规则的制定者](#2. 纯虚函数与抽象类:强制规则的制定者)
- [📝 第四部分:高频面试题与解析](#📝 第四部分:高频面试题与解析)
- [🟢 第五部分:多态的底层原理解剖](#🟢 第五部分:多态的底层原理解剖)
-
- [1. 隐藏在对象开头的秘密:虚函数表指针(_vfptr)](#1. 隐藏在对象开头的秘密:虚函数表指针(_vfptr))
- [2. 多态的核心引擎:虚函数表(vftable)与"覆盖"机制](#2. 多态的核心引擎:虚函数表(vftable)与“覆盖”机制)
- [3. 动态绑定与静态绑定的终极定义](#3. 动态绑定与静态绑定的终极定义)
- [4. 灵魂拷问:虚函数和虚表到底存在哪里?](#4. 灵魂拷问:虚函数和虚表到底存在哪里?)
- [📝 第五部分:高频面试题与解析](#📝 第五部分:高频面试题与解析)
-
🟢 第一部分:多态的初探与核心概念
1. 简短的前置知识准备
在接触多态之前,我们需要明确两个前提:
- 继承关系:类与类之间存在父子关系(基类与派生类)。比如"学生"继承自"人"。
- 指针与对象:通过指针去指向或者操作一个具体的对象。
2. 什么是多态?(Polymorphism)
通俗来说,多态就是"多种形态"。
在C++中,多态分为两大类:
-
编译时多态(静态多态) :这主要指我们以前学过的函数重载 和函数模板。为什么叫"编译时"?因为你传不同类型的参数调用不同函数时,编译器在编译代码的阶段就已经精准匹配好了实参和形参,一切都已经"静态"地定死了。
-
运行时多态(动态多态):这是我们接下来要死磕的重点。它是指去完成某个相同的行为(调用同一个函数),但传入不同的对象,就会产生截然不同的结果。
3. 核心比喻:生活场景与游戏架构
比喻一:火车站的售票窗口
想象一下火车站的"买票"行为。无论谁去,动作都是"买票"。
-
当普通人 买票时,行为结果是:全价买票。
-
当学生 买票时,行为结果是:优惠买票(5折或75折)。
-
当军人 买票时,行为结果是:优先买票。
text
[12306 售票系统 (买票行为 BuyTicket)]
|
+-------------+-------------+
| | |
传入[普通人] 传入[学生对象] 传入[军人对象]
| | |
v v v
【全价出票】 【半价/75折出票】 【优先出票通道】
比喻二:动物的叫声与回合制宠物对战
同样是"叫"这个行为(函数)。
-
传猫 对象过去,结果就是
"(>^w^<)喵"。 -
传狗 对象过去,结果就是
"汪汪"。
在构建一个回合制宠物对战游戏的架构时,这种思想尤为重要。你可以设计一个通用的"攻击(Attack)"指令。当你按下攻击键时,系统并不需要知道当前是哪一种宠物。如果系统接收到的是"火系宠物"对象,它自然会释放火焰技能;如果接收到的是"水系宠物"对象,它就会喷水。
同一个指令(函数调用),因为背后的对象不同,展现出了多种不同的形态,这就是运行时多态的魅力。
📝 第一部分:高频面试题与解析
面试题 1:请简述什么是多态?C++ 中多态分为哪几种?
- 详细解析与答案 :
多态通俗来说就是事物的多种形态。在C++中,多态主要指不同对象接收到相同的消息(调用相同的函数)时,会产生不同的行为。
C++中的多态分为两种:
- 静态多态(编译时多态) :在程序编译阶段就确定了具体的调用目标。典型代表是函数重载 (通过参数列表不同来区分)和泛型编程(模板)。
- 动态多态(运行时多态) :在程序运行阶段,根据具体传入或绑定的对象类型来决定调用哪个具体的函数。典型代表是继承关系下的虚函数(virtual)重写。
面试题 2:为什么函数重载被称为"静态多态"?
- 详细解析与答案 :
"静态"一词在这里指的是"在编译阶段就已经确定"。当我们在代码中调用一个被重载的函数时,编译器会根据我们传入的实参类型和数量,在编译期就去匹配对应的函数版本。如果匹配成功,编译器直接将该函数的地址绑定到调用处。因为这个决策过程不需要等到程序真正运行起来,所以在编译时就已经静态完成了。
🟢 第二部分:多态的构成条件与代码实现
1. 前提引入:万能遥控器与隐藏身份
多态是一个继承关系下的类对象,去调用同一个函数,从而产生了不同的行为。比如 Student 继承了 Person,Person 对象买票是全价,而 Student 对象则是优惠买票。
但在代码层面,程序是怎么知道现在到底该全价还是优惠呢?这就引出了实现多态的核心规则。
2. 构成多态的两个"硬性条件"
要实现多态效果,必须同时满足以下两个重要条件:
-
调用载体:必须是基类的指针或者引用调用虚函数。
-
核心操作:被调用的函数必须是虚函数,并且派生类必须完成了对该虚函数的重写(也叫覆盖)。
为什么要用基类的指针或引用?
打个通俗的比方,基类的指针或引用就像是一个"万能遥控器"。只有基类的指针或引用才能做到"向下兼容"------它既能指向基类对象本身,也能指向派生类对象。如果是直接传对象的值(比如按值传参),在传递过程中会发生拷贝和截断,派生类特有的那部分属性就丢了,多态也就无从谈起。
3. 什么是虚函数与重写?
-
虚函数(virtual) :
在类成员函数的前面加上
virtual修饰,那么这个成员函数就被称为虚函数。注意:非成员函数(类外部的全局函数)是不能加
virtual修饰的。 -
虚函数的重写/覆盖 :
派生类中有一个跟基类完全相同 的虚函数,这就称作派生类的虚函数重写了基类的虚函数。"完全相同"意味着这三个要素必须一模一样:返回值类型、函数名字、参数列表。
text
[ 基类 Person ]
|
+---> 虚函数:virtual void BuyTicket() { 买票全价 }
|
(继承 & 重写)
|
v
[ 派生类 Student ]
|
+---> 虚函数:virtual void BuyTicket() { 买票半价 }
====================================================================
运行时多态的发生地 (动态绑定):
[万能遥控器] ---> Person* ptr
|
+------------------+------------------+
| |
ptr 指向 Person 对象 ptr 指向 Student 对象
| |
调用基类买票全价 调用派生类买票半价
4. C++ 核心 Demo 演示
我们把火车站买票的场景转化为具体的 C++ 代码。仔细观察 Func 函数,这正是多态发生的舞台。
cpp
#include <iostream>
using namespace std;
// 基类:普通人
class Person {
public:
// 1. 定义虚函数
virtual void BuyTicket() {
cout << "买票-全价" << endl;
}
};
// 派生类:学生
class Student : public Person {
public:
// 2. 派生类完成虚函数的重写(函数名、参数、返回值完全一致)
// 注意:派生类这里如果不加virtual关键字,其实也构成重写,但不规范,建议加上。
virtual void BuyTicket() {
cout << "买票-打折" << endl;
}
};
// 3. 必须是基类的指针(或引用)调用虚函数
void Func(Person* ptr) {
// 此时虽然是Person类型的指针在调用BuyTicket
// 但最终执行谁的代码,取决于ptr在运行时真正指向的是哪个对象[cite: 1]!
ptr->BuyTicket();
}
int main() {
Person ps;
Student st;
// 传入基类对象的地址,ptr指向基类,调用全价
Func(&ps);
// 传入派生类对象的地址,ptr指向派生类,调用打折
Func(&st);
return 0;
}
在上述代码中,同一句 ptr->BuyTicket() 根据传入对象的不同,展现出了不同的购票策略,完美契合了多态的定义。
📝 第二部分:高频面试题与解析
面试题 1:派生类在重写基类虚函数时,如果不写 virtual 关键字,还能构成多态吗?为什么?
- 详细解析与答案 :
能构成多态。
在重写基类虚函数时,派生类的虚函数即使不加virtual关键字,也可以构成重写。因为基类的虚函数被继承下来后,在派生类中依旧保持着虚函数属性。但是,这种写法在工程实践中不是很规范,容易造成代码可读性变差,不建议这样使用。在笔试或面试的选择题中,经常会利用这一点故意挖坑让你判断是否构成多态。
面试题 2:以下代码中能成功实现多态吗?如果不可以,请说明理由。
cpp
void CallTicket(Person p) {
p.BuyTicket();
}
- 详细解析与答案 :
不能实现多态。
多态的构成条件明确要求必须是基类的指针或者引用调用虚函数。上述代码使用的是基类对象的"按值传递"。当传入派生类对象时,会发生切片(Slicing)现象,派生类特有的部分会被截断丢弃,仅仅拷贝并构造出一个单纯的基类对象,因此只能调用基类的函数,无法展现多态。
🟢 第三部分:虚函数重写(覆盖)的深水区
1. 虚函数重写的特例:协变(了解即可)
在上一节中我们提到,虚函数重写要求三同:函数名、参数列表、返回值完全相同。但是有一个特例,叫做协变。
通俗比喻 :
假设基类提供了一个"获取交通工具"的虚函数。基类返回的是一个普通的"车"钥匙(基类指针),而派生类重写这个函数时,返回的是更具体的"跑车"钥匙(派生类指针)。虽然返回的钥匙类型不同,但本质上都是车钥匙,这种"向下细化"是被允许的。
核心规则 :
派生类重写基类虚函数时,与基类虚函数返回值类型可以不同。前提是:基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用。
2. 最危险的陷阱:析构函数的重写
这是C++多态中最容易引发严重Bug的地方。如果基类的析构函数不设置为虚函数,极易导致内存泄漏。
比喻:粗心的拆迁大队
假设有一栋别墅(派生类对象),它是在普通地基(基类)上扩建出来的,并且别墅里有一座专属的豪华泳池(派生类动态申请的内存)。
现在你要把这栋别墅拆除,但是你给拆迁大队的图纸只是一个"普通地基"的图纸(基类指针)。
- 如果不加虚析构函数:拆迁大队只看图纸,把地基拆了就走人,结果豪华泳池(派生类的资源)留在了原地,变成了无人管理的废墟(内存泄漏)。
- 如果加上虚析构函数(virtual):相当于告诉拆迁大队:"看清楚实际建筑再拆!"他们会先拆除别墅的泳池(调用派生类析构),再拆除地基(调用基类析构),完美清理现场。
text
[ 基类指针 A* p2 = new B; ]
|
不加 virtual | 加了 virtual
(静态绑定) | (动态绑定)
|
只调用 ~A() | 先调用 ~B(),再调用 ~A()
⚠️ 内存泄漏 | ✅ 完美释放
代码演示:
cpp
class A {
public:
// 强烈建议:基类的析构函数必须加 virtual
virtual ~A() { cout << "~A()" << endl; }
};
class B : public A {
public:
B() { _p = new int[10]; } // 派生类申请了资源
// 派生类析构函数重写了基类析构函数
~B() {
cout << "~B()->delete" << endl;
delete[] _p; // 释放资源
}
protected:
int* _p;
};
int main() {
A* p2 = new B;
// 如果基类析构不是虚函数,这里只会调用 ~A(),导致 B 中的 _p 无法释放!
delete p2;
return 0;
}
特殊机制说明 :基类与派生类的析构函数名字明明不同(~A 和 ~B),为什么能构成重写?因为编译器在底层做了特殊处理,编译后析构函数的名称会被统一处理成 destructor。因此只要基类加了 virtual,派生类的析构函数就自动构成重写。
3. 语法护盾:override 和 final 关键字
C++11 提供了两个关键字,用来在编译阶段帮助我们进行多态的语法检查。
-
override(检查官) :
修饰派生类的虚函数。它的作用是检查该虚函数是否成功重写了基类的某个虚函数,如果没有重写成功(比如函数名拼错了、参数写错了),编译器会直接报错。这极大地避免了运行后才发现预期多态未生效的尴尬。
-
final(终结者) :
修饰基类的虚函数。顾名思义,它表示这个虚函数是"最终版",绝不允许任何派生类去重写它。如果派生类强行重写,编译器会报错。
📝 第三部分:高频面试题与解析
面试题 1:为什么在涉及到继承和多态时,基类的析构函数通常建议设计为虚函数?(高频核心题)
- 详细解析与答案 :
在多态场景下,我们经常使用基类指针去操作(指向)派生类对象。当使用delete释放这个基类指针时,如果基类的析构函数不是虚函数,编译器会采用静态绑定,只调用基类的析构函数。如果派生类内部申请了额外的内存等资源,派生类的析构函数未能被调用,就会导致内存泄漏。
将基类析构函数设计为虚函数(virtual),可以使其构成虚函数重写(编译器会将析构函数统一命名为destructor)。这样在使用delete基类指针时,就会触发多态,先调用派生类的析构函数清理派生类资源,再自动调用基类析构函数,从而保证对象被安全且彻底地销毁。
面试题 2:什么是协变?它违背了虚函数重写的规则吗?
- 详细解析与答案 :
协变是虚函数重写的一个特例,它并没有违背规则,而是规则允许的特殊情况。
普通的虚函数重写要求基类和派生类的函数返回值完全相同。而协变允许两者返回值不同,前提是:基类虚函数返回基类对象的指针或引用,派生类重写的虚函数返回派生类对象的指针或引用。这种设计在某些需要细化返回类型的场景下提供了便利。
🟢 第四部分:重载、重写与隐藏的终极辨析 & 抽象类
1. 概念大乱斗:重载 vs 重写 vs 隐藏
我们先用通俗的比喻来区分它们:
- 重载(Overload)------"一个人换工具":同一个木匠(同一个作用域),他有一个技能叫"砍"。当你给他一把斧头(参数A),他就劈柴;当你给他一把菜刀(参数B),他就切菜。名字相同,但工具(参数)不同。
- 重写/覆盖(Override)------"子承父业且升级核心技术" :父亲(基类)和儿子(派生类)都会"祖传手艺"(同名虚函数)。儿子不仅继承了手艺,还用全新的方式实现了它,把父亲的老方法给覆盖了。
- 隐藏(Hide)------"年轻人的专属黑话屏蔽了长辈":父亲和儿子在各自的圈子(不同作用域)。只要儿子定义了一个和父亲同名的东西(不管参数相不相同),在儿子的地盘里,父亲的那个同名事物就被"屏蔽/隐藏"了。
下面是基于这三个概念的严谨对比,请务必理解记忆:
| 概念 | 作用域要求 | 函数名要求 | 参数与返回值要求 | 虚函数要求 |
|---|---|---|---|---|
| 重载 | 两个函数在同一作用域 | 函数名相同 | 参数不同(类型或个数不同),返回值可同可不同 | 无要求 |
| 重写/覆盖 | 两个函数分别在继承体系的父类和子类不同作用域 | 函数名相同 | 参数、返回值都必须相同(协变例外) | 两个函数都必须是虚函数 |
| 隐藏 | 两个函数分别在继承体系的父类和子类不同作用域 | 函数名相同 | 无要求(只要不构成重写,就是隐藏) | 无要求 |
特别注意 :除了函数,如果父子类定义了名字相同的成员变量,这也叫隐藏。
2. 纯虚函数与抽象类:强制规则的制定者
在实际开发中,有些基类是非常抽象的概念。比如"汽车(Car)",在现实中你买不到一辆名叫"汽车"的具体车,你只能买到"奔驰"或者"宝马"。因此,"汽车"这个类其实不应该被创建出具体的对象,它存在的意义是为了定义规范。
如何实现这种规范呢?这就需要引入纯虚函数。
-
纯虚函数的定义 :在虚函数的后面写上
= 0,则这个函数为纯虚函数。纯虚函数只要声明即可,不需要定义实现(虽然语法上可以实现,但没啥意义,因为它注定要被派生类重写)。 -
抽象类的概念 :包含纯虚函数的类叫做抽象类。
-
抽象类的两大特性:
-
抽象类不能实例化出对象 (你不能
Car car;这样写,编译器会报错)。 -
如果派生类继承了抽象类后,不重写纯虚函数,那么派生类也会变成抽象类,同样无法实例化。
这种机制某种程度上强制了派生类必须重写虚函数,因为不重写就实例化不出对象。
cpp
#include <iostream>
using namespace std;
// 抽象类:汽车(概念模型,制定规则)
class Car {
public:
// 纯虚函数,只需声明,后面加 = 0
virtual void Drive() = 0;
};
// 派生类:奔驰
class Benz : public Car {
public:
// 强制重写纯虚函数,否则Benz也是抽象类
virtual void Drive() {
cout << "Benz-舒适" << endl;
}
};
// 派生类:宝马
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-操控
delete pBenz;
delete pBMW;
return 0;
}
📝 第四部分:高频面试题与解析
面试题 1:请详细对比 C++ 中的重载(Overload)、重写(Override)与隐藏(Hide)的区别。
- 详细解析与答案 :
这是C++面向对象中最核心的考点之一。
- 作用域不同:重载发生在同一作用域内;而重写和隐藏都发生在继承体系下,即分别在基类和派生类的不同作用域中。
- 条件要求不同:
- 重载要求函数名相同,但参数列表必须不同(类型或个数)。
- 重写要求极其严格,必须是虚函数,且函数名、参数列表、返回值必须完全相同(协变除外)。
- 隐藏的要求最宽泛,只要在派生类和基类中有同名的函数(且不满足重写条件),或者有同名的成员变量,基类的同名成员就会在派生类作用域中被隐藏。
- 多态绑定:重写是实现动态多态的基础(动态绑定);而重载属于静态多态(静态绑定)。
面试题 2:什么是纯虚函数和抽象类?它们在实际开发中有什么意义?
- 详细解析与答案 :
在虚函数声明的末尾加上= 0,该函数即为纯虚函数。包含至少一个纯虚函数的类被称为抽象类。
意义:
- 防止非法实例化:抽象类代表一种抽象的概念(如"形状"、"动物"),本身不具备完整的实现细节,因此语法上禁止其实例化出对象,防止了无意义的操作。
- 接口规范与强制重写:纯虚函数相当于定义了一套标准的"接口协议"。它强制要求任何继承该抽象类的派生类都必须提供具体的实现逻辑,否则派生类也会变成抽象类无法使用。这种机制极大地提升了代码的规范性和扩展性。
🟢 第五部分:多态的底层原理解剖
1. 隐藏在对象开头的秘密:虚函数表指针(_vfptr)
如果你在一个类里写了虚函数,这个类的对象在内存中的大小会发生意想不到的变化。我们来看一个测试:
cpp
#include <iostream>
using namespace std;
class Base {
public:
virtual void Func1() {
cout << "Func1()" << endl;
}
protected:
int _b = 1;
char _ch = 'x';
};
int main() {
Base b;
// 在32位系统下,输出结果是 12
cout << sizeof(b) << endl;
return 0;
}
为什么是 12 字节?
按照常规理解,一个 int 占 4 字节,一个 char 占 1 字节,加上内存对齐,应该是 8 字节。那么多出来的 4 字节是什么?
答案是:对象的前面多了一个指针,叫做虚函数表指针(_vfptr) 。(注:v 代表 virtual,f 代表 function)。
一个含有虚函数的类中都至少有一个虚函数表指针。因为一个类所有虚函数的地址,需要被统一存放到一个"表格"里,而这个指针就是用来寻找这个表格的。
2. 多态的核心引擎:虚函数表(vftable)与"覆盖"机制
这个"表格"就是虚函数表(简称虚表) 。它的本质是一个存虚函数指针的指针数组。
多态底层的核心精髓,就在于派生类重写的虚函数,在派生类的虚函数表中会把继承自基类的对应虚函数地址给覆盖掉。
比喻:公司的任务执行手册
假设基类是一份"通用任务手册"(基类虚表),派生类是一份"定制任务手册"(派生类虚表)。
如果派生类没有重写某个任务,它的手册里就抄一份基类的做法;如果派生类重写 了任务,它的手册里就会把原本那一页撕掉,贴上自己的新做法(覆盖)。
我们用字符画来直观展示前面买票例子的内存布局:
text
[基类 Person 对象 ps]
+-------------------+
| _vfptr (指向虚表) | --------> [ Person 虚表 ]
+-------------------+ | [0] Person::BuyTicket() 的地址 |
| _name = "张三" | +-------------------------------+
+-------------------+
[派生类 Student 对象 st]
+-------------------+
| _vfptr (指向虚表) | --------> [ Student 虚表 ]
+-------------------+ | [0] Student::BuyTicket() 的地址 | <--- 发生覆盖!
| _name = "张三" | +-------------------------------+
| _id = "1" |
+-------------------+
当执行 ptr->BuyTicket() 时,底层不再是编译时直接确定函数地址,而是运行时到指向的对象的虚表中确定对应的虚函数的地址。
-
如果
ptr指向Person对象,它就顺着Person对象的_vfptr找到基类虚表,调用全价买票。 -
如果
ptr指向Student对象,它就顺着Student对象的_vfptr找到派生类虚表,此时表里存的是被覆盖后的打折买票地址,所以调用打折买票。
3. 动态绑定与静态绑定的终极定义
理解了底层,我们就可以给出最严谨的定义了:
-
静态绑定 :对不满足多态条件的函数调用(比如不是通过指针/引用调用,或者调用的不是虚函数),编译器在编译时就直接写死了要调用函数的内存地址。
-
动态绑定 :满足多态条件的函数调用,是在运行时绑定,也就是程序跑起来之后,动态地去对象身上的虚函数表里翻找需要调用的函数地址。
4. 灵魂拷问:虚函数和虚表到底存在哪里?
-
虚函数存在哪? 虚函数和普通函数一样,编译好后是一段指令,存在代码段。
-
虚函数表存在哪? C++ 标准其实没有硬性规定,但在主流的 VS 系列编译器下,虚函数表是存在代码段(常量区)的。
📝 第五部分:高频面试题与解析
面试题 1:请简述 C++ 中多态的底层实现原理。
- 详细解析与答案 :
C++ 运行时多态的底层主要依靠虚函数表(vftable)和虚函数表指针(_vfptr)来实现。
- 如果一个类包含了虚函数,编译器在实例化该类的对象时,会在对象的内存布局中(通常是头部)偷偷塞入一个虚表指针(_vfptr)。
- 这个指针指向该类专属的虚函数表。虚表本质上是一个函数指针数组,里面存放着该类所有虚函数的内存地址。
- 当派生类继承基类并重写了某个虚函数时,派生类虚表中的该虚函数地址会被更新替换为派生类自己的函数地址,这个过程称为"覆盖"。
- 当通过基类的指针或引用调用虚函数时,程序在运行时会通过该指针实际指向的对象的
_vfptr找到对应的虚表,并在虚表中查找真正的函数地址来执行。这就是动态绑定。
面试题 2:同一个类的不同对象,它们拥有几张虚函数表?基类和派生类的虚函数表是同一张吗?
- 详细解析与答案:
-
同类型的对象共用同一张虚表 。不管你实例化了多少个
Base对象,它们内部的_vfptr都指向内存中唯一的那一张Base类的虚表。 -
不同类型的对象各自有独立的虚表。基类和派生类拥有各自独立的虚表。即使派生类没有重写任何虚函数,编译器也会为派生类单独生成一张属于它自己的虚表(只是表里的内容暂时和基类一样罢了)。