前言
大家知道,C++是一门面向对象的语言,其中最重要的三个特性就是,封装,继承和多态。相信对于这三个特性,小伙伴们肯定都会有所了解。但是,今天这篇文章并不是要说这三个特性,而是简单聊一聊虚函数以及在实现继承后,基类和派生类对象的内存分布。
一
虚函数是类的成员函数,使用关键字virtual进行修饰,与其他普通成员函数进行区分。
注意:虚函数中有一种特殊的函数,叫做纯虚函数,拥有纯虚函数的类是不可以实例化对象的,在被继承后的子类必须重写这个纯虚函数。
class A { public: virtual int fun1(); //虚函数 void int fun2(); //普通成员函数};class B { public: virtual int func1() = 0; //纯虚函数}
二
当一个类中拥有虚函数,意味着类的对象拥有一个指向虚函数表的指针(vPtr),简称虚表指针,指针中存储的是虚函数表(vTable)的地址。虚函数表是一个存储类中所有虚函数地址的数组,根据下标进行偏移,从而获取每个虚函数的地址。
例如,当我们定义如下几个类
//1. 只含有2个成员变量class A {public: int data1; int data2;};//2. 含有1个普通成员函数和2个成员变量class B {public: int data1; int data2; void func1() {} };//3. 含有1个虚函数和2个成员变量class C {public: int data1; int data2; virtual int func1() {}};
如上面代码所示,当一个类中没有虚函数时,这个类的大小就等于所有成员变量加起来的大小(注意内存对齐)。而当类中定义了虚函数后,它的类的大小与上面两个类A,B的大小还一样吗?
接下来我们来看类A,类B,类C的对象的内存分布图。
![f22febb91da34036d107f694b4f417e0.png](https://i-blog.csdnimg.cn/blog_migrate/b006f1644950f119ee933c3f5b91dc19.jpeg)
class A
![b18a700fe3e7539666f9b53e9b8e02e5.png](https://i-blog.csdnimg.cn/blog_migrate/82b972438b6f4fbff360ebf809b5e539.jpeg)
class B
![93ec5acd8ee37a7a9632399fd9495a89.png](https://i-blog.csdnimg.cn/blog_migrate/01b0cf6d7fa525d21732eec0545788db.jpeg)
class C
由上面上面3张图可知,在32位系统下,类A的对象大小为8,类B的对象大小为8,类C的对象大小为12。
可能看到这里,有的小伙伴会想到,如果一个类中有2个甚至更多的虚函数,那么是不是也会有对应n个虚表指针?
其实不然,我们之前说了,对象里的虚表指针指向的是一个存储本类中所有虚函数地址的数组,所以只有有1个虚表指针。
class A {public: int data1; int data2; virtual int func1() {} virtual int func2() {} };
![57988e2892116dfde6338c0bf68bcc34.png](https://i-blog.csdnimg.cn/blog_migrate/c3437991b785e311684223288244461a.jpeg)
class A
如上图所示,类A的大小依然等于12.。
三
接下来我们讲一讲当存在单继承时,派生类的对象内存分布是怎么样的。
1. 当基类没有虚函数,派生类继承基类
class Base1 { public: int base1_data1; int base1_ata2; void base1_func1() {}};class Child : public Base1 { public: int child_data1; int child_data2;};
![77284d51cefa0e728f34681ca83f2fff.png](https://i-blog.csdnimg.cn/blog_migrate/263eba6c0260ea11695b17e5c65499cf.jpeg)
由上图可知,由于派生类继承基类,所以先构建基类对象,再构建派生类对象。所以基类的成员变量在前面,派生类的成员变量在后面。
2. 当基类中有虚函数,派生类本身没有虚函数,并且派生类没有重写基类虚函数
class Base1 { public: int base1_data1; int base1_data2; virtual void base1_func1() {} virtual void base1_func2() {}};class Child : public Base1 { public: int child_data1; int child_data2; void child_func1() {} };
![ac6a7c95b464adccecb51b05f8433b24.png](https://i-blog.csdnimg.cn/blog_migrate/e424bcc15fa3c7c2a6230987369c3697.jpeg)
由上图可知,派生类虽然本身没有虚函数,但是它从基类继承过来了两个虚函数。所以派生类的对象也会拥有一个虚表指针,指向的是存储基类虚函数地址的虚表地址。
3. 当基类中有虚函数,派生类本身没有虚函数,并且派生类重写基类虚函数
class Base1 { public: int base1_data1; int base1_data2; virtual void base1_func1() {} virtual void base1_func2() {}};class Child : public Base1 { public: int child_data1; int child_data2; void child_func1() {} void base1_func1() override {} //重写基类的虚方法 };
![08e7fb51e69675e6970878dfdd1a6a09.png](https://i-blog.csdnimg.cn/blog_migrate/b0f894835a247cd5f9c93040026a38d2.jpeg)
由上图可知,派生类重写了基类的base1_func1函数,所以派生类中的虚表指针指向的虚函数表中,将把基类原有的虚函数覆盖掉,这也就是多态实现的原理。
4. 当基类中有虚函数,派生类本身也有虚函数
class Base1 { public: int base1_data1; int base1_data2; virtual void base1_func1() {} virtual void base1_func2() {}};class Child : public Base1 { public: int child_data1; int child_data2; virtual void child_func1() {} void base1_func1() override {} //重写基类的虚方法 };
![190f57a2777a83bb167be7404ed755c1.png](https://i-blog.csdnimg.cn/blog_migrate/e8094a10ea77f2c2188943d2ed8ffa36.jpeg)
由上图可知,虽然派生类本身拥有自身的虚函数,并且从基类那里继承了基类的虚函数,但是派生类的对象也只是有1个虚表指针。虚表指针指向的虚函数表的第一个元素,是重写的基类的虚函数,第二个是没有覆盖的基类虚函数,第三个元素是派生类自身的虚函数地址。所以,如果派生类自身还有第二个,第三个虚函数,也是依次在虚函数表中进行存储。
四
最后我们来讲一讲当存在多继承时,派生类的对象内存分布是怎么样的。
1. 当存在两个基类,base1和base2,并且派生类自身也拥有虚函数
class Base1 { public: int base1_data1; int base1_data2; virtual void base1_func1() {} virtual void base1_func2() {}};class Base2 { public: int base2_data1; int base2_data2; virtual void base2_func1() {} virtual void base2_func2() {}};class Child : public base1, public base2 { public: int child_data1; int child_data2; virtual child_func1() {} void base1_func1() override {} //重写base1的虚函数 void base2_func1() override {} //重写base2的虚函数};
![a30917da328fd225175b19fb50e58a7f.png](https://i-blog.csdnimg.cn/blog_migrate/b78daf48c29be2daf6243b2361f7e728.jpeg)
由上图可知,派生类child按照顺序继承base1和base2。由于先继承base1,所以child本身的虚函数与base1中的虚函数一起将地址存储同一个虚函数表中,由第一个虚表指针指向;然后再存储base1的成员变量;接下来继承base2,需要一个新的虚表指针来指向base2的存储虚函数地址的虚表,接下来再存储base2的成员变量,最后是自身的成员变量。
所以在多继承中,派生类自身虚函数的地址是放在存储第一个继承的基类的虚表中。
2. 如果有2个基类,一个有虚函数,另一个没有,派生类自身有虚函数
class Base1 {public: int base1_data1; int base1_data2; void base1_func1() {} void base1_func2() {}};class Base2 {public: int base2_data1; int base2_data2; virtual void base2_func1() {} virtual void base2_func2() {} };class Child : public Base1, public Base2 {public: int child_data1; int child_data2; virtual child_func1() {} virtual child_func2() {} void base2_func1() override; //重写base2的虚函数};
![30041750f90db0cf850ace0b2e516926.png](https://i-blog.csdnimg.cn/blog_migrate/f0131399b6be4800f83d8ae890521f3b.jpeg)
由上图可知,派生类按顺序先继承Base1,再继承Base2,但是由于Base1中没有虚函数,Base2中拥有虚函数,派生类中也拥有自身的虚函数。所以派生类对象中依然存在虚表指针,并且虚表指针依然靠前,虚表指针指向的虚表中分别存储了被派生类覆盖的Base2_func1,从Base2继承过来的Base2_func2,以及两个自身的虚函数的函数地址。
总结
以上主要从单个类,单继承和多继承三个方面阐述了类的对象内存分布,并且每个方面都考虑了多种情况,希望大家读后能够对大家有所帮助。
总之,C++是一门高深的语言,我们都不断的在学习的过程中,望共勉之。
上面如有概念错误之处,烦请指正,谢谢!
最后,喜欢的小伙伴麻烦点个关注,以后定期会发一些更多的文章,谢谢!