光明楼网站建设,优秀网站设计案例分析ppt,建设银行防钓鱼网站,企业培训网站建设【c随笔14】虚函数表 一、虚函数表#xff08;Virtual Function Table#xff09;1、定义2、查看虚函数表2.1、 问题#xff1a;三种类型#xff0c;包含一个int类型的class、一个int类型的变量、int类型的指针#xff1a;这三个大小分别是多少呢#xff1f;2.2、怎么发现… 【c随笔14】虚函数表 一、虚函数表Virtual Function Table1、定义2、查看虚函数表2.1、 问题三种类型包含一个int类型的class、一个int类型的变量、int类型的指针这三个大小分别是多少呢2.2、怎么发现虚函数表存在的2.3、查看虚函数表里面都有什么 3、继承——虚函数的重写与覆盖4、虚函数为何可以实现多态5、对象也能切片为什么不能实现多态普通的继承为何不能实现多态?6、打印虚函数表6.1同一个类型它们的虚表内存地址都是一样的同一类型的对象共用一份虚表。6.2打印虚函数表 二、多态原理1、动态绑定、动态类型、静态类型2、动态绑定和静态绑定对比3、虚函数表的存储位置4、汇编层面看多态实现原理 三、多继承中的虚函数表1、多继承会有两张虚表继承两个时2、派生类定义的虚函数存放在第一张虚函数表中 四、经典问题 原创作者郑同学的笔记 原创地址https://zhengjunxue.blog.csdn.net/article/details/131932164 qq技术交流群921273910
一、虚函数表Virtual Function Table
1、定义
虚函数表是一个由虚函数组成的表格用于实现动态绑定和多态性。每个包含虚函数的类都有自己的虚函数表该表列出了该类及其所有基类的虚函数。当一个对象被创建时它的类虚函数表也被创建并且可以通过该对象的指针或引用来调用虚函数表中的函数。 虚函数表是一种实现动态多态性的机制。每个包含虚函数的类都有一个虚函数表其中存储着该类的虚函数地址。当通过基类指针或引用调用虚函数时程序会根据对象的实际类型查找对应类的虚函数表并调用正确的虚函数。 2、查看虚函数表
2.1、 问题三种类型包含一个int类型的class、一个int类型的变量、int类型的指针这三个大小分别是多少呢 #include iostream
using namespace std;class Base {
private:int _b 1;
public:void Func1() {cout Func1() endl;}void Func2() {cout Func2() endl;}void Func3() {cout Func3() endl;;}
};int main(void)
{Base b;cout sizeof(b) endl;cout sizeof(int) endl;cout sizeof(int *) endl;return 0;
}输出 答案64位系统 class 对象实例占4个字节int 变量占4个字节int 指针占用8个字节 其他结论 类class占用内存的大小就是类calss成员变量占用内存的大小类class占用内存的大小和成员函数无关类也可以作为一种数据类型来看待 Base实例b里面有什么 只有成员变量_b 2.2、怎么发现虚函数表存在的
2.2.1加了virtual后虚函数Base的大小
#include iostream
using namespace std;class Base {
private:int _b 1;
public:virtual void Func1() {cout Func1() endl;}virtual void Func2() {cout Func2() endl;}virtual void Func3() {cout Func3() endl;;}
};int main(void)
{Base b;cout sizeof(b) endl;return 0;
}输出 结论 加了virtua后虚函数Base大小为16 2.2.2、加了虚函数表后为何变成了16字节 调试查看Base类的实例b里面都有什么 调试截图如下除了 _b 成员外还有了一个 _vfptr 在 b1对象中 结论
由于类里面除了 _b 成员外还增加了_vfptr所以由4字节变成了16字节_vfptr就是虚函数表指针virtual function pointer指向虚函数表 扩展虚函数表指针占用8字节int类型占用4字节那84应该是12字节为何总的内存变成了18字节呢这里面有个内存对齐的问题。打个比方你int类型占用4字节double占用8字节那么会总的便会占用16字节int类型也会分配8字节的空间。 2.3、查看虚函数表里面都有什么 结论
虚函数表里面是一个数组数组里面存储的是每一个虚函数的地址 扩展 其实看了我之前写的文章就知道其实类的实例的每一个成员函数都有一个单独的地址存储在代码段.text段。 3、继承——虚函数的重写与覆盖
代码现在我们增加一个子类 Derive 去继承 Base
#include iostream
using namespace std;class Base {
private:int _b 1;public:virtual void Func1() {cout Func1() endl;}virtual void Func2() {cout Func2() endl;}virtual void Func3() {cout Func3() endl;;}
};// 子类 Derive
class Derive : public Base {
public:virtual void Func1() {cout Derive::Func1() endl;}
private:int _d 2;
};int main(void)
{Derive d;cout sizeof(d) endl;return 0;
}
输出 父类 b 对象和子类 b 对象虚表是不一样的这里看我们发现 Func1 完成了重写
所以 d 的虚表中存的是重写的 Derive::Func1所以虚函数的重写也叫做覆盖。
就可以理解为子类的虚表拷贝了父类的虚表子类的 Func1 覆盖掉了父类上的 Func1。
覆盖指的是虚表中虚函数的覆盖
虚函数重写语法层的概念子类对继承父类虚函数实现进行了重写。虚函数覆盖原理层的概念子类的虚表拷贝父类虚表进行了修改覆盖重写那个虚函数。 总结虚函数的重写与覆盖重写是语法层的叫法覆盖是原理层的叫法。
4、虚函数为何可以实现多态
多态调用实现是依靠运行时去指向对象的虚表中查调用函数地址。
#include iostream
using namespace std;class Base {
private:int _b 1;public:virtual void Func1() {cout Base::Func1() endl;}virtual void Func2() {cout Base::Func2() endl;}virtual void Func3() {cout Base::Func3() endl;;}
};// 子类 Derive
class Derive : public Base {
public:virtual void Func1() {cout Derive::Func1() endl;}
private:int _d 2;
};int main(void)
{Base b;Derive d;Base* ptr b;ptr-Func1(); // 调用的是父类的虚函数ptr d;ptr-Func1(); // 调用的是子类的虚函数return 0;
}输出 5、对象也能切片为什么不能实现多态普通的继承为何不能实现多态?
既然指针和引用可以实现多态那父类赋值给子类对象也可以切片 根本原因是对象切片时子类对象只会拷贝成员给父类对象并不会拷贝虚表指针。没有虚函数表 之前我们讨论过为何没有静态多态的概念除了当时说的部符合多态的定义外本质的原因就在这里。 6、打印虚函数表
6.1同一个类型它们的虚表内存地址都是一样的同一类型的对象共用一份虚表。
#include iostream
using namespace std;class Base {
public:int _b 1;public:virtual void Func1() {cout Base::Func1() endl;}virtual void Func2() {cout Base::Func2() endl;}
};// 子类 Derive
class Derive : public Base {
public:virtual void Func1() {cout Derive::Func1() endl;}virtual void Func3() {cout Derive::Func3() endl;;}
public:int _d 2;
};using pf void(*)();
//typedef void(*pf)(void); //和上面的写法相等看不懂的可以看下我的另外一篇博客《函数指针》int main(void)
{Base b;Derive d1;Derive d2;return 0;
}查看局部变量的窗口 可以看到子类继承自父类的虚函数表中Func1函数地址是重写之后的函数地址已经将父类的func函数地址覆盖掉。
如果父类中的虚函数没有被子类重写那么子类的虚函数表中的地址仍然是父类中虚函数的地址。只有虚函数才会进虚函数表非虚函数是不进虚函数表的。如果派生类中存在新增加的虚函数那么就会按照在派生类中的声明顺序依次添加到派生类的虚函数表的最后。虚函数表本质就是一个虚函数指针数组而虚函数表指针本质就是这个数组的首元素地址。虚函数表的最后一个字段通常置为nullptr。
6.2打印虚函数表
我们例子中每一个虚函数返回值类型void,参数无所以虚函数指针数组中元素的类型为void(*)(void)不懂的可以查看我的另外一篇博客《【c随笔09】函数指针》
注意派生类的虚函数visual并没显示出来但是我们打印出来了可见visual的可视化是有些问题的。
#include iostream
using namespace std;class Base {
public:int _b 1;public:virtual void Func1() {cout Base::Func1() endl;}virtual void Func2() {cout Base::Func2() endl;}
};// 子类 Derive
class Derive : public Base {
public:virtual void Func1() {cout Derive::Func1() endl;}virtual void Func3() {cout Derive::Func3() endl;;}
public:int _d 2;
};using pf void(*)();
//typedef void(*pf)(void); //和上面的写法相等看不懂的可以看下我的另外一篇博客《函数指针》int main(void)
{Base b;Derive d1;Derive d2;Base* ptr d1;//ptr-Func1(); // 调用的是父类的虚函数//Base* ptr2 new Derive();pf* pfun (pf*)*(long long*)ptr;//2.pp这个指针是函数指针数组的首元素的地址。while (*pfun){cout *pfun endl;(*pfun)();cout endl;pfun;}cout ---------------------------------------- endl;ptr d2;pfun (pf*)*(long long*)ptr;while (*pfun){cout *pfun endl;(*pfun)();cout endl;pfun;}cout ---------------------------------------- endl;ptr b;pfun (pf*)*(long long*)ptr;while (*pfun){cout *pfun endl;(*pfun)();cout endl;pfun;}return 0;
}输出 二、多态原理
1、动态绑定、动态类型、静态类型
我们依然查看《C Primer 第5版》第15章节末尾 术语表中的介绍p575-576页 动态绑定(dynamic binding 直到运行时才确定到底执行函数的哪个版本。在C语言中动态绑定的意思是在运行时根据引用或指针所绑定对象的实际类型来选择执行虚函数的某一个版本。 动态类型(dynamic type) 对象在运行时的类型。引用所引对象或者指针所指对象的动态类型可能与该引用或指针的静态类型不同。基类的指针或引用可以指向一个派生类对象。在这样的情况中静态类型是基类的引用或指针而动态类型是派生类的引用或指针。 静态类型(static type) 对象被定义的类型或表达式产生的类型。静态类型在编译时是已知的。
《C Primer 第5版》第15.2章节p529页
以动态绑定有时又被称为运行时绑定run-time binding。在C语言中当我们使用基类的引用或指针调用一个虚函数时将发生动态绑定。
2、动态绑定和静态绑定对比 由于《C Primer 第5版》并没有给出静态绑定的概念我们暂时把在程序编译期间确定了程序的行为也称为静态绑定。比如函数重载。 动态绑定
#include iostream
using namespace std;class Base {
private:int _b 1;
public:void Func1() {cout Func1() endl;}
};int main(void)
{Base *ptr1 new Base();ptr1-Func1();return 0;
}静态绑定
#include iostream
using namespace std;class Base {
private:int _b 1;
public:virtual void Func1() {cout Func1() endl;}
};int main(void)
{Base *ptr1 new Base();ptr1-Func1();return 0;
}汇编层面分析静态绑定和动态绑定的区别
g main.cpp
objdump -h -d -x ./a.out对比反汇编的代码如下截图 3、虚函数表的存储位置
推断虚表存储在只读数据段上。
4、汇编层面看多态实现原理
多态
#include iostream
using namespace std;class Base {
private:int _b 1;
public:virtual void Func1() {cout Func1() endl;}
};// 子类 Derive
class Derive : public Base {
public:virtual void Func1() {cout Derive::Func1() endl;}
private:int _d 2;
};void pf(Base *b)
{b-Func1();
}int main(void)
{Base *ptr1 new Base();pf(ptr1);Base *ptr2 new Derive();pf(ptr2);return 0;
}
非多态
#include iostream
using namespace std;class Base {
private:int _b 1;
public:void Func1() {cout Func1() endl;}
};// 子类 Derive
class Derive : public Base {
public:void Func1() {cout Derive::Func1() endl;}
private:int _d 2;
};void pf(Base *b)
{b-Func1();
}int main(void)
{Base *ptr1 new Base();pf(ptr1);Base *ptr2 new Derive();pf(ptr2);return 0;
}g main.cpp
objdump -h -d -x ./a.out对比反汇编的代码如下截图 三、多继承中的虚函数表 1、多继承会有两张虚表继承两个时
// 基类A
class A {
public:virtual void func1() {std::cout A::func1() std::endl;}virtual void func2() {std::cout A::func2() std::endl;}};// 基类B
class B {
public:virtual void func1() {std::cout B::func1() std::endl;}/*virtual void func2() {std::cout B::func2() std::endl;}*/};// 派生类C多继承自A和B
class C : public A, public B {
public:virtual void func1() override {std::cout C::func1() std::endl;}/*virtual void func2() override {std::cout C::func2() std::endl;}*/virtual void func3() {std::cout C::func3() std::endl;}
};int main() {C c;
}我们先透过监视简单看一下 2、派生类定义的虚函数存放在第一张虚函数表中
#include iostream// 基类A
class A {
public:virtual void func1() {std::cout A::func1() std::endl;}virtual void func2() {std::cout A::func2() std::endl;}};// 基类B
class B {
public:virtual void func1() {std::cout B::func1() std::endl;}/*virtual void func2() {std::cout B::func2() std::endl;}*/};// 派生类C多继承自A和B
class C : public A, public B {
public:virtual void func1() override {std::cout C::func1() std::endl;}/*virtual void func2() override {std::cout C::func2() std::endl;}*/virtual void func3() {std::cout C::func3() std::endl;}
};int main() {C c;// 打印A的虚函数表std::cout As vtable: std::endl;void** aVTable *(void***)(c);for (int i 0; aVTable[i] ! nullptr; i) {std::cout [ i ] aVTable[i] - 函数执行;void(*func)() (void(*)())(aVTable[i]);func();}// 打印B的虚函数表std::cout Bs vtable: std::endl;void** bVTable *(void***)(((char*)c) sizeof(A));for (int i 0; bVTable[i] ! nullptr; i) {std::cout [ i ] aVTable[i] - 函数执行;void(*func)() (void(*)())(bVTable[i]);func();} 根据虚函数表地址运行虚函数//std::cout Running virtual function using As vtable: std::endl;//void(*aFunc)() (void(*)())(aVTable[0]);//aFunc();//std::cout Running virtual function using Bs vtable: std::endl;//void(*bFunc)() (void(*)())(bVTable[0]);//bFunc();return 0;
}输出 四、经典问题 inline函数可以是虚函数嘛 inline函数可以是虚函数但是其内联的特性也就没有了因为inline只是对编译器的建议。内联函数是在调用的地方展开没有函数地址而虚函数的地址是要写入虚函数表的所以内联函数和虚函数只能为其中的一个不可兼得。 静态成员函数可以是虚函数嘛 不能因为静态成员函数没有this指针使用类名::成员函数的调用方式 无法访问虚函数表所以静态成员函数无法放进虚函数表。 构造函数可以是虚函数嘛 不可以因为虚函数表指针是在构造函数的初始化列表初始化的但是虚函数又要借助虚函数表指针来调用虚函数两者矛盾所以不可以为虚函数。 析构函数可以是虚函数嘛 可以并且建议将析构函数定义为虚函数因为这样可以避免内存泄漏的问题。如果子类对象是动态开辟的使用父类指针指向子类对象在delete时如果构成多态那么就会调用子类析构函数而调用子类析构函数前系统会默认先调用父类析构函数这样可以避免内存泄漏。 对象访问普通函数快还是访问虚函数快 如果是通过实例化的对象访问那么是一样快的如果是指针或引用对象访问的话是访问普通函数快的因为指针或引用去访问虚函数时走的是多态调用是一个晚绑定需要在运行时去需表中找函数的地址。 虚函数表是在什么时候形成的存在哪
和虚函数相关的字符字符在只读数据区.rodata,但是虚函数的实现代码应该是和其他的函数一样存储在代码段.text 什么是抽象类
函数纯虚函数的类叫做抽象类此类不能实例化出对象这也强制了其派生类如果想要实例化出对象那么就必须重写纯虚函数。 C菱形继承解决方案和多态原理 菱形继承具有数据冗余和二义性的问题解决的方法是通过虚继承的方式虚继承的派生类中会产生一个虚基表指针该指针指向虚基表表中的内容是一个到冗余数据的偏移量而原本冗余的数据会被放到派生类对象的最后。 多态的原理是通过重写虚函数达到在派生类的虚函数表中重写的虚函数地址覆盖掉原本的地址然后通过基类的指针或者引用指向派生类对象时调用虚函数调用的时子类重写后的虚函数而执行基类对象时调用的就是基类的虚函数达到多态的行为。 不要将虚基表和虚函数表搞混。