c++ 虚函数_C++虚函数继承之对象内存分布

前言

大家知道,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

class A

b18a700fe3e7539666f9b53e9b8e02e5.png

class B

93ec5acd8ee37a7a9632399fd9495a89.png

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

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

由上图可知,由于派生类继承基类,所以先构建基类对象,再构建派生类对象。所以基类的成员变量在前面,派生类的成员变量在后面。

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

由上图可知,派生类虽然本身没有虚函数,但是它从基类继承过来了两个虚函数。所以派生类的对象也会拥有一个虚表指针,指向的是存储基类虚函数地址的虚表地址。

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

由上图可知,派生类重写了基类的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

由上图可知,虽然派生类本身拥有自身的虚函数,并且从基类那里继承了基类的虚函数,但是派生类的对象也只是有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

由上图可知,派生类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

由上图可知,派生类按顺序先继承Base1,再继承Base2,但是由于Base1中没有虚函数,Base2中拥有虚函数,派生类中也拥有自身的虚函数。所以派生类对象中依然存在虚表指针,并且虚表指针依然靠前,虚表指针指向的虚表中分别存储了被派生类覆盖的Base2_func1,从Base2继承过来的Base2_func2,以及两个自身的虚函数的函数地址。

总结

以上主要从单个类,单继承和多继承三个方面阐述了类的对象内存分布,并且每个方面都考虑了多种情况,希望大家读后能够对大家有所帮助。

总之,C++是一门高深的语言,我们都不断的在学习的过程中,望共勉之。

上面如有概念错误之处,烦请指正,谢谢!

最后,喜欢的小伙伴麻烦点个关注,以后定期会发一些更多的文章,谢谢!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值