C++虚函数表

一、虚函数引入后类会产生什么变化

在C++中引入虚函数(virtual functions)后,类(Class)和它的对象(Objects)会发生以下主要变化:

1. 增加虚函数表(vtable)指针

每一个包含虚函数的类的对象都会额外包含一个指向“虚函数表”(Virtual Table,或简称vtable)的指针。这个表包含了该类中所有虚函数的地址。

2. 对象大小的增加

由于每个对象需要存储一个额外的虚函数表指针,因此对象的大小会增加。通常,这个增加的大小等于一个指针的大小。

3. 构造函数的修改

类的构造函数会被编译器自动修改以初始化这个虚函数表指针。通常,这是在构造函数的开始处完成的,以确保对象在使用前是正确配置的。

4. 支持动态多态

引入虚函数意味着这个类支持“动态多态性”(Dynamic Polymorphism)。也就是说,你可以使用指向基类的指针或引用来调用实际指向的派生类对象的虚函数,这个决定是在运行时(Run-time)做出的。

5. 性能开销

虚函数增加了一些运行时的性能开销,因为需要通过虚函数表来间接调用函数。然而,这通常被认为是可以接受的,特别是考虑到它带来的灵活性和扩展性。

6. 禁止部分编译器优化

由于虚函数的动态性质,编译器在某些情况下可能无法进行某些优化,比如内联函数(inline functions)。

引入虚函数后,这些是类和对象主要会面临的变化。总的来说,这些变化主要是为了支持动态多态性,即使这意味着会有一些额外的内存和性能开销。

二、虚函数表的生成时机和生成原因

1、生成时机

虚函数表(Virtual Function Table,简称为 vtable)通常在编译阶段生成。当编译器遇到一个包含虚函数的类定义时,它会为该类创建一个虚函数表。

在多态继承结构中,每一个含有虚函数(直接或从父类继承而来)的类都会有自己的虚函数表。如果派生类覆盖了基类的某个虚函数,该派生类的虚函数表中对应的条目会被更新为指向派生类的函数实现。

2、生成原因

  1. 支持动态多态性: 虚函数表的主要目的是支持C++的动态多态特性。通过虚函数表,程序在运行时可以确定应该调用哪个类的哪个函数。

  2. 代码复用和可维护性: 使用虚函数,可以编写出更加通用的代码,这些代码对基类和派生类都适用。这样,你可以很容易地添加新的派生类而不需要修改现有的代码。

  3. 解耦合: 虚函数允许基类和派生类解耦,这意味着基类代码可以在不知道具体派生类情况的前提下进行编写和运行。

  4. 实现抽象接口: 虚函数也允许类声明自己的接口(抽象的或具体的),这样其他类就可以实现或者继承这个接口。

  5. 方便扩展: 使用虚函数,你可以更容易地添加或修改派生类的行为,而无需修改基类或其他派生类的代码。

  6. 动态分发: 虚函数表提供了一种机制,使得程序在运行时根据对象的实际类型来调用相应的函数,这称为动态分发。

三、虚函数表指针被赋值的时机

1、对象构造时

  • 基类构造函数执行阶段:当一个对象被创建(无论是静态创建还是动态创建)时,其构造函数会被调用。在基类的构造函数中,虚函数表指针(vptr)会被设置为指向基类的虚函数表(vtable)。

  • 派生类构造函数执行阶段:在派生类的构造函数被调用时,虚函数表指针会被重新设置,以指向派生类自己的虚函数表。这通常在派生类构造函数体执行前完成,因此确保了即使在派生类构造函数体内,虚函数也能正确地解析到派生类的版本。

2、多态赋值或转型操作

虽然这并不严格算作虚函数表指针的“赋值”,但在多态赋值或转型(如使用dynamic_cast)的操作中,相应的虚函数表指针会被用来确定对象的实际类型。

3、注意点

  • 在对象的整个生命周期中,其虚函数表指针通常不会再改变,除非进行了类型转换或其他类似操作。
  • 析构函数中也有相似的机制。当派生类的析构函数开始执行时,vptr 指向派生类的虚函数表;当派生类析构完成,控制权传递给基类的析构函数,并且vptr 会被重置为指向基类的虚函数表,以确保正确的析构过程。

总的来说,虚函数表指针主要在对象构造(和析构)的过程中被赋值,以确保动态多态的正确实现。

四、类对象在内存中的布局

在C++中,类对象在内存中的布局会受到多种因素的影响,包括数据成员的布局、虚函数表指针、多重继承和虚继承等。以下是一些主要的考虑因素:

1、非虚函数情况下的布局

  • 数据成员:在最简单的情况下,没有虚函数或继承的类对象在内存中的布局就是其数据成员的连续存储。数据成员按照它们在类定义中出现的顺序进行布局。

2、包含虚函数时的布局

  • 虚函数表指针:一旦一个类有虚函数,该类的对象就会增加一个额外的“虚函数表指针”(通常简称为vptr)。
  • 成员与vptr的排列:这个指针通常存储在对象的开始位置,但这一点并没有在标准中明确规定。除了这个额外的指针,其余的布局与没有虚函数时大致相同。

3、多重继承情况下的布局

  • 多个基类:在多重继承的情况下,一个对象可能会包含多个基类的数据成员。
  • 多个虚函数表指针:如果有多个基类含有虚函数,那么对象会有多个虚函数表指针。

4、虚继承(虚基类)情况下的布局

  • 虚基类指针:在虚继承中,派生类对象会包含一个指向虚基类的指针。
  • 单一虚基类实例:所有从虚基类派生的对象都会共享一个虚基类实例。

五、虚函数的多态性的体现

  1. 动态多态性: 虚函数是C++实现动态多态性(Run-time Polymorphism)的机制。你可以使用基类的指针或引用来操作派生类对象,并且可以在运行时动态地决定应该调用哪个类的哪个函数。

  2. 代码复用: 基于虚函数,你可以编写更加通用和可复用的代码。比如,你可以定义一个接收基类指针或引用的函数,并在其中调用虚函数,而这个函数能够正确地处理任何派生类对象。

  3. 扩展性: 多态性使得代码更容易扩展。你可以添加新的派生类而不必修改现有的基类或使用基类的代码。

  4. 接口抽象: 通过虚函数,可以定义接口(可能是抽象的),使得多个不同的派生类可以以统一的方式被操作。

  5. 解耦合: 虚函数和多态性有助于解耦合代码。你可以编写只依赖于基类接口的代码,而不需要知道具体的派生类。

总的来说,虚函数的工作原理主要是通过虚函数表和虚函数表指针来实现的,而多态性是通过动态分发和这种间接调用机制来体现的。这增加了一定的运行时开销,但获得了更高的程序设计灵活性。

 六、虚函数表占据内存空间的哪个位置

虚函数表(Virtual Table,通常简称为 vtable)本身一般存储在程序的数据段(Data Segment),而不是对象实例内。但每一个含有虚函数的对象实例会在内存中持有一个指向虚函数表的指针,这个指针通常称为虚函数表指针(Virtual Table Pointer,通常简称为 vptr)。

在C++程序中,虚函数表(vtable)通常存储在数据段(Data Segment)主要出于以下几个原因:

  1. 常量性: 虚函数表是在编译时生成的,并且在程序运行期间不会被修改。数据段是用于存储初始化的全局变量和静态变量的地方,包括那些在程序执行期间不会改变的数据。

  2. 共享: 在多个对象实例之间,虚函数表是可以共享的。如果每个对象都有自己的虚函数表,那么会产生很大的内存开销。将虚函数表存储在数据段允许多个对象共享同一个虚函数表,从而节省内存。

  3. 生命周期: 数据段的生命周期与程序的生命周期相同,这意味着虚函数表会在程序启动时被加载,程序结束时被卸载。这符合虚函数表作为编译时生成的、与特定类相关的元信息的性质。

  4. 安全性和可维护性: 将虚函数表存储在数据段意味着它们通常是只读的,这增加了程序的安全性。因为虚函数表通常不应被修改,将它们标记为只读有助于防止不正确或恶意的代码操作。

  5. 效率: 由于所有对象共享同一个虚函数表,这样在进行虚函数调用时,代码可以更加高效地进行查找和调用,因为它总是知道虚函数表的确切位置。

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值