网站怎么更改后台登陆密码,养殖公司网站,无锡网站制作推广,保定哪家做网站专业类的虚表
每个包含了虚函数的类都包含一个虚表。
当一个类#xff08;B#xff09;继承另一个类#xff08;A#xff09;时#xff0c;类B会继承类A的函数的调用权。所以如果一个基类包含了虚函数#xff0c;那么其继承类也可调用这些虚函数#xff0c;换句话说…类的虚表
每个包含了虚函数的类都包含一个虚表。
当一个类B继承另一个类A时类B会继承类A的函数的调用权。所以如果一个基类包含了虚函数那么其继承类也可调用这些虚函数换句话说一个类继承了包含虚函数的基类那么这个类也拥有自己的虚表。
来看以下的代码。类A包含虚函数vfunc1vfunc2由于类A包含虚函数故类A拥有一个虚表。
class A {
public:virtual void vfunc1(){ cout A::vfunc1 endl;}virtual void vfunc2(){ cout A::vfunc2 endl;}void func1();void func2();
private:int m_data1, m_data2;
};
类A的虚表如图1所示。 虚表是一个指针数组其元素是虚函数的指针每个元素对应一个虚函数的函数指针。需要指出的是普通的函数即非虚函数其调用并不需要经过虚表所以虚表的元素并不包括普通函数的函数指针。
虚函数指针的赋值发生在编译器的编译阶段也就是说在代码的编译阶段虚表就可以构造出来了。
虚表是属于类的而不是属于某个具体的对象一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。
虚表指针
为了指定对象的虚表对象内部包含一个虚表的指针来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针编译器在类中添加了一个指针*__vptr用来指向虚表。这样当类的对象在创建时便拥有了这个指针且这个指针的值会自动被设置为指向类的虚表。 上面指出一个继承类的基类如果包含虚函数那个这个继承类也有拥有自己的虚表故这个继承类的对象也包含一个虚表指针用来指向它的虚表。
虚函数表存储位置
首先虚函数表存储在只读数据段.rodata、虚函数存储在代码段.text、虚表指针的存储的位置与对象存储的位置相同可能在栈、也可能在堆或数据段等。
虚函数
只有类的成员函数才能声明为虚函数虚函数仅适用于有继承关系的类对象。普通函数不能声明为虚函数。virtual具有继承性父类中定义为virtual的函数在子类中重写的函数也自动成为虚函数。静态成员函数不能是虚函数因为静态成员函数不受限于某个对象。内联函数inline不能是虚函数因为内联函数不能在运行中动态确定位置。构造函数不能是虚函数。
构造一个对象的时候必须知道对象的实际类型而虚函数是在运行期间确定实际类型的。如果构造函数为虚函数则在构造一个对象时由于对象还未构造成功编译器还无法知道对象的实际类型是该类本身还是派生类。无法确定。虚函数的执行依赖于虚函数表而虚函数表是在构造函数中初始化的即初始化vptr让它指向虚函数表。如果构造函数为虚函数则在构造对象期间虚函数表还没有被初始化将无法进行。
析构函数可以是虚函数而且有时是必须声明为虚函数。
虚析构函数是为了解决这样的一个问题基类的指针指向派生类对象并用基类的指针删除派生类对象时要使用虚析构函数。
此时 vtable 已经初始化了完全可以把析构函数放在虚函数表里面来调用。C类有继承时基类的析构函数必须为虚函数。如果不是虚函数则使用时可能存在内存泄漏的问题。
如果我们以这种方式创建对象
SubClass* pObj new SubClass();
delete pObj;没有实现多态不管析构函数是否是虚函数(即是否加virtual关键词)delete时基类和子类都会被释放
如果我们要实现多态令基类指针指向子类即以这种方式创建对象
BaseClass* pObj new SubClass();
delete pObj;若析构函数是虚函数(即加上virtual关键词)delete时基类和子类都会被释放若析构函数不是虚函数(即不加virtual关键词)只会调用基类的析构函数delete时只释放基类不释放子类会造成内存泄漏问题。
构造函数或者析构函数中调用虚函数会怎样
由于类的构造次序是由基类到派生类所以在构造函数中调用虚函数派生类还没有完全构造虚函数是不会呈现出多态的。类的析构是从派生类到基类当调用继承层次中某一层次的类的析构函数时意味着其派生类部分已经析构掉所以也不会呈现多态。
动态绑定
静态绑定又称为前期绑定(早绑定)在程序编译期间确定了程序的行为也称为静态多态比如函数重载动态绑定又称后期绑定(晚绑定)是在程序运行期间根据具体拿到的类型确定程序的具体行为调用具体的函数也称为动态多态。
C是如何利用虚表和虚表指针来实现动态绑定的。我们先看下面的代码。
class A
{
public:virtual void vfunc1(){cout A::vfunc1 endl;}virtual void vfunc2(){cout A::vfunc2 endl;}void func1();void func2();private:int m_data1, m_data2;
};class B : public A
{
public:void vfunc1(){{cout B::vfunc1 endl;}}void func1();private:int m_data3;
};class C : public B
{
public:virtual void vfunc2(){cout C::vfunc2 endl;}void func2();private:int m_data1, m_data4;
};类A是基类类B继承类A类C又继承类B。类A类B类C其对象模型如下图3所示。 由于这三个类都有虚函数故编译器为每个类都创建了一个虚表即类A的虚表A vtbl类B的虚表B vtbl类C的虚表C vtbl。类A类B类C的对象都拥有一个虚表指针*__vptr用来指向自己所属类的虚表。
类A包括两个虚函数故A vtbl包含两个虚函数指针内容为虚函数地址的指针变量分别指向A::vfunc1()和A::vfunc2()。
类B继承于类A故类B可以调用类A的函数但由于类B重写覆盖了B::vfunc1()函数两个分属基类和派生类的同名函数返回值,函数名参数列表都要与基类相同才叫重写会生成一个具有新地址的虚函数覆盖掉继承下来的虚函数否则就是重定义父类指针就不能调用子类故B vtbl的两个指针分别指向B::vfunc1()和A::vfunc2()。
类C继承于类B故类C可以调用类B的函数但由于类C重写了C::vfunc2()函数故C vtbl的两个指针分别指向B::vfunc1()指向继承的最近的一个类的函数和C::vfunc2()。
总结“对象的虚表指针用来指向自己所属类的虚表如果虚函数没有重写虚表中的指针会指向其继承的最近的一个类的虚函数如果重写虚表中的指针会指向重写的新的虚函数的地址且继承下来的虚函数会被覆盖掉”
非虚函数的调用不用经过虚表故不需要虚表中的指针指向这些函数。
假设我们定义一个类B的对象。由于bObject是类B的一个对象故bObject包含一个虚表指针指向类B的虚表。
int main()
{B bObject;
}
现在我们声明一个类A的指针p来指向对象bObject。虽然p是基类的指针只能指向基类的部分但是虚表指针亦属于基类部分所以p可以访问到对象bObject的虚表指针。bObject的虚表指针指向类B的虚表所以p可以访问到B vtbl。我们使用p来调用vfunc1()函数
int main()
{B bObject;A *p bObject;p-vfunc1();p-vfunc2();
}// 输出
B::vfunc1
A::vfunc2
程序在执行p-vfunc1()时会发现p是个指向对象的指针且调用的函数是虚函数非虚函数的话直接调用虚函数需要借助虚表调用接下来便会进行以下的步骤。 首先根据虚表指针p-__vptr来访问对象bObject对应的虚表。虽然指针p是基类A*类型但是*__vptr也是基类的一部分所以可以通过p-__vptr可以访问到对象bObject对应的虚表。 然后在虚表中查找所调用的函数对应的条目。由于虚表在编译阶段就可以构造出来了所以可以根据所调用的函数定位到虚表中的对应条目。对于 p-vfunc1()的调用B vtbl的第一项即是vfunc1对应的条目。由于vfun1重写了覆盖了A中继承的vfunc1,故调用的是B中重写的函数 最后根据虚表中找到的函数指针调用函数。从图3可以看到B vtbl的第一项指向B::vfunc1()所以 p-vfunc1()实质会调用B::vfunc1()函数。
如果p指向类A的对象情况又是怎么样
int main()
{A aObject;A *p aObject;p-vfunc1();
}
当aObject在创建时它的虚表指针__vptr已设置为指向A vtbl这样p-__vptr就指向A vtbl。vfunc1在A vtbl对应在条目指向了A::vfunc1()函数所以 p-vfunc1()实质会调用A::vfunc1()函数。
通过使用这些虚函数表即使使用的是基类的指针来调用函数也可以达到正确调用运行中实际对象的虚函数。
我们把经过虚表调用虚函数的过程称为动态绑定其表现出来的现象称为运行时多态。动态绑定区别于传统的函数调用传统的函数调用我们称之为静态绑定即函数的调用在编译阶段就可以确定下来了。
多态的概念通俗来说就是多种形态具体点就是去完成某个行为当不同的对象去完成时会产生出不同的状态
执行函数的动态绑定需要符合以下两个条件。
(1) 只有虚函数才能进行动态绑定非虚函数不进行动态绑定。 (2) 必须通过基类类型的引用或指针进行函数调用。
如果一个函数调用符合以上两个条件编译器就会把该函数调用编译成动态绑定其函数的调用过程走的是上述通过虚表的机制。
通过基类指针或基类引用做形参当实参传入不同的派生类(或基类)的指针或引用在函数内部触发动态绑定从而来运行时实现多态的。
扩展
编译时多态静态多态通过重载函数实现运行时多态动态多态通过虚函数实现
在继承中构成多态的两个条件
必须通过基类的指针或者引用调用虚函数被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写
override 和 final (C11)
C对函数重写的要求比较严格但是有些情况下由于疏忽可能会导致函数名字母次序写反而无法构成重写而这种错误在编译期间是不会报出的只有在程序运行时没有得到预期结果才来debug会得不偿失因此C11提供了override和final两个关键字可以帮助用户检测是否重写
final修饰虚函数表示该虚函数不能再被重写
class Person
{
public:virtual void Print() final{cout _No endl;}int _No 1;
};
class Student : public Person
{
public:virtual void Print()//不能继承{cout _age endl;}int _age 100;
};2.override: 检查派生类虚函数是否重写了基类某个虚函数如果没有重写编译报错
class Person
{
public:virtual void Print(){cout _No endl;}int _No 1;
};
class Student : public Person
{
public:virtual void Print(int x) override{cout _age endl;}int _age 100;
};如果我们上面图片中的override去掉能够编译通过但是基类Person与派生类Student里面的Print函数不构成动态绑定而是构成隐藏关系因为同名函数不在同一作用域派生类会隐藏掉基类里面的同名函数。
class A
{
public:virtual void vfunc1(){cout A::vfunc1 endl;}virtual void vfunc2(){cout A::vfunc2 endl;}void func1();void func2();private:int m_data1, m_data2;
};class B : public A
{
public:void vfunc1(int x){{cout B::vfunc1 endl;}}void func1();private:int m_data3;
};class C : public B
{
public:virtual void vfunc2(){cout C::vfunc2 endl;}void func2();private:int m_data1, m_data4;
};int main()
{B bObject;A *p bObject;p-vfunc1();p-vfunc2();
}// 输出
A::vfunc1
A::vfunc2
如果改成
int main()
{B bObject;B *p bObject;p-vfunc1();p-vfunc2();
}
报错 error: no matching function for call to B::vfunc1()只有子类的虚函数和父类的虚函数定义完全一样才被认为是虚函数,比如父类后面加了const,如果子类不加的话就是隐藏了,不是覆盖. 纯虚函数
纯虚函数是一种特殊的虚函数在许多情况下在基类中不能对虚函数给出有意义的实现而把它声明为纯虚函数它的实现留给该基类的派生类去做。这就是纯虚函数的作用。
纯虚函数声明如下 virtual void funtion1()0; 纯虚函数一定没有定义纯虚函数用来规范派生类的行为即接口。包含纯虚函数的类是抽象类抽象类不能够实例化但可以声明指向实现该抽象类的具体类的指针或引用。
为啥引入纯虚函数呢
为了方便使用多态特性我们常常需要在基类中定义虚函数。在很多情况下基类本身生成对象是不合情理的。例如动物作为一个基类可以派生出老虎、孔雀等子类但动物本身生成对象明显不合常理。
为了解决上述问题引入了纯虚函数的概念将函数定义为纯虚函数。若要使派生类为非抽象类则编译器要求在派生类中必须对纯虚函数予以重写以实现多态性。同时含有纯虚函数的类称为抽象类它不能生成对象。这样就很好地解决了上述两个问题。
纯虚函数的意义让所有的类对象主要是派生类对象都可以执行纯虚函数的动作但类无法为纯虚函数提供一个合理的默认实现。所以类纯虚函数的声明就是在告诉子类的设计者你必须提供一个纯虚函数的实现但我不知道你会怎样实现它。
#include iostream
using namespace std;// 抽象类
class Shape
{
public:// 提供接口框架的纯虚函数virtual int getArea() 0;void setWidth(int w){width w;}void setHeight(int h){height h;}
protected:int width;int height;
};// 派生类
class Rectangle: public Shape
{
public:int getArea(){ return (width * height); }
};
class Triangle: public Shape
{
public:int getArea(){ return (width * height)/2; }
};int main(void)
{Rectangle Rect;Triangle Tri;Rect.setWidth(5);Rect.setHeight(7);// 输出对象的面积cout Total Rectangle area: Rect.getArea() endl;Tri.setWidth(5);Tri.setHeight(7);// 输出对象的面积cout Total Triangle area: Tri.getArea() endl; return 0;
}// 输出
Total Rectangle area: 35
Total Triangle area: 17
从上面的实例中我们可以看到一个抽象类是如何定义一个接口 getArea()两个派生类是如何通过不同的计算面积的算法来实现这个相同的函数。