C++ 多态实现的原理

<在网上看了很多文档后,自己对多态的实现原理有了一定的理解,本部分内容算是根据自己的理解来解释一下多态实现的原理吧,一方面为了加深记忆,另一面万一有理解不对的地方,也好有一个被别人发现和更正的机会。这也是一开始通过博客记笔记的初衷>。

一、类的大小
首先简单讲一下类的内存大小计算

类的内存大小与两个因素有关:
1、类的普通成员变量,虚函数,继承
2、字节对齐详见之前写的这篇文章

注意:类的大小与静态数据成员、成员函数(无论是静态函数非静态)无关

另外比较特殊的是空类的大小并不为0,而是为1.这是因为空类尽管没有任何成员,但是仍然可以实例化一个空类的对象,该对象就会在内存占据大小,那么编译器就会将空类内存大小优化为1个字节。

1、首先来举一个没有虚函数,没有继承的栗子:

class A {
};

class B {
public:
	void func1() {};
	static void func2() {};
private:
	int a;
	char c;
	static double d;
};

int main()
{
	cout << sizeof(A) << endl;  //输出为1
	cout << sizeof(B) << endl;  //输出为8 
    return 0;
}

注:本人电脑为32位,默认的对齐系数为4

如上述代码所示:首先A类为空类,因此大小为1,B类中与内存大小有关的只有int型的a和char型的c,一个int型占4字节,一个char型占1字节,加上字节对齐的作用,最终有sizeof(B)=5+3=8。(之所以写成5+3,是因为后面3个字节是编译器自动补上的。可以参考之前关于字节对齐的笔记(字节对齐))

2、接下来看一个含有虚函数的但是没有继承的栗子:

class C {
public:
	virtual void func() {};
};

int main()
{
	cout << sizeof(A) << endl;  //输出为4
    return 0;
}

如上述代码所示:类C只有一个虚函数,没有其他成员,但是通过sizeof©得到C类的内存大小为4。显然这与虚函数的存在有关。实际上含有虚函数的类会生成一个虚函数表,并且生成一个指向该表的虚函数表指针,上述例子C类的内存大小4其实就是C类中虚函数表指针的大小。

二。虚函数表及虚函数指针
当一个类含有虚函数时,或者某个派生类的基类含有虚函数时,编译器会为该类创建一个虚函数表(vtable)以保存所有虚函数地址,实际上虚函数表保存的是所有虚函数指针,因此虚函数表其实是一个保存函数指针的数组。vtable为该类的所有对象所共有,并且在类定义时被初始化。另外该类的每个对象还含有一个指向该虚函数表的指针,称为虚指针(vptr),虚指针在对象被创建时被初始化指向该类的vtable。

1、举个栗子:

class C {
public:
	virtual void func1() {};
	virtual void func2(){};
	int a;
};
//sizeof(C)=8; 

对于C类的对象的内存空间布局如下:
在这里插入图片描述

指向虚函数表的虚指针必须位于对象内存布局的最前面,这样可以准确取到虚函数的偏移量。

根据上面的内存布局图就可以解释为什么sizeof©=8,因为虚函数指针vptr占4个字节,int型a占4个字节,所以类对象的内存大小为8字节。

2、虚函数表具有继承性,基类可以把自己的虚函数表继承给派生类。接下来看一个派生类对象的内存布局。
①、如果派生类中没有虚函数,如:

class Base{
public:
virtual void func1(){};
virtual void func2(){};
int b;
};
class A:public Base{
public:
char a1;
int a2;
};
//sizeof(A)=16

如上述代码所示,那么类A对象的内存布局如下所示:
在这里插入图片描述

同样,首先是指向虚函数表的虚指针,然后是基类的数据成员b,接着是A类本身的数据成员a1和a2。至于为什么sizeof(A)=16,这就是字节对齐的作用了,由于存在字节对齐规则,实际上类A对象中各个成员所占字节空间示意图如下:
在这里插入图片描述
至于为什么A::a1后面要自动填充3个字节,因为要满足后面A::a2首地址偏移量的要求啊~(字节对齐的内容就不在展开了)

②、如果派生类中含有虚函数,如:

class Base {
public:
	virtual void func1() {};
	virtual void func2() {};
	int b;
};
class A :public Base {
      public:
		      void func1() {};  //覆盖了基类中的虚函数func1
		  virtual void func3() {};  //派生类自己定义的虚函数func3.
		char c;
	    int a;
};
//sizeof(A)=16

此时类A对象的内存空间布局示意图如下:
在这里插入图片描述
如上所示:
首先基类的虚函数放在派生类虚函数的前面,如果派生类覆盖了基类的某个虚函数,那么在虚函数表中该虚函数位置换成派生类版本的虚函数,如图中的函数func1()。

因此基类与派生类的虚函数表分别如下:
在这里插入图片描述
当我们使用基类的指针或者引用调用func1()时,可以理解为通过指针所指向对象的vptr遍历相应的虚函数表,找到func1()函数,由于不同派生类对象的虚函数表不同,因此通过基类指针或者引用调用不同派生类对象中的虚函数也不同,这就是多态。

3、接下来看一个多继承的栗子。对于多继承,派生类同样会继承每一个基类中虚函数表,并且派生类对象中包含指向每一个虚函数表的虚指针。如果派生类定义了自己的虚函数,那么该虚函数位于第一个基类的虚函数表后面。如:

class Base1 {
public:
	virtual void func1() {};
	virtual void func2() {};
	int b1;
};

class Base2 {
public:
	virtual void func1() {};
	virtual void func4() {};
	int b2;
};

class Base3 {
public:
	virtual void func1() {};
	virtual void func5() {};
	char b3;
};

class A :public Base1,public Base2,public Base3 {
      public:
		      void func1() {};
		  virtual void func3() {};
		char a1;
	    int a2;
};
//sizeof(A)=32

上述代码中,类A对象的内存分布空间示意图如下:
在这里插入图片描述
如上所示:派生类的虚函数位于第一个基类的虚函数表中,如果派生类覆盖了每个基类中的虚函数,那么每个基类虚函数表中相应的虚函数替换成派生类版本的虚函数,如上述例子中的func1()函数。

此时派生类的内存大小等于各个基类大小加上派生类自身数据成员大小,由于有:
sizeof(Base1)=8;
sizeof(Base2)=8;
sizeof(Base3)=8;
另外派生类中的数据成员是一个char型和一个int型,因此派生类中数据成员大小为5,因此派生类A的内存大小为8+8+8+5=29,由于字节对齐规则,最终得到sizleof(A)=32。

学习C++越深入 ,越能感受到c++的巧妙啊~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值