C++:虚函数补充

        在上次谈到动态多态时提及了以下基础内容: 

        C++的虚函数是实现动态多态性的一种机制。 在C++中,通过基类指针或引用调用一个成员函数时,通常会调用基的版本,而不是指针实际指向类型的版本。 但如果我们将这个成员函数声明为虚函数(virtual),那么就可以根据实际类型来决定调用哪个版本。

        编译器在实现虚函数重写时主要完成了以下几个步骤:

  1. 虚函数表(vtable)的创建:编译器会为每一个含有虚函数的类创建一个虚函数表。这个表是一个函数指针数组,其中包含了这个类的所有虚函数的地址。

  2. 虚函数表指针(vptr)的添加:编译器会在每一个类对象中添加一个虚函数表指针,这个指针指向该对象所属类的虚函数表。当这个对象调用虚函数时,编译器会通过这个指针找到虚函数表,然后在虚函数表中查找对应的函数地址。

  3. 虚函数的调用代码生成:当编译器遇到虚函数的调用时,它会生成一段代码,这段代码会通过对象的虚函数表指针找到虚函数表,然后在虚函数表中查找对应的函数地址,最后通过这个地址调用函数。

  4. 虚函数表的更新:当一个类重写了基类的虚函数时,编译器会在这个类的虚函数表中更新对应的函数地址。这样,当这个类的对象调用这个虚函数时,就会调用到这个类自己的版本,而不是基类的版本。

        现在我们来讨论一些额外的细节和补充和相关部分:

一:虚函数表在什么时候生成?创建在什么地方?编译器如何找到具体的一个虚函数?

        虚函数表在编译时期由编译器生成(对于每一个含有虚函数的类来说),编译器会将类的所有虚函数信息存入到虚函数表中(虚函数实现的地址,如果没有发生重写,指向父类的实现;如果发生了重写,指向自己的实现),每个虚函数按照声明的顺序有一个偏移量,编译器会根据这个偏移量信息来在虚函数表查找一个具体的虚函数。

        补充说明:如果派生类没有重写任何虚函数,编译器也会为其生成一个。(1)

                        如果派生类没有声明和重写任何虚函数,它的虚表就是基类的虚表。(2)

        注:不重要的问题,另外在VS2022编译器下 结果是(1)。

        虚函数表是属于类范围的,即所有的类对象共享一个虚表,因此虚表应该在全局区域,并且虚表一旦在编译期生成后,其在运行时期是不会发生改变的(也不希望被人为在运行时期改变),因此推断虚函数表存储在只读数据段中。

二:有哪些函数不能是虚函数?

  • 构造函数(Constructor):构造函数不能被声明为虚函数。因为在构造对象时,对象的虚指针还没有完全建立起来。
  • 内联函数(Inline functions):由于虚函数需要通过vtable进行动态绑定,而内联函数则需要在编译阶段就确定具体调用哪个版本。因此这两者是互斥的。

        补充说明:并不是说内联成员函数不能设置为Virtual,只是这两者是矛盾的,不能同时发生。再进一步说明:当我们以虚函数的方式进行调用时(指针/引用+调用虚函数),内联不会起作用;当我们不以虚函数的方式调用时,内联可能会生效(最终还是由编译器决定是否内联)。

  • 静态成员函数(Static member functions):静态成员不属于任何实例对象,它们没有this指针。但是虚机制需要通过实例对象的vptr来访问vtable。因此静态成员不能是虚拟的。

        补充说明:可能不是很准确,静态成员函数的内部确实没有this指针,但是调用函数不需要函数内部有this指针。归根到底的原因可能是因为规定如此,以下是考虑一些概念上的原因:

        静态成员函数和虚函数的运行机制是冲突的。静态成员函数是在编译时就已经确定的,而虚函数的调用是在运行时动态决定的。虚函数需要通过对象的虚函数表(vtable)来查找和调用,而静态成员函数并没有与之对应的对象实例,也就没有虚函数表。因此,静态成员函数不能声明为虚函数。这是由C++的语言设计决定的,以确保语言的一致性和正确性。

        静态成员函数并不依赖于任何对象,它们是类级别的,而不是对象级别的。因此,从语义上来说,静态成员函数没有必要也不应该是虚函数。

  • 友元函数 (Friend functions): 友元不是类的成员, 只是有权访问类的私有和保护成员, 所以它们不能被声明为虚拟。
  • 全局范围内定义的普通非成员方法: 这些方法也无法被定义为虚拟,因为他们并不属于任何类。

                

三:多重继承下的虚函数机制

        在C++中,一个类可以从多个基类继承,这就是所谓的多重继承。当一个类从多个基类继承时,如果这些基类有虚函数,那么派生类会有多个虚函数表(vtable),每个基类对应一个。

        同时,派生类中也会包含有多个虚指针,每个虚指针指向指向其对应的虚表。

四:派生类中Virtual和Override关键字

        派生类在实现基类的虚函数时,是否需要加 virtual 关键字,取决于编程习惯和代码的可读性需求。一旦基类中的函数被声明为 virtual,在派生类中重写该函数,无论是否显式添加 virtual 关键字,该函数都是虚函数。然而,为了代码的清晰和可读性,可以派生类中显式地标记出虚函数。

        override 关键字,是 C++11 新增的一个特性,用于显式地表明派生类中的函数是重写了基类的虚函数。如果一个函数被声明为 override,但基类中并没有对应的虚函数,那么编译器会报错。这可以帮助开发者找出一些潜在的错误,例如函数签名错误,或者误将一个普通函数标记为 override

五:对类对象使用memset会发生什么?

        (1)如果类包含有虚函数,则这个类的对象中会有一个隐藏的虚指针vptr指向这个类的虚表vtable,vptr是我们调用虚函数的基础。在memset类对象时,vptr的值会被覆盖,从而不再指向正确的虚表,从而可能(调用虚函数时)导致未定义的行为。

        (2)如果类包含有非平凡型成员(例如动态分配的内存,其他类对象,string等),使用memset可能会破坏这些成员的状态,造成内存泄漏。

六:RTTI(Run-Time Type Identification)

        RTTI主要包含了 类型信息(type_id) 和 动态转换(dynamic_cast)功能。

        如果开启了RTTI,对于含有虚函数的类来说,在其虚表中的首项会是该类型的type_id的指针信息,在使用dynamic_cast的时候,编译器会通过指针/引用类对象的vptr找到vtable,进而找到实际类型信息type_id,通过这个信息的比较来判断能否进行安全地转换。

        因此可以看出dynamic_cast的使用是有需求的:指针/引用类型,类型要有虚表(即含有虚函数)。

七:能否在构造函数和析构函数中调用虚函数?

        在C++中,你可以在构造函数和析构函数中调用虚函数,但结果可能并不是期望的。这是因为,在构造函数和析构函数被执行时,对象的动态类型(dynamic type)就是正在被构造或析构的类。换句话说,如果你在一个基类的构造函数或析构函数中调用一个虚函数,那么将会调用基类版本的那个虚函数,而不是派生类版本。

        更具体的说明:

        在调用构造函数的时候,会首先调用父类的构造函数(直到没有父类为之),在调用父类的构造函数的时候,对象的动态类型就是父类(因为子类的任何成员都没有被初始化,尤其是虚指针,虚指针在构造函数中进行的初始化(核心),此时虚指针指向父类的虚表),如果在父类的构造函数中调用了虚函数的话,则会调用父类的版本。

        在调用析构函数的时候,会先析构子类,然后调用父类的析构函数(知道没有父类为之),在调用父类的析构函数的时候,虚指针会退回到指向父类的虚表(核心),即此时对象的动态类型也转变为了父类,如果在父类的析构中调用了虚函数的话,则会调用父类的版本。

        后续可能还会有补充。

        

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值