当前位置: 代码迷 >> 综合 >> C++多态、虚函数表、动态链接,虚函数指针,RTTI
  详细解决方案

C++多态、虚函数表、动态链接,虚函数指针,RTTI

热度:6   发布时间:2024-01-04 02:13:39.0

多态

多态的概念:基类指针指向派生类的地址,通过实现派生类的重写函数实现同一个函数接口不同的功能。多态性在Object Pascal和C++中都是通过虚函数(Virtual Function)实现的,因为通过父类的指针去调用不同的子类指针对父类虚函数的重写方法实现多态,同样的接口函数可以实现不同的功能。

虚函数通过动态绑定实现、动态绑定通过vfptr(虚函数指针)和vftable(虚函数表)来实现的

联编/绑定:程序调用函数,编译器决定调用哪个函数地址。

 

静态绑定:函数重载和运算符重载,它是在编译过程汇总进行的联编,又称早期联编。静态联编在汇编的底层是通过绑定绝对地址来实现的,call指令后面是具体的函数地址。这个地址是线性地址,也就是说是一个虚拟地址。而在进行函数的地址的访问的时候是需要通过地址映射完成线性地址到物理地址转换的过程。

 

动态绑定:是在程序运行过程中才动态的确定调用函数的地址,在汇编的底层来看就是call指令后面是一个eax寄存器的值,其具体的地址只有在程序运行的时候才确定eax寄存器上的具体值。在函数调用时,如果被调用的函数是虚函数,那么发生的就是动态绑定。


虚函数:

虚函数就是在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数。

用法格式为: virtual 函数返回类型函数名(参数表) {函数体};

如果在基类中定义了虚函数,那么派生类中的同名函数将自动变为虚函数,但是我们可以在派生类同名函数前也加上virtual关键字,这样会增加程序的可读性。

注意:

        在析构函数中如果基类的析构函数声明了是虚函数,那么派生类的析构函数也是虚函数,虽然他们的名字不同!在有时候必须将基类的析构函数声明为虚函数。

注意:

        这里一定要注意什么时候会调用用虚函数,必须是用指针或引用调用虚函数时,因为如果是通过对象调用虚函数,那么编译的时候就知道应该用哪个方法了,这是静态绑定,不是动态绑定。

多态的本质:不是重载声明而是覆盖。 虚函数调用方式:通过基类指针或引用,执行时会根据指针指向的对象的类,决定调用哪个函数。

 那么虚函数指针是在运行的什么时候初始化的?



·拥有虚函数的类会有一个虚表,而且这个虚表存放在类定义模块的只读数据段中。模块的数据段通常存放定义在该模块的全局数据和静态数据,这样我们可以把虚表看作是模块的全局数据或者静态数据

·类的虚表会被这个类的所有对象所共享。类的对象可以有很多,但是他们各自的虚表指针都指向同一个虚表,从这个意义上说,我们可以把虚表简单理解为类的静态数据成员。值得注意的是,虽然虚表是共享的,但是虚表指针并不是,类的每一个对象有一个属于它自己的虚表指针,该指针的优先级比较高,总是在对象的前四个字节。

·虚表中存放的是虚函数的地址。其中在虚表的前四个字节存的是RTTI指针,指向的是RTTI信息,也就是一个运行时类型识别功能就是通过RTTI指针来是实现的。接下来的四个字节是一个偏移量,表示虚函数指针在对象中的偏移量,最后才是虚函数的地址!

类的非静态成员函数调用时,编译器会传入一个"隐藏"的参数。这个参数就是通常我们说的"this"指针,它的值就是对象的地址。在代码中,寄存器 EAX 保存的就是这个。

由此可见,虚表的地址被存放在对象的起始位置,即对象的第一个数据成员也就是我们说的虚表指针。同时我们还可以注意到,虚表指针的初始化确实发生在构造函数的调用过程中,但是在执行构造函数体之前,即进入到构造函数的"{"和"}"之前。为了更好的理解这一问题,我们可以把构造函数的调用过程细分为两个阶段,即:

1.      进入到构造函数体之前。在这个阶段如果存在虚函数的话,虚表指针被初始化。如果存在构造函数的初始化列表的话,初始化列表也会被执行。

2.      进入到构造函数体内。这一阶段是我们通常意义上说的构造函数。

总结:虚函数表在编译的时候就确定了,而类对象的虚函数指针vptr是在运行阶段赋值根据虚函数表加载到内存中的地址进行赋值的。先调用基类构造函数把基类的虚函数表地址赋值到虚函数指针上,但又在自身构造函数或初始化列表之前,再次让虚函数指针指向派生类类型的虚函数表,对继承过来的基类的虚函数表进行函数的覆盖,这是实现多态的关键!

静态(编译时期)绑定和动态(运行时期)绑定问题

对象的内存会改变:vfptr虚函数指针

vfptr =>>>> vftable:指向虚函数表的地址

vftable虚函数表是什么时候产生的?运行时它存放在哪个内存区域?

编译阶段产生的,运行时加载到内存中.data段中的.rodata段只读数据段

vftable里面放的是什么东西?

虚函数的入口地址,最后四个字节是NULL表示虚函数表的结束

构造函数能实现成虚函数吗? static成员方法呢?友元方法呢?

构造函数不可以实现为虚函数,析构函数可以,静态方法和友元方法都不行。


构造函数和静态成员函数不能是虚函数,还没有构造成对象,因此没有对象地址。

静态函数通过类的作用域就可以调用,不需要生成对象,调用静态函数是静态绑定的过程和虚函数的实现机制是相反的。

析构函数可以是虚函数:有时候必须为虚析构函数。

因为在一个类里声明友元时,由于友元不是自己的成员函数自然在自己的类里不能把它声明为虚函数。

注意:友元是另外一个类的成员函数(在那个类里它可以定义为虚函数)而这个类将它声明为自己的友元,只是让它可以存取自己的私有变量

构造函数为什么不能是虚函数?:

1,从内存空间角度

虚函数对应一个vptr,可是这个vptr其实是存储在对象的内存空间的。

运行时指向只读数据段的vftable。问题出来了,如果构造函数是虚的,就需要通过 vptr来调用,可是对象还没有实例化,也就是内存空间还没有,此时就不知道vptr具体的地址。所以构造函数不能是虚函数,不能通过vptr指针去调用虚的构造函数。        

2,从使用角度

    虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化对象,那使用虚函数也没有实际意义呀。

虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数

3、从实现上看,vfptr在构造函数调用后才建立,因而构造函数不可能成为虚函数。 

  从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数。

vftable虚函数表的地址是什么时候写到vfptr虚函数指针当中的?

vfptr是在构造函数的栈帧进行初始化的时候:在构造函数初始化列表之后并调用构造函数第一行代码之前,函数栈帧开辟后进行赋值虚表地址赋值给vfptr的,This指针的赋值也是在构造函数的栈帧进行。

析构函数能不能实现为虚函数?什么时候必须实现为虚函数?

可以实现虚函数,并且在基类指针指向堆空间开辟的派生类对象的时候必须将基类的析构函数实现为虚构造函数,因为后面需要程序员自己delete该基类指针。

如果基类的析构函数不是虚函数的话,Delete基类指针时对基类析构函数的调用只是静态绑定,只会调用基类的析构函数,派生类的析构函数无法调用,造成派生类的资源无法释放。

如果基类中析构函数是虚函数的话,派生类的析构函数自动成为虚函数。基类就有一张虚函数表,派生类继承基类的时候会把自己的析构函数覆盖到虚函数表中,delete基类指针的时候就发生动态绑定,调用的就是该派生类析构函数而该派生类析构函数会先释放派生类对象再释放基类对象。这样的话就不会造成派生类的资源没有释放的问题。

但是基类指针指向栈上的派生类地址就不会有问题,因为栈上的派生类是自动释放的,自动调用派生类析构函数的,也自然会调用基类的析构函数。

 

运行时这个类型是怎么识别的?类型信息存在哪里?

RTTI机制  run-time type infomation/identify

运行时的类型识别 RTTI机制是什么?

RTTI也是通过vfptr和vftable来实现的,vftable里面存在RTTI指针 => RTTI信息的字符串的地址。

正常的覆盖是基类的方法是虚函数,派生类自动处理成虚函数;请问如果

派生类的函数是虚函数而基类是普通函数,通过如下调用会发生什么问题?

内存释放的时候地址不对也就是说开辟的和释放的地址相差四个字节。

堆上开辟:

基类指针指向派生类的地址,该返回值指针永远指向基类的起始地址,如果基类没有虚函数指针会出错.

这个时候必须把基类的析构函数写成虚函数,就可以解决问题

因为这个时候派生类的虚函数指针就是从基类继承过来的

         G++就不会有问题,进行过优化。

         VS就出问题。

栈上开辟就没有问题:派生类对象继承基类的时候,如果基类有虚函数,那么派生类直接使用基类的虚函数指针,派生类的虚函数指针和基类的进行合并。

构造函数中调用虚函数是静态绑定还是动态绑定?析构函数中呢?

构造函数之后才会产生对象,有对象产生之后的前四个字节才能找到vfptr才能有vftable,vftable才有虚函数的地址。

在析构函数中调用虚函数也是静态绑定,析构函数调用开始对象已经不存在了,逻辑意义。

都是静态绑定,在编译的时候就确定了函数的地址。

也可以从对象的构造过程:先构造基类部分======》再派生类内存部分

如果在派生类的构造函数里调用虚函数的话,因为派生类的对象还没有构造完全,如果是动态绑定的话,派生类的对象还没有构造,自然也不知道派生类对象的函数的地址,这个时候如果去调用派生类对象的重写函数的话会出错,所以编译器的选择是静态绑定。

析构函数里面调用虚函数也是一样,先析构派生类的对象==========》》》在析构基类部分

如果在派生类的析构函数里面调用虚函数,而派生类已经是析构的状态,如果调用派生类的重写函数的话也是未定义的过程。所以编译器看来只能去调用已经存在的基类的部分的函数,所以是静态绑定

什么情况下才会发生多态(动态绑定)

用基类指针指向派生类对象的地址去调用基类中的虚函数的时候。

在用栈上的对象去调用虚方法时不会发生多态

什么是纯虚函数和抽象类?

抽象类和普通类的区别是抽象类里面有纯虚函数并且抽象类往往只是一个抽象。不能实例化的,不能产生对象,但可以定义指针和引用。

常常作为一个统一的接口,实现不同的功能。

Inline 可以是虚函数,因为Inline只是一种编译请求编译器不一定会允许。在有virtual关键字的函数,inline的作用失效。