`
java-mans
  • 浏览: 11410511 次
文章分类
社区版块
存档分类
最新评论

C++中虚函数工作原理和(虚)继承类的内存占用大小计算

 
阅读更多
一、虚函数的工作原理
每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就会为这个类创建一个虚函数表(VTABLE)保存该类所有虚函数的地址,其实这个VTABLE的作用就是保存自己类中所有虚函数的地址,可以把VTABLE形象地看成一个函数指针数组,这个数组的每个元素存放的就是虚函数的地址。在每个带有虚函数的类 中,编译器秘密地置入一指针,称为v p o i n t e r(缩写为V P T R),指向这个对象的V TA B L E。 当构造该派生类对象时,其成员VPTR被初始化指向该派生类的VTABLE。所以可以认为VTABLE是该类的所有对象共有的,在定义该类时被初始化;而VPTR则是每个类对象都有独立一份的,且在该类对象被构造时被初始化。
通过基类指针做虚函数调 用时(也就是做多态调用时),编译器静态地插入取得这个V P T R,并在V TA B L E表中查找函数地址的代码,这样就能调用正确的函数使晚捆绑发生。为每个类设置V TA B L E、初始化V P T R、为虚函数调用插入代码,所有这些都是自动发生的,所以我们不必担心这些。
#include<iostream>
using namespace std;

class A
{
public:
	virtual void fun1()
	{
		cout << "A::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "A::fun2()" << endl;
	}
};

class B : public A
{
public:
	void fun1()
	{
		cout << "B::fun1()" << endl;
	}
	void fun2()
	{
		cout << "B::fun2()" << endl;
	}
};

int main()
{
	A *pa = new B;
	pa->fun1();
	delete pa;

	system("pause"); 
	return 0;
}
毫无疑问,调用了B::fun1(),但是B::fun1()不是像普通函数那样直接找到函数地址而执行的。真正的执行方式是:首先取出pa指针所指向的对象的vptr的值,这个值就是vtbl的地址,由于调用的函数B::fun1()是第一个虚函数,所以取出vtbl第一个表项里的值,这个值就是B::fun1()的地址了,最后调用这个函数。因此只要vptr不同,指向的vtbl就不同,而不同的vtbl里装着对应类的虚函数地址,所以这样虚函数就可以完成它的任务,多态就是这样实现的。
而对于class A和class B来说,他们的vptr指针存放在何处?其实这个指针就放在他们各自的实例对象里。由于class A和class B都没有数据成员,所以他们的实例对象里就只有一个vptr指针。
含有虚函数的对象在内存中的结构如下:
class A
{
private:
	int a;
	int b;
public:
	virtual void fun0()
	{
		cout<<"A::fun0"<<endl;
	}
};

1、直接继承
那我们来看看编译器是怎么建立VPTR指向的这个虚函数表的,先看下面两个类:
class base
{
private:
	int a;
public:
	void bfun()
	{
	}
	virtual void vfun1()
	{
	}
	virtual void vfun2()
	{
	}
};

class derived : public base
{
private:
	int b;
public:
	void dfun()
	{
	}
	virtual void vfun1()
	{
	}
	virtual void vfun3()
	{
	}
};
两个类的VPTR指向的虚函数表(VTABLE)分别如下:
base类
——————
VPTR——> |&base::vfun1 |
——————
|&base::vfun2 |
——————

derived类
———————
VPTR——> |&derived::vfun1 |
———————
|&base::vfun2 |
———————
|&derived::vfun3 |
———————
每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就为这个类创建一个VTABLE,如上图所示。在这个表中,编译器放置了在这个类中或在它的基类中所有已声明为virtual的函数的地址。如果在这个派生类中没有对在基类中声明为virtual的函数进行重新定义,编译器就使用基类 的这个虚函数地址。(在derived的VTABLE中,vfun2的入口就是这种情况。)然后编译器在这个类中放置VPTR。当使用简单继承时,对于每个对象只有一个VPTR。VPTR必须被初始化为指向相应的VTABLE,这在构造函数中发生。
一旦VPTR被初始化为指向相应的VTABLE,对象就"知道"它自己是什么类型。但只有当虚函数被调用时这种自我认知才有用。
2、虚继承
这个是比较不好理解的,对于虚继承,若派生类有自己的虚函数,则它本身需要有一个虚指针,指向自己的虚表。另外,派生类虚继承父类时,首先要通过加入一个虚指针来指向父类,因此有可能会有两个虚指针。
二、(虚)继承类的内存占用大小
首先,平时所声明的类只是一种类型定义,它本身是没有大小可言的。 因此,如果用sizeof运算符对一个类型名操作,那得到的是具有该类型实体的大小。
计算一个类对象的大小时的规律:
1、空类、单一继承的空类、多重继承的空类所占空间大小为:1(字节,下同);
2、一个类中,虚函数本身、成员函数(包括静态与非静态)和静态数据成员都是不占用类对象的存储空间的;
3、因此一个对象的大小≥所有非静态成员大小的总和;
4、当类中声明了虚函数(不管是1个还是多个),那么在实例化对象时,编译器会自动在对象里安插一个指针vPtr指向虚函数表VTable;
5、虚承继的情况:由于涉及到虚函数表和虚基表,会同时增加一个(多重虚继承下对应多个)vfPtr指针指向虚函数表vfTable和一个vbPtr指针指向虚基表vbTable,这两者所占的空间大小为:8(或8乘以多继承时父类的个数);
6、在考虑以上内容所占空间的大小时,还要注意编译器下的“补齐”padding的影响,即编译器会插入多余的字节补齐;
7、类对象的大小=各非静态数据成员(包括父类的非静态数据成员但都不包括所有的成员函数)的总和+ vfptr指针(多继承下可能不止一个)+vbptr指针(多继承下可能不止一个)+编译器额外增加的字节。
示例一:含有普通继承
class A   
{   
};  

class B   
{
	char ch;   
	virtual void func0()  {  } 
}; 

class C  
{
	char ch1;
	char ch2;
	virtual void func()  {  }  
	virtual void func1()  {  } 
};

class D: public A, public C
{   
	int d;   
	virtual void func()  {  } 
	virtual void func1()  {  }
};   

class E: public B, public C
{   
	int e;   
	virtual void func0()  {  } 
	virtual void func1()  {  }
};

int main(void)
{
	cout<<"A="<<sizeof(A)<<endl;    //result=1
	cout<<"B="<<sizeof(B)<<endl;    //result=8    
	cout<<"C="<<sizeof(C)<<endl;    //result=8
	cout<<"D="<<sizeof(D)<<endl;    //result=12
	cout<<"E="<<sizeof(E)<<endl;    //result=20
	return 0;
}
前面三个A、B、C类的内存占用空间大小就不需要解释了,注意一下内存对齐就可以理解了。
求sizeof(D)的时候,需要明白,首先VPTR指向的虚函数表中保存的是类D中的两个虚函数的地址,然后存放基类C中的两个数据成员ch1、ch2,注意内存对齐,然后存放数据成员d,这样4+4+4=12。
求sizeof(E)的时候,首先是类B的虚函数地址,然后类B中的数据成员,再然后是类C的虚函数地址,然后类C中的数据成员,最后是类E中的数据成员e,同样注意内存对齐,这样4+4+4+4+4=20。
示例二:含有虚继承
class CommonBase
{
	int co;
};

class Base1: virtual public CommonBase
{
public:
	virtual void print1() {  }
	virtual void print2() {  }
private:
	int b1;
};

class Base2: virtual public CommonBase
{
public:
	virtual void dump1() {  }
	virtual void dump2() {  }
private:
	int b2;
};

class Derived: public Base1, public Base2
{
public:
	void print2() {  }
	void dump2() {  }
private:
	int d;
};
sizeof(Derived)=32,其在内存中分布的情况如下:
class Derived size(32):
     +---
     | +--- (base class Base1)
 | | {vfptr}
 | | {vbptr}
 | | b1
     | +---
     | +--- (base class Base2)
 | | {vfptr}
 | | {vbptr}
 | | b2
    | +---
 | d
    +---
    +--- (virtual base CommonBase)
 | co
    +---
示例3:
class A
{
public:
	virtual void aa() {  }
	virtual void aa2() {  }
private:
	char ch[3];
};

class B: virtual public A
{
public:
	virtual void bb() {  }
	virtual void bb2() {  }
};

int main(void)
{
	cout<<"A's size is "<<sizeof(A)<<endl;
	cout<<"B's size is "<<sizeof(B)<<endl;
	return 0;
}
执行结果:A's size is 8
B's size is 16
说明:对于虚继承,类B因为有自己的虚函数,所以它本身有一个虚指针,指向自己的虚表。另外,类B虚继承类A时,首先要通过加入一个虚指针来指向父类A,然后还要包含父类A的所有内容。因此是4+4+8=16。

分享到:
评论

相关推荐

    C++中虚函数工作原理和(虚)继承类的内存占用大小计算1

    2、虚继承 这个是比较不好理解的,对于虚继承,若派生类有自己的虚函数,则它本身需要有一个虚指针,指向自己的虚表 2、一个类中,虚函数本身、成员函数(包括

    C++类的虚函数虚继承所占的空间

     GCC中, 无论是虚函数还是虚继承, 都需要将指针存储在虚函数表(virtual function table), 占用4个字节.  继承会继承基类的数据, 和虚函数表, 即继承基类的空间.  代码: /* * test.cpp * * Created ...

    c++思维导图/很全,附带考点

    ●C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要...

    C++实现员工管理系统.zip

    在虚基类Staff中定义虚函数updateProperty(更新属性值函数)、虚函数output(输出函数)和虚函数outputWithNumber(输出函数),并在派生类中进行了覆盖以达到动态多态性。 详细介绍参考:...

    面向对象与C++试题.doc

    2、在C++中,三种继承方式的说明符号为 、 和 ,如果不加说明,则默认的继承方式为 。 3、如果只想保留公共基类的一个复制,就必须使用关键字 把这个公共基类声明为虚基类。 4、若要把void fun( )定义为类A的友元...

    muluoleiguo#interview#虚基类1

    虚继承、虚函数对比相同之处:都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)不同之处:虚继承虚基类依旧存在继承类中,只占用存储空间虚基类表存储

    c++ 面试题 总结

    各种内存管理都有它自己的方法来计算出程序片断在主存中的物理地址,其实都很相似。 这只是一个大概而已,不足以说明内存管理的皮毛。无论哪一本操作系统书上都有详细的讲解 -----------------------------------...

    java 面试题 总结

    引用类型和原始类型具有不同的特征和用法,它们包括:大小和速度问题,这种类型以哪种类型的数据结构存储,当引用类型和原始类型用作某个类的实例数据时所指定的缺省值。对象引用实例变量的缺省值为 null,而原始...

    超级有影响力霸气的Java面试题大全文档

    引用类型和原始类型具有不同的特征和用法,它们包括:大小和速度问题,这种类型以哪种类型的数据结构存储,当引用类型和原始类型用作某个类的实例数据时所指定的缺省值。对象引用实例变量的缺省值为 null,而原始...

    java面试题

    答:折构函数式销毁一个类的函数,虚函数是为了C++的动态绑定而设计的。 描述你的编程风格? 答:类名首字母大写,常量一般全部大写,给自己的代码加注释。 控制流程? 答:控制流程一般使用if判断条件。有第二分支...

    PT80-NEAT开发指南v1.1

    窗口类 CNeatView 视图类 ................................................................................................................... 26 CNeatControl 类 ............................................

Global site tag (gtag.js) - Google Analytics