空间设计方案,seo技术中心,软文文案范文,wordpress 4.5.6#x1f525; 个人主页#xff1a;大耳朵土土垚 #x1f525; 所属专栏#xff1a;C从入门至进阶 这里将会不定期更新有关C/C的内容#xff0c;欢迎大家点赞#xff0c;收藏#xff0c;评论#x1f973;#x1f973;#x1f389;#x1f389;#x1f389; 前言
我… 个人主页大耳朵土土垚 所属专栏C从入门至进阶 这里将会不定期更新有关C/C的内容欢迎大家点赞收藏评论 前言
我们知道C多态实现有两个条件——一是基类的指针或引用调用虚函数另一个是基类中有虚函数并且在派生类中实现虚函数重写这两个条件缺一不可这与多态实现的底层原理有关今天我们就来学习一下多态实现的原理 目录 前言1.虚函数表2.派生类中的虚表情况一只有基类有虚函数派生类没有情况二基类和派生类中都有虚函数并且虚函数没有被重写情况三基类中定义虚函数并且派生类中对该虚函数进行了重写综合这三种情况 3.多态原理动态绑定与静态绑定 4.多继承中的虚函数表5.结语 1.虚函数表
虚函数表Virtual Function TableVTable是C中实现动态多态性的一种机制。每个包含虚函数的类都有一个对应的虚函数表用于存储该类的虚函数地址。
虚函数表是一个包含函数指针的数组每个函数指针指向相应虚函数的实现。 也就是说在类中定义了虚函数那么该类就会包含一个虚函数表来存放虚函数的函数指针注意这里是指类中会存储虚函数表的指针来达到效果因为如果虚函数很多直接存储虚函数表可能会占用很多空间。 例如
class Base
{
public:virtual void Func1(){cout Func1() endl;}
private:int _b 1;
};int main()
{cout sizeof(Base) endl;return 0;
}当我们计算Base类的大小时发现只有一个int类型的成员变量_b所以应该是4个字节但是我们可以看一下结果 这里显示的是8字节这是因为Base类中创建了虚函数而每个包含虚函数的类都有一个对应的虚函数表虚函数的地址要被放到虚函数表中所以需要多余的空间来存储虚函数表的指针这个指针我们叫做虚函数表指针。 如下图所示 Base对象b中除了_b成员还多一个__vfptr指针放在对象的前面(注意有些平台可能会放到对象的最后面这个跟平台有关)这个指针我们叫做虚函数表指针(v代表virtualf代表function)。 注意是虚函数表的指针而不是直接存储虚函数表。 因为上述例子是在32位操作系统下执行的所以指针的大小是4字节Base类大小是8字节如果是64位那么指针的大小是8字节Base类的大小就应该参考结构体内存对齐规则应该是16字节。 2.派生类中的虚表
如果基类中没有虚函数派生类中有虚函数那么它的虚函数表和上面的一致。 例如
class Base
{
private:int _b 1;
};class Derive :public Base
{
public:virtual void Func2(){cout Func2() endl;}
private:int _d 2;
};int main()
{Derive d;return 0;
}上述代码基类中没有虚函数也就不存在虚函数表但派生类Derive类中存在虚函数所以会存放一个虚函数表指针__vfptr来指向虚函数表而虚函数表中又会存放Derive类中虚函数的指针。 如下图所示 但是如果基类中有虚函数表那么派生类该如何继承呢
情况一只有基类有虚函数派生类没有
例如
//情况一只有基类有虚函数
class Base
{
public:virtual void Func1(){cout Func1() endl;}
private:int _b 1;
};class Derive :public Base
{
private:int _d 2;
};int main()
{Derive d;return 0;
}d对象由两部分构成一部分是父类继承下来的成员另一部分是自己的成员如下图所示 我们看到d对象继承了基类的成员变量_b和虚函数表的指针虚函数表里面存放的是基类中虚函数Func1()的地址。 情况二基类和派生类中都有虚函数并且虚函数没有被重写
例如
//情况二基类和派生类中都有虚函数并且虚函数没有被重写
class Base
{
public:virtual void Func1(){cout Func1() endl;}
private:int _b 1;
};class Derive :public Base
{
public:virtual void Func2(){cout Func2() endl;}
private:int _d 2;
};int main()
{Derive d;return 0;
}首先基类中有虚函数所以派生类中包含基类的那部分成员肯定会包含基函数表的指针但是派生类的虚函数应该怎么存放呢结果如下图所示 我们发现派生类并没有生成自己的虚函数表所以它的虚函数应该存放在从基类继承下来的虚函数表中上图中看到虚函数数组内只存放一个虚函数Func1()没有派生类自己的虚函数Fun2()这里是编译器的监视窗口故意隐藏了Fun2()函数也可以认为是他的一个小bug那么我们可以思考从打印虚函数地址的思路入手查看Fun1()和Fun2()的地址是否是存放在一起。 虚函数的地址存放在虚函数表中而对象中前四个字节存放的是虚函数表的指针所以我们可以使用强制类型转换取出对象的前四个字节但是int类型与Base和Derive类型不兼容不能相互转换但是指针之间可以相互转换所以我们考虑先取Base和Derive类对象的地址然后强制转换成int*类型然后再解引用就得到了虚函数表的地址 代码如下
//情况二基类和派生类中都有虚函数并且虚函数没有被重写
//基类和派生类代码如上//先定义一个函数指针类型
typedef void(*VFPTR) ();//打印函数指针数组中存放的函数地址
void PrintVTable(VFPTR vTable[])
{// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数cout 虚表地址 vTable endl;for (int i 0; vTable[i] ! nullptr; i){printf( 第%d个虚函数地址 :0X%x-, i, vTable[i]);VFPTR f vTable[i];f();//调用该函数}cout endl;
}void test()
{Base b;Derive d;//打印b对象中虚函数的地址VFPTR* vTableb (VFPTR*)(*(int*)b);PrintVTable(vTableb);//打印d对象中虚函数的地址VFPTR* vTabled (VFPTR*)(*(int*)d);PrintVTable(vTabled);
}结果如下 我们发现基类对象和派生类对象的虚函数表是不同的并且派生类对象虚函数表中存放了两个虚函数的地址其中一个与基类的虚函数地址相同也就是Func1()的地址另一个则是派生类自己定义的虚函数Func2()的地址。 综上所述如果派生类和基类都定义了自己的虚函数并且基类的虚函数没有在派生类中重写的话那么派生类中虚函数的地址会存放在派生类继承的基类那部分的虚函数表中的末尾并且基类定义的对象和派生类定义的对象的虚函数表的地址是不同的。
同一类定义的不同对象使用的基函数表是同一个。 如下图所示 情况三基类中定义虚函数并且派生类中对该虚函数进行了重写
例如
//情况三基类中定义虚函数并且派生类中对该虚函数进行了重写
class Base
{
public:virtual void Func1(){cout Base::Func1() endl;}
private:int _b 1;
};class Derive :public Base
{
public:virtual void Func1()//重写虚函数Fun1(){cout Derive::Func1() endl;}
private:int _d 2;
};int main()
{Base b;Derive d;return 0;
}按照之前的结论派生类中的虚函数的地址是存放在继承的基类的虚函数表中的那么对于重写的虚函数是写在基类虚函数表的末尾还是将基类被重写的虚函数地址覆盖呢 结果如下图 可以看到派生类的重写的虚函数的地址覆盖了继承的基类的虚函数的地址我们还可以使用上文中打印虚函数地址的方式更加直观的看清楚 上述例子中基类b对象和派生类d对象虚表是不一样的这里我们发现Func1完成了重写所以d的虚表中存的是重写的Derive::Func1所以虚函数的重写也叫作覆盖覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法覆盖是原理层的叫法。 综合这三种情况
如果基类没有虚函数子类有虚函数那么子类会自己生成一个虚函数表来存放自己的虚函数如果基类有虚函数子类也有自己的虚函数那么子类中虚函数的地址会存放在子类继承的基类那部分的虚函数表中的末尾 注意虽然说是继承基类的虚函数表但是基类对象和子类对象的虚函数表是不同的表它们各自有各自的表。只是因为子类会继承基类的虚函数所以基类的虚函数指针也会存在该子类的虚函数表中相当于将基类的虚函数表直接继承下来再将子类自己的虚函数指针存放进去子类也就不用自己再生成一个虚函数表。 如果基类有虚函数并且子类对该虚函数进行了重写那么子类虚函数表中基类被重写的虚函数地址就会被子类重写的虚函数地址覆盖而不再和第二点一样写在虚函数表的尾部。
例如
class Base
{
public:virtual void Func1(){cout Base::Func1() endl;}virtual void Func2(){cout Base::Func2() endl;}void Func3(){cout Base::Func3() endl;}
private:int _b 1;
};class Derive : public Base
{
public:virtual void Func1()//重写{cout Derive::Func1() endl;}
private:int _d 2;
};int main()
{Base b;Derive d;
}基类b对象和派生类d对象虚表是不一样的这里我们发现Func1完成了重写所以d的虚表中存的是重写的Derive::Func1另外Func2继承下来后是虚函数所以放进了虚表Func3也继承下来了但是不是虚函数所以不会放进虚表。 虚函数表本质是一个存虚函数指针的指针数组一般情况这个数组最后面放了一个nullptr。
总结一下派生类的虚表生成 a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
✨✨这里还有一个很容易混淆的问题虚函数存在哪的虚表存在哪的 虚函数存在虚表虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意虚表存的是虚函数指针不是虚函数虚函数和普通函数一样的都是存在代码段的只是他的指针又存到了虚表中。另外对象中存的不是虚表存的是虚表指针。那么虚表存在哪的呢实际我们去验证一下会发现vs下是存在代码段的。 代码如下
int main()
{int i 0;static int j 1;int* p1 new int;const char* p2 aaaaa;Base b;Derive d;printf(栈%p\n, i);printf(静态区%p\n, j);printf(堆%p\n, p1);printf(常量区%p\n, p2);printf(Base虚表地址%p\n, *(int*)b);printf(Derive虚表地址%p\n, *(int*)d);return 0;
}通过比较不同区域的地址来判断虚表地址在哪发现虚表离常量区最近如下图所示 3.多态原理
了解了虚函数表我们就可以深入学习多态的原理。
例如
//多态原理
class Person {
public:virtual void BuyTicket() { cout 买票-全价 endl; }
};class Student : public Person {
public:virtual void BuyTicket() { cout 买票-半价 endl; }
};void Func(Person p)
{p.BuyTicket();
}int main()
{Person Black;Func(Black);Student tutu;Func(tutu);return 0;
}对于买票这个行为不同的对象会有不同的结果普通人买票是全价学生则可能是半价如下图所示 那么我们可以通过调试来看看它有不同结果的原因 当使用Person类对象调用函数Func时 当使用Student类对象调用函数Func时 我们看到p是指向Black对象时p-BuyTicket在Black的虚表中找到虚函数是Person::BuyTicket。p是指向tutu对象时p-BuyTicket在tutu的虚表中找到虚函数Student::BuyTicket。 我们发现不同的对象调用Func函数时使用的虚函数表是不同的Person类对象和Student类对象都使用各自的虚函数表所以调用不同的虚函数如下图所示 这样就实现出了不同对象去完成同一行为时展现出不同的形态。 反过来思考我们要达到多态有两个条件一个是虚函数覆盖一个是基类对象的指针或引用调用虚函数。具体原因我们先要了解一下动态绑定。
动态绑定与静态绑定
静态绑定又称为前期绑定(早绑定)在程序编译期间确定了程序的行为也称为静态多态比如函数重载动态绑定又称后期绑定(晚绑定)是在程序运行期间根据具体拿到的类型确定程序的具体行为调用具体的函数也称为动态多态。
那么当我们直接使用对象调用成员函数时走的是静态绑定是指编译期间就确定的程序行为当我们使用基类指针或引用调用虚函数时走的是动态绑定需要通过虚函数表来确定不同对象调用不同的函数根据具体拿到的类型确定程序的具体行为。 所以对于多态实现的两个条件首先我们需要通过基类对象的指针或引用调用虚函数才能走动态绑定其次派生类的虚函数还需要重写这样不同类的对象使用的虚函数才是不一样的才会显现不同的状态实现多态。 如果只是完成了虚函数的覆盖而没有通过基类对象的指针或引用调用或者只有第二个条件都无法完成多态的实现。 多态实现的两个条件缺一不可。 4.多继承中的虚函数表
在多继承中派生类会继承多个基类每个基类都有自己的虚表。因此派生类会有多个虚表每个虚表对应于一个基类。 这是为了保证派生类能够正确调用和重写每个基类的虚函数。当派生类实例化时会为每个基类分配一个虚表指针这些虚表指针会存储在派生类对象的内存中。 例如
class Base1 {
public:virtual void func1() { cout Base1::func1 endl; }virtual void func2() { cout Base1::func2 endl; }
private:int b1;
};class Base2 {
public:virtual void func1() { cout Base2::func1 endl; }virtual void func2() { cout Base2::func2 endl; }
private:int b2;
};class Derive : public Base1, public Base2 {
public:virtual void func1() { cout Derive::func1 endl; }//重写Fun1()virtual void func3() { cout Derive::func3 endl; }
private:int d1;
};//打印虚表地址以及虚函数地址
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{cout 虚表地址 vTable endl;for (int i 0; vTable[i] ! nullptr; i){printf( 第%d个虚函数地址 :0X%x,-, i, vTable[i]);VFPTR f vTable[i];f();}cout endl;
}int main()
{Derive d;//第一个虚表地址VFPTR* vTableb1 (VFPTR*)(*(int*)d);PrintVTable(vTableb1);//找到第二个虚表的地址VFPTR* vTableb2 (VFPTR*)(*(int*)((char*)d sizeof(Base1)));PrintVTable(vTableb2);return 0;
}结果如下 上图可以看出多继承派生类的未重写的虚函数func3()放在第一个继承基类部分的虚函数表中图示如下 5.结语
虚函数表的存在是为了实现动态绑定也就是实现多态当派生类对基类的虚函数进行重写时通过基类对象指针和引用调用虚函数时就会通过虚函数表来确定不同对象调用不同的函数根据具体拿到的类型确定程序的具体行为所以多态实现的两个条件缺一不可。以上就是今天所有的内容啦~ 完结撒花