C++ 虚函数表与多态 —— 虚函数表的内存布局
1. 多态的本质:
形式上,使用统一的父类指针做一般性处理。但是实际执行时,这个指针可能指向子类对象。
形式上,原本调用父类的方法,但是实际上会调用子类的同名方法。
坦白的说,多态就是为了通过使用父类的指针,能够调用父类与子类他们各自的方法。如果不使用多态,用父类指针调用子类的方法时,也会调用到父类的方法。
【注意】
程序执行时,父类指针指向父类对象,或子类对象时,在形式上是无法分辨的。只有通过多态机制,才能执行真正对应的方法。
2. 虚函数:
在父类的方法函数前,增加 virtual 便可以使这个函数变为虚函数,如:
需要注意一点,例子用的是内联函数,封装到外部时,具体方法实现前不用加 virtual,用了会出错。
1 class Father 2 { 3 public: 4 virtual void play() //父类的 play() 方法前增加 virtual 关键字,这个函数便成为了虚函数 5 { 6 std::cout << "这是个父类的play" << std::endl; 7 } 8 }; 9 10 class Son : public Father 11 { 12 public: 13 void play() 14 { 15 std::cout << "这是个子类的Play" << std::endl; 16 } 17 };
3. 虚函数的继承:
如果某个成员函数被声明为虚函数,那么它的子类【派生类】中所继承的成员函数,也会变为虚函数。
如果在子类中重写这个虚函数,可以不用再写 virtual ,但是仍建议写上 virtual,这样会使代码更可读,如13行:
1 class Father 2 { 3 public: 4 virtual void play() //父类的 play() 方法前增加 virtual 关键字,这个函数便为虚函数 5 { 6 std::cout << "这是个父类的play" << std::endl; 7 } 8 }; 9 10 class Son : public Father 11 { 12 public: 13 virtual void play() //派生类继承的虚函数前,可以不加 virtual,但加上会使代码更加可读 14 { 15 std::cout << "这是个子类的Play" << std::endl; 16 } 17 };
4. 虚函数表的原理 & 对象内存空间:
虚函数的原理是通过虚函数表来实现的,虚函数表是编译器搞出来的东西他并不存在于对象中,先看下边代码:
1 #include <iostream> 2 using namespace std; 3 4 class Father 5 { 6 public: 7 virtual void func_1() { cout << "Father::func_1" << endl; } 8 virtual void func_2() { cout << "Father::func_2" << endl; } 9 virtual void func_3() { cout << "Father::func_3" << endl; } 10 }; 12 13 int main(void) 14 { 15 Father father_1; //虚函数表就保存在这个 father 对象里边 16 17 cout << "sizeof(father_1)=="<< sizeof(father_1) << endl; 18 19 }
运行后打印一下,看看 father 对象占用多大内存空间。
运行结果:sizeof(father_1)==4
3个虚函数为什么只占4个字节?因为他存的是一张表,他没有占用对象的内存空间,对象中只存在一个指针,指向一个虚函数表,如下方示意图:
不管你有多少个虚函数,他都在虚函数表里,并且同类下多个对象也会指向同一个虚函数表。
对象内,首先存储的是“虚函数表指针”,又称为“虚表指针”。
然后存储的是非静态数据成员。
对象的非虚函数保存在类的代码中。
对象的内存,只储存虚函数表和数据成员。(类的静态数据成员保存在数据区中,和对象是分开储存的)
添加虚函数后,对象的内存空间不变,仅虚函数表表中添加条目,同类下的多个对象,共享同一个虚函数表。
下面用代码打印对象中的各个元素的地址来了解下:
1 #include <iostream> 2 using namespace std; 3 4 class Father 5 { 6 public: 7 virtual void func_1() { cout << "Father::func_1" << endl; } 8 virtual void func_2() { cout << "Father::func_2" << endl; } 9 virtual void func_3() { cout << "Father::func_3" << endl; } 10 void func_4() { cout << "非虚函数:Father::func_4" << endl; } //它不存在与对象中 11 12 public: 13 int x = 666; 14 int y = 888; 15 }; 16 17 typedef void(*func_t)(void); //定义一个函数指针类型,返回类型void,参数也是void,给 33 行进行函数类型转换 18 19 int main(void) 20 { 21 Father father; //虚函数表就保存在这个 father 对象里边 22 23 cout << "sizeof(father)=="<< sizeof(father) << endl; 24 25 cout << "对象地址:" << (int*)&father << endl; //转换为int类型的指针,会打印出十六进制的地址 26 27 int* vptr = (int*)*(int*)(&father); //取到虚函数表的地址 28 //第一个 (int*) 仅仅是为了让编译器通过,因为 *(int*)(&father) 取出来的是一个整数,而接受类型是 int* 29 //中间的 * 号,取 father 对象地址中的内容 30 //第二个 (int*) 是强转为 int* 后取地址,不强转类型会不匹配 31 32 cout << "通过虚函数表指针调用第一个虚函数:"; 33 ((func_t) * (vptr + 0))(); //vptr 是虚函数表的地址,加*号取内容,访问到第一个虚函数,但这时他是一个地址,我们需要给他强转为函数 34 35 cout << "\n通过虚函数表指针调用第二个虚函数:"; 36 ((func_t) * (vptr + 1))(); 37 38 cout << "\n通过虚函数表指针调用第三个虚函数:"; 39 ((func_t) * (vptr + 2))(); 40 41 cout << "\n查看其他成员地址:" << endl; 42 cout << "访问方式一:数据成员 x 的地址:" << &father.x << endl; 43 cout << "访问方式二:数据成员 x 的地址:" << std::hex << (int)&father + 4 << endl; 44 45 cout << "\n\n第一个数据成员地址与对象地址相差:" << (char)&father.x - (char)(int*)&father << endl; 46 47 //方式二:取father的地址,转成int类型后+4个字节访问对象的第2个数据成员,然后再把地址值转成指针,访问里边的数据 48 cout << "\n第一个数据成员 x 的值:" << endl; 49 cout << "访问方式一:" << std::dec << father.x << endl; 50 cout << "访问方式二:" << *(int*)((int)&father + 4) << endl; 51 52 cout << "\n第二个数据成员 y 的值:" << endl; 53 cout << "访问方式一:" << std::dec << father.y << endl; 54 cout << "访问方式二:" << *(int*)((int)&father + 8) << endl; 55 }
打印结果:
sizeof(father)==12
对象地址:0033F994
通过虚函数表指针调用第一个虚函数:Father::func_1
通过虚函数表指针调用第二个虚函数:Father::func_2
通过虚函数表指针调用第三个虚函数:Father::func_3
查看其他成员地址:
访问方式一:数据成员 x 的地址:0033F998
访问方式二:数据成员 x 的地址:33f998
第一个数据成员地址与对象地址相差:4
第一个数据成员 x 的值:
访问方式一:666
访问方式二:666
第二个数据成员 y 的值:
访问方式一:888
访问方式二:888
如果觉得上边方法太过于麻烦,那么你可以使用VS编译器来打印内存布局,方法如下:
项目的命令行配置中添加: /d1 reportSingleClassLayoutFather
项目属性 -> 配置属性 -> C/C++ -> 命令行
编译代码后的输出打印:
===========================================================================================================================