当前位置: 首页 > 科技观察

C++编译器多态实现原理总结

时间:2023-03-12 13:39:32 科技观察

问题:定义一个空类型,里面没有任何成员变量和成员函数,对该类型进行sizeof操作,结果是?结果为1,因为空类型的实例不包含任何信息。sizeof计算的结果为0是合理的,但是在声明任何类型的实例时,都必须在内存中占用一定的空间,否则这些实例是无法使用的。至于占用多少内存,由编译器决定。继续追问:如果给这个类型加上构造函数和析构函数,结果是什么?还是1,因为我们在调用构造函数和析构函数时只需要知道函数的地址即可,而这些函数的地址只与类型有关,与类型的实例无关。编译器不会为这两个函数的实例添加任何附加信息。继续问:如果把析构函数改成虚函数怎么办?结果是什么?当c++编译器发现该类型中有虚函数时,就会为该类型生成一个虚函数表,并在该类型的每个实例中添加一个指向虚函数表的指针。在32位机器上,指针类型大小为4字节,结果为4。在64位机器上,指针大小为8字节,结果为8。object的实现效果面向多态性多态性:同一个调用语句有多种不同的表现形式。请参阅以下示例:{cout<<"fishbubble"<breathe();return0;}父类指针指向子类对象,breathe方法被调用,那么结果就是animalbreathe,也就是调用了父类的breath方法。这没有实现多态性。因为C++编译器在编译时必须确定每个对象调用的函数的地址,所以这叫做早期绑定(earlybinding)。当fish类的对象fh的地址赋值给父类的pAn指针时,C++编译器进行类型转换,认为父类的指针变量pAn存放的是动物对象的地址。在main函数中执行pAn->breath时,会调用动物对象的breathe函数。#p#进一步说:我们在构造fish类的对象时,首先要调用父类:animal类的构造函数来构造animal类的对象,然后再调用fish类的构造函数来完成构建自己的部分,从而拼接出一个完整的鱼对象。在将fish类的一个对象转为animal类型时,该对象被认为是原对象整个内存模型的上半部分,也就是图中的“animal对象占用的内存”。那么在使用类型转换后的对象指针调用其方法时,当然是调用其所在内存中的方法。因此,输出动物呼吸。这不是多态性的表现。多态的三个条件的必要前提是必须有继承关系,然后我们需要父类的指针(引用)来调用子类的对象,关键是:子类有对virtual的重写父类的功能。virtual关键字告诉编译器这个函数应该支持多态性。我们不应该根据指针类型来判断如何调用方法,而是根据指针指向的实际对象类型来判断如何调用。多态性的理论基础在前面的例子中,输出结果是因为编译器在编译时已经确定了对象调用的函数的地址。要解决这个问题,就必须使用后期绑定(latebinding)技术。当编译器使用后期绑定时,它将确定对象的类型并在运行时调用正确的函数。为了让编译器使用后期绑定,在基类中声明函数时需要使用virtual关键字。此类函数称为虚函数。一旦一个函数在基类中声明为虚函数,该函数在所有派生类中都是虚函数,而不需要显式声明它为虚函数。所谓动态绑定:根据实际对象类型判断重写函数的调用。C++中多态的实现原理当在类中声明了一个虚函数时,编译器会在类中生成一个虚函数表。虚函数表是一种存储类成员函数指针的数据结构。虚函数表由编译器自动生成。而维护,虚成员函数会被编译器放入虚函数表中。当存在虚函数时,每个对象都有一个指向虚函数表的指针(vptr指针)。如图所示,编译器为该对象提供了一个虚表指针vptr,它指向该对象所属类的虚函数表。程序运行时,根据对象的类型对vptr进行初始化,使vptr能够正确指向类的虚表,从而在调用虚函数时找到正确的函数。鱼fh;动物*pAn=&fh;pAn->呼吸;由于父类的指针pAn实际指向的对象类型是子类的对象,因此vptr指向的子类fish类的vtable,在调用pAn->breathe时,找到了fish类的breathe函数根据虚表中的函数地址。正是因为每个对象调用的虚函数都是以虚表指针为索引的,所以也决定了虚表指针的正确初始化是非常重要的。换句话说,在虚表指针未正确初始化之前,我们无法调用虚函数。#p#那么虚拟表指针是在什么时候,在哪里初始化的?C++创建虚表并在构造函数中初始化虚表指针。构造函数的调用顺序:构造子类对象时,必须先调用父类的构造函数。这时,编译器只是“看到”了父类,并不知道其背后是否有继承者。它初始化父类对象虚表指针的虚表指针vptr指向父类的虚表。当子类的构造函数执行时,会初始化子类对象的虚表指针vptr,此时vptr指向自己的虚表。在构造fish类的fh对象时,其内部虚表指针被初始化为指向fish类的虚表。类型转换后,调用pAn->breath。由于pAn实际指向的是fish类的对象,对象内部的虚表指针指向的是fish类的虚表,所以最后调用的是fish类的breath函数。解释:通过虚函数表指针VPTR调用重写函数是在程序运行时进行的,所以需要进行寻址操作来确定应该调用的函数。普通成员函数是在编译时调用的函数。在效率方面,虚函数的效率要低很多。为了效率,没有必要将所有的成员函数都声明为虚函数。创建对象时,编译器会初始化VPTR指针。只有当对象的构造完全完成后,VPTR指针才最终确定下来。是父类吗?对象的VPTR指向父类的虚函数表还是子类对象的VPTR指向子类的虚函数表。回到最初的问题:classA{voidg(){.....}};然后sizeof(A)=1;如果改为:classA{public:virtualvoidf(){...}voidg(){.....}}sizeof(A)=4,这是因为A类中有虚函数,顺序为了实现多态性,每个包含虚函数的类都隐含了一个静态虚指针vptr指向类的静态虚表vtable,vtable中的表项指向类中每个虚函数的表项地址。多态性是在程序进行动态绑定时实现的,而不是在编译时确定对象的调用方法的静态绑定。当程序运行到动态绑定时,通过vptr找到基类指针指向的对象类型所指向的vtable,然后调用其对应的方法实现多态。这称为动态绑定或惰性编译。classbase;base*pbase;classbase{public:base(){pbase=this;}virtualvoidfn(){cout<<"base"<fn();return0;}在基类的构造函数中,保存指向pbase全局变量的this指针。在定义全局对象aa,即调用derivedaa;时,必须调用基类的构造函数,先构造基类的部分,再构造子类的部分,将两部分拼接成一个完整的对象aa。当然this指针指向的是aa对象,所以我们在main函数中使用pbase调用fn,因为pbase实际指向的是aa对象,而aa对象内部的虚表指针指向的是自己的虚表,而最后的调用当然是派生类中的fn函数。在派生类中声明fn函数时,忘记加上public关键字,导致声明为private(默认为private),但是通过我们上面描述的虚函数调用机制,了解到这个地方不影响其正确输出的结果。不知道这是不是C++的bug,因为虚函数的调用是在运行时决定调用哪个函数,所以编译器在编译的时候并不知道pbase指向的是aa对象,所以出现这种奇怪的现象发生。如果直接使用aa对象来调用,由于对象类型是确定的(注意aa是对象变量,不是指针变量),编译器往往在编译时使用earlybinding来确定要调用的函数,所以会发现fn是private的,不能直接调用。#p#如果在基类的构造函数中直接调用虚函数怎么办?在调用基类的构造函数时,编译器只是“看到”了父类,并不知道其背后是否有继承者。它只是初始化父类对象的虚表指针,让虚表指针指向父类的虚表,所以结果当然是不正确的。只有在子类的构造函数被调用后,整个虚表被构造出来,C++的多态性才能真正得到应用。也就是说,不要在构造函数中调用虚函数来实现多态。当然,如果你只是想调用这个类的函数也没关系。得出一个结论:对比虚函数和纯虚函数。引入虚函数的原因:为了方便使用多态特性,我们往往需要在基类中定义虚函数。引入纯虚函数的原因:为了实现多态性,纯虚函数有点像java中的接口。他们不自己实现流程,而是让继承它的子类来实现它。在很多情况下,让基类自己生成对象是没有意义的。比如动物作为基类,可以派生出老虎、孔雀等子类,但是动物生成对象显然是不合理的。这时我们把动物类定义为一个抽象类,即包含纯虚函数的类。纯虚函数是指基类只定义了函数体,没有实现过程:virtualvoidEat()=0;direct=0不是在cpp中定义虚函数和纯虚函数的区别即可。虚函数中的函数即使为空也会被实现。虚函数是一个接口,一个函数声明,在基类中没有实现,必须在子类中实现。子类中可以不重载虚函数,但子类中必须实现虚函数。总结:对于虚函数调用,每个对象内部都有一个虚表指针,虚表指针初始化为本类的虚表。所以在程序中,不管你的对象类型怎么转换,对象内部的虚表指针是固定的,所以可以实现动态对象函数调用。这就是C++多态实现的原理。如果基类有虚函数:1.每个类都有一个虚表。2.虚拟表可以继承。如果子类不重写虚函数,那么子类虚表中仍然会有该函数的地址,只是这个地址指向了基类的虚函数实现。如果基类有3个虚函数,那么基类的虚表中就有3项(虚函数地址),派生类也会有一个虚表,至少有3项。如果对应的虚函数被改写,那么虚表中的地址就会发生变化,指向自己的虚函数实现。如果派生类有自己的虚函数,则将此条目添加到虚表中。3、派生类虚表中虚函数地址的顺序与基类虚表中虚函数地址的顺序相同。