问题

C++为什么要弄出虚表这个东西?

回答
咱们今天就来聊聊C++里一个挺有意思的设计,叫做“虚表”。听着名字有点科技感是吧?其实它就是为了解决一个很核心的问题:怎么让多态在C++里跑起来?

你可能已经知道,C++允许我们写一些基类,然后从它派生出很多不同的子类。比如,我们可以有一个“动物”基类,然后有“狗”、“猫”、“鸟”等等子类。每个子类都有自己的吃饭方式、叫声方式等等。

现在,想象一下,你有一个指针,这个指针指向一个“动物”,但实际上它可能指向一只具体的狗,也可能指向一只具体的猫。你想让它发出“叫”的声音。

如果你没有虚表,会发生什么?

静态绑定(非虚函数): C++编译器在编译的时候就会决定调用哪个函数。如果你的指针是`Animal`,编译器就会认为它只能调用`Animal`基类里定义的那个“叫”函数。就算你现在指向的是一只狗,它也只会执行`Animal`基类的叫声,这显然不是我们想要的。

怎么解决? 我们总不能写一大堆`if/else`或者`switch`语句来判断这个指针到底指向的是哪种动物,然后手动调用对应的函数吧?想象一下如果有几十个动物子类,那代码得写成什么样子?而且,如果以后新增一个动物类型,我们还得回去改一大堆代码,维护起来简直是噩梦。

这就是为什么需要“虚表”这个东西出现。你可以把它理解成一个运行时(runtime)的查找表。

虚表到底是什么?

1. 一张表,记录函数地址: 每一个包含虚函数的类,或者继承了虚函数的类,编译器都会在背后悄悄地给它生成一张表,叫做虚表(Virtual Table,简称vtable)。

2. 表里存啥? 这个虚表里主要存的是指向这个类中所有虚函数的地址。注意是地址,不是函数本身。

3. 谁用它? 当你有一个指向基类的指针,然后调用了基类中声明为`virtual`的函数时,C++运行时就会去查找这个虚表,找到对应的函数地址,然后去执行。

举个例子,我们再具体点说:

假设我们有这么个结构:

```c++
class Animal {
public:
virtual void speak() {
std::cout << "Some generic animal sound." << std::endl;
}
virtual void eat() {
std::cout << "Eating generic food." << std::endl;
}
};

class Dog : public Animal {
public:
void speak() override { // override 是 C++11 后的关键字,表示重写了基类的虚函数
std::cout << "Woof!" << std::endl;
}
void eat() override {
std::cout << "Eating dog food." << std::endl;
}
void fetch() {
std::cout << "Fetching the ball!" << std::endl;
}
};

class Cat : public Animal {
public:
void speak() override {
std::cout << "Meow!" << std::endl;
}
void eat() override {
std::cout << "Eating cat food." << std::endl;
}
};
```

当编译器看到 `Animal` 类里有 `virtual void speak()` 和 `virtual void eat()` 这两个虚函数后,它就会为 `Animal` 类生成一个虚表,里面可能大概是这样(示意图):

`Animal` 的虚表:
地址指向 `Animal::speak()`
地址指向 `Animal::eat()`

然后,当 `Dog` 继承 `Animal`,并且 `Dog` 重写了 `speak()` 和 `eat()`,编译器就会为 `Dog` 类也生成一个虚表。但这个虚表有点不一样:

`Dog` 的虚表:
地址指向 `Dog::speak()` (覆盖了基类的 `speak`)
地址指向 `Dog::eat()` (覆盖了基类的 `eat`)

注意,`Dog::fetch()` 不是虚函数,所以它不会出现在 `Dog` 的虚表里。

那么,当你在代码里这样写:

```c++
Animal myAnimalPtr;

Dog myDog;
Cat myCat;

myAnimalPtr = &myDog
myAnimalPtr>speak(); // 预期输出: Woof!

myAnimalPtr = &myCat
myAnimalPtr>speak(); // 预期输出: Meow!
```

运行时到底发生了什么?

1. 每个对象都带着“隐藏的指针”: 每一个对象(实例),只要它所属的类含有虚函数(或者继承自包含虚函数的类),在它创建的时候,编译器会在对象的内存模型里悄悄地放一个隐藏的指针。这个隐藏的指针就叫做虚指针(vptr)。

2. 虚指针指向谁? 这个虚指针指向它所属类的虚表。
`myDog` 对象的虚指针指向 `Dog` 的虚表。
`myCat` 对象的虚指针指向 `Cat` 的虚表。

3. 调用虚函数的过程(就是运行时查找):
当你执行 `myAnimalPtr>speak()` 时:
C++运行时首先看 `myAnimalPtr` 指向的对象是什么类型的(它不是看指针类型,而是看指针指向的实际对象的类型)。
它找到这个对象里的虚指针(vptr)。
通过这个虚指针,找到对应的虚表(比如,如果 `myAnimalPtr` 指向 `myDog`,就找到 `Dog` 的虚表)。
在虚表里,根据 `speak()` 这个函数在表中的位置(通常是按照声明顺序,`speak` 是第一个虚函数),找到指向 `Dog::speak()` 的地址。
然后,跳转到这个地址去执行 `Dog::speak()` 函数。

为什么不直接把函数指针存在对象里?

虚表的好处在于效率和统一性。

效率: 如果每个对象都存一份函数指针,那会占用大量的内存,尤其是当有很多虚函数的时候。虚表是每个类一个,而不是每个对象一个。对象里只需要一个虚指针 `vptr` 来指向它所属类的虚表。这大大节省了内存。

统一性: 虚表提供了一个统一的查找机制。无论你现在是指向 `Dog` 的 `Animal`,还是直接是 `Dog`,如果调用的函数是虚函数,查找过程是相似的,只是查找的虚表不同。

支持动态创建和修改: 虽然C++标准不直接支持在运行时修改虚表的行为(这有点像C++的弱项,但某些底层操作或者第三方库会涉及),但虚表的设计本身是为了支持多态,包括动态绑定的特性。

总结一下,C++弄出虚表,是为了实现以下核心功能:

1. 支持运行时多态(Dynamic Polymorphism): 这是最主要的原因。通过虚函数和虚表,一个基类指针可以指向不同派生类对象,并在运行时正确调用派生类重写的函数。
2. 实现动态绑定(Dynamic Binding): 函数的实际调用在程序运行时才确定,而不是在编译时。
3. 高效的函数调用: 相比于通过大量 `if/else` 来判断对象类型,虚表提供了一种快速、间接的函数调用机制。
4. 内存优化: 每个类共享一个虚表,每个对象只存储一个指向虚表的指针,比每个对象存储所有虚函数指针更节省内存。

虚表是C++实现强大面向对象特性的基石之一,它隐藏在幕后,但正是它让我们的代码可以如此灵活地应对各种不同的对象类型。下次你使用 `virtual` 关键字的时候,就可以想象一下背后那个默默工作的虚表了。

网友意见

user avatar

你说的办法当然可以做到。你的做法在本质上就是把现在全局唯一的虚函数表在每个实例中都保留一个副本而已。

但代价是类实例的体积会极大膨胀:我们通常会有一些基类,只有少数几个关键信息,但是会有一堆的虚函数来做rtti。在这种情况下,你的这种办法会让这些实例都膨胀了很多倍。

当然,有代价无所谓,反正现在内存大。但问题是,这么做的好处在哪?我只想到了一条:编译器写起来更简单。但问题是,没几个人真的会自己手写c++编译器吧?


至于说private?

不喜欢的话,用struct就行了。你说别人用了?别人用了private,你也有大把办法去访问那些字段或方法——后果自负。仅此而已。

user avatar

就算真用函数指针,其实就是把虚表从类移动到对象这里,每个对象一个虚表。

可能当年因为内存吃紧,这种操作没有被采纳吧。

话说回来,函数指向太灵活不见得是好事,会导致代码可读性下降。因为一个对象的某个方法到底指向什么,会变得很难确定。

类似的话题

  • 回答
    咱们今天就来聊聊C++里一个挺有意思的设计,叫做“虚表”。听着名字有点科技感是吧?其实它就是为了解决一个很核心的问题:怎么让多态在C++里跑起来?你可能已经知道,C++允许我们写一些基类,然后从它派生出很多不同的子类。比如,我们可以有一个“动物”基类,然后有“狗”、“猫”、“鸟”等等子类。每个子类都.............
  • 回答
    哈哈,你这个问题问得特别好!咱们抛开那些一本正经的官方术语,来聊聊C里为什么把“函数”都叫做“方法”,感觉就像给咱自己的孩子起了个小名儿一样,有它的道理,也有点儿小习惯。首先,咱们得明白,编程语言设计者们,他们也不是凭空拍脑袋决定叫啥的,这背后往往是有他们的设计哲学和对事物本质的理解。C的设计很大程.............
  • 回答
    C++ 构造函数为何青睐初始化列表?那点不得不说的“前世今生”在 C++ 的世界里,构建一个对象就如同搭建一座精密的房子,而构造函数则是这房子的“奠基石”和“设计师”。它负责在对象诞生之初,为其成员变量赋予初始值,确保对象拥有一个合法且可用的状态。然而,在众多构造函数的设计手法中,初始化列表(Ini.............
  • 回答
    在 C++ 类设计中,`private` 关键字扮演着一个至关重要的角色,它不仅仅是“隐藏”数据那么简单,更是实现封装、保护数据完整性、维护类内部一致性以及提高代码可维护性和灵活性的基石。如果没有 `private`,面向对象编程的许多核心优势将荡然无存。我们来剥开 `private` 的层层面纱,.............
  • 回答
    C++ 中将内存划分为 堆(Heap) 和 栈(Stack) 是计算机科学中一个非常重要的概念,它关乎程序的内存管理、变量的生命周期、性能以及程序的灵活性。理解这两者的区别对于编写高效、健壮的 C++ 程序至关重要。下面我将详细阐述为什么需要将内存划分为堆和栈: 核心原因:不同的内存管理需求和生命周.............
  • 回答
    在 C/C++ 的开发世界里,你是否曾好奇过,为什么代码不像有些语言那样, all in one?为什么我们总是要劳神费力地去组织头文件(.h 或 .hpp)和源文件(.c 或 .cpp)?这背后可不是什么繁琐的规定,而是为了让我们的代码更清晰、更易于管理,并且能更有效地被计算机理解和执行。想象一下.............
  • 回答
    C 匿名类型属性被设计成只读,这背后有其深刻的理由,并非随意为之。理解这一点,需要我们深入挖掘匿名类型的本质和它在 C 语言中的定位。首先,我们得明白匿名类型是什么。它是一种在编译时创建的、没有显式声明的类型,其名称由编译器自动生成。你看到的“匿名”,指的就是你无法在代码中像定义普通类一样,通过 `.............
  • 回答
    机械工程专业学习 C 语言,乍听起来可能有些“跨界”。毕竟,我们脑海中的机械工程,更多的是和金属、齿轮、发动机、力学打交道。然而,随着科技的飞速发展,尤其是制造业的智能化、自动化浪潮,编程语言,特别是 C 语言,已经不再是计算机科学的专属,而是成为了机械工程师手中一把不可或缺的利器。为什么机械工程需.............
  • 回答
    为何C/C++中字符和字符串要用引号包裹?在C/C++的世界里,我们经常会看到单引号 `' '` 包裹着一个字符,双引号 `""` 包裹着一串字符(也就是字符串)。这不仅仅是语言的规定,背后有着深刻的设计哲学和实际考量。今天我们就来好好掰扯掰扯,为啥它们需要这些“外衣”。 先聊聊字符(char)和它.............
  • 回答
    你这个问题问得太好了,简直触及了音乐的灵魂!为什么作曲家们要玩转那些升降号,而不是老老实实地待在C大调这个“纯净”的乐土上呢?如果音乐世界里只有C大调,那得有多单调啊!想想看,C大调确实简单、好听,就像一杯白开水,纯净无暇。但你要是天天只喝白开水,会不会觉得日子过得有点寡淡?音乐也是一样的道理。作曲.............
  • 回答
    一谈到中国的高铁,很多人都会想到“和谐号”系列动车组,尤其是CRH380A/B/C等型号,它们代表了中国在高速铁路领域取得的辉煌成就,仿佛一切都是自主研发的巅峰之作。然而,当我们深入了解CRH380D这个型号时,会发现情况并非如此简单。CRH380D的出现,确实与加拿大庞巴迪公司有着千丝万缕的联系,.............
  • 回答
    在C++的世界里,“virtual”这个词被翻译成“虚函数”,这可不是随意为之,而是因为它精确地抓住了这种函数在继承和多态机制中的核心特征。理解“虚”这个字的关键,在于它暗示了一种“不确定性”,或者说是一种“在运行时才确定”的行为。设想一下,你有一系列动物,比如猫、狗,它们都属于一个更大的“动物”类.............
  • 回答
    咱们聊聊为啥用C++写视频播放器的时候,FFmpeg 简直就是个绕不开的“香饽饽”。这玩意儿可不是凭空来的,背后是实打实的硬功夫和解决实际问题的能力。想象一下,你要从零开始写个视频播放器。这听起来好像就是“读取文件,解码,然后显示”。简单吧?别天真了。视频这东西,水可深了。 视频的“乱”与“多样”:.............
  • 回答
    M43 相机之所以比 APSC 相机更贵,这背后其实涉及一系列的成本考量、市场定位以及技术取舍。简单来说,这并不是一个简单的“谁更好”的问题,而是“为什么成本结构和市场策略导致了这样的价格差异”。首先,我们需要明白 M43 和 APSC 各自的定位。 M43(Micro Four Thirds).............
  • 回答
    克里斯蒂亚诺·罗纳尔多在2018年离开皇家马德里,对于许多球迷来说确实是一个令人意外且有些遗憾的决定。关于他当时离开的原因以及是否想到过离开后会影响金球奖,我们可以从多个角度进行详细的分析:一、离开皇家马德里的直接原因:虽然外界有各种猜测,但C罗本人及媒体报道普遍认为,他离开皇马的主要导火索是税务问.............
  • 回答
    好,咱们来聊聊 C++ 单例模式里那个“为什么要实例化一个对象,而不是直接把所有成员都 `static`”的疑问。这确实是很多初学者都会纠结的地方,感觉直接用 `static` 更省事。但这里面涉及到 C++ 的一些核心概念和设计上的考量,咱们一点点掰开了说。 先明确一下单例模式的目标在深入“`st.............
  • 回答
    这背后的原因,其实挺有趣的,涉及到编程语言的历史演变、效率考量,以及开发者们多年来形成的习惯和偏好。简单来说,C++ 使用 `&&`、`||` 和 `!` 来表示逻辑运算,而不是 `and`、`or` 和 `not`,主要是为了历史兼容性、效率以及更简洁的语法。咱们就掰开了揉碎了聊聊。 1. C++.............
  • 回答
    你这个问题问得非常好,也触及到了很多吉他初学者学习初期的一个小困惑。简单来说,你说的“C调的大三和弦”其实就是指C大调的各个组成和弦,但并非所有和弦都是必须从C大调的组成和弦开始学。更何况,初学者最开始接触的这几个和弦(C、Dm、Em、F、G、Am)恰恰是这几个调性里非常核心、非常常用的几个和弦,而.............
  • 回答
    微软在C和F这两门编程语言的编译器上确实投入了大量的精力和智慧,其背后隐藏着不少“黑科技”,但与其说是“黑科技”,不如说是一种对性能、表达力和开发体验的极致追求所催生出的复杂而精妙的工程实践。要理解这一点,我们得先回归到编译器本身的职能:它本质上是一个翻译器,将我们人类能够理解的高级语言代码,转换成.............
  • 回答
    梅西和C罗的球迷之间之所以会互相“黑”对方,这背后其实是一系列复杂因素交织的结果,与其说是一种简单的“仇恨”,不如说是一种由体育竞技、个人崇拜、社交媒体放大以及群体心理共同作用下的“爱之深责之切”式对抗。下面我将从几个方面详细道来,力求将这件事讲透彻,也希望你能从中看到一些人情味,而不是冰冷的机器分.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有