一个类的实例化对象所占空间的大小? 注意不要说类的大小,是类的对象的大小.
首先,类的大小是什么?确切的说,类只是一个类型定义,它是没有大小可言的。 用sizeof运算符对一个类型名操作,得到的是具有该类型实体的大小。
如果 Class A; A obj; 那么sizeof(A)==sizeof(obj) 那么sizeof(A)的大小和成员的大小总和是什么关系呢,很简单,一个对象的大小大于等于所有非静态成员大小的总和。 参照:http://www.myexception.cn/program/1666528.html
上述结果分别是:1,1,4
class A是一个空类型,它的实例不包含任何信息,本来求sizeof应该是0。但当我们声明该类型的实例的时候,它必须在内存中占有一定的空间,否则无法使用这些实例。至于占用多少内存,由编译器决定。Visual Studio 2008中每个空类型的实例占用一个byte的空间。 class B在class A的基础上添加了构造函数和析构函数。由于构造函数和析构函数的调用与类型的实例无关(调用它们只需要知道函数地址即可),在它的实例中不需要增加任何信息。所以sizeof(B)和sizeof(A)一样,在Visual Studio 2008中都是1。 class C在class B的基础上把析构函数标注为虚拟函数。C++的编译器一旦发现一个类型中有虚拟函数,就会为该类型生成虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针。在32位的机器上,一个指针占4个字节的空间,因此sizeof(C)是4。 C++标准规定类的大小不为0,空类的大小为1,当类不包含虚函数和非静态数据成员时,其对象大小也为1。 如果在类中声明了虚函数(不管是1个还是多个),那么在实例化对象时,编译器会自动在对象里安插一个指针指向虚函数表VTable,在32位机器上,一个对象会增加4个字节来存储此指针,它是实现面向对象中多态的关键。而虚函数本身和其他成员函数一样,是不占用对象的空间的。注意要对齐!
等于基类的和,但是要注意字节对齐。不过这个和一般的字节对齐还不一样,比如I,按理说应该是4(1)+4+4(1),但是实际应该是4(1+1)+4。
一个很奇怪的地方就是:
class A { }; class A1 { }; class A2 { }; class A3 { }; class A4 { }; class G :public A{}//sizeof(G)=1 class G :public A, public B{}//sizeof(G)=1 class G :public A, public B{} ,public A1{}//sizeof(G)=1 class G :public A, public B ,public A1, public A2{}//sizeof(G)=3这个结果不明白! ????
class H :public A, public C, public A1, public A2, public A3, public A4, public B {};//sizeof(H)=12 class H :public A, public C, public A1, public A2, public A3, public B {};//sizeof(H)=8大致规律就是继承2个空类时,占1,每多加1个会多1。
class J { private: char a1; int data; char a2; }; cout << "J的大小" << sizeof(J) << endl;//12 这个有字节对齐的。 摘自:http://www.myexception.cn/program/1666528.html //编译器为每个有虚函数的类都建立一个虚函数表(其大小不计算在类中),并为这个类安插一个指向虚函数表的指针,即每个有虚函数的类其大小至少为一个指针的大小4 class A { public: int a; void Function(); }; class B { public: int a; virtual void Function(); }; class C : public B { public: char b; }; class D : public B { public: virtual void Function2(); }; class E { public: static void Function(); }; class staticE { static int intVar; static void fun(){} }; void test1() { cout<<"sizeof(A)="<<sizeof(A)<<endl;//4 (内含一个int,普通函数不占大小) cout<<"sizeof(B)="<<sizeof(B)<<endl;//8 (一个int ,一个虚函数表指针) cout<<"sizeof(C)="<<sizeof(C)<<endl;//12 (一个int ,一个虚函数表指针,一个char ,再加上数据对齐) cout<<"sizeof(D)="<<sizeof(D)<<endl;//8 (一个int ,一个虚函数表指针,多个虚函数是放在一个表里的,所以虚函数表指针只要一个就行了) cout<<"sizeof(E)="<<sizeof(E)<<endl;//1 (static 函数不占大小,空类大小为1) cout<<"sizeof(staticE)="<<sizeof(staticE)<<endl;//1 静态数据成员不计入类内 } class cA { public: virtual void PrintA1(void){} virtual void PrintA2(void){} }; class cB { public: virtual void PrintB(void){} }; class cC { public: virtual void PrintC(void){} }; class cD : public cA, public cB, public cC { }; class cE : public cA, public cB, public cC { public: virtual void PrintE(void){} }; void test2() { cout<<"sizeof(cD)="<<sizeof(cD)<<endl;//12 如果一个类里面什么也不实现,只实现一个或多个虚函数的话,测它的sizeof会得到4,但如果一个类从多个类继承,并且它的多个基类有虚函数的话,它就会有多个虚函数表了,这个在COM也有体现. cout<<"sizeof(cE)="<<sizeof(cE)<<endl;//12 } class dA { int a; virtual ~dA(){} }; class dB:virtual public dA { virtual void myfunB(){} }; class dC:virtual public dA { virtual void myfunC(){} }; class dD:public dB,public dC { virtual void myfunD(){} }; void test3() { cout<<"sizeof(dA)="<<sizeof(dA)<<endl;//8 cout<<"sizeof(dB)="<<sizeof(dB)<<endl;//12 cout<<"sizeof(dC)="<<sizeof(dC)<<endl;//12 cout<<"sizeof(dD)="<<sizeof(dD)<<endl;//16 //解释:A中int+虚表指针。B,C中由于是虚继承因此大小为A+指向虚基类的指针,B,C虽然加入了自己的虚函数,但是虚表指针是和基类共享的,因此不会有自己的虚表指针。D由于B,C都是虚继承,因此D只包含一个A的副本,于是D大小就等于A+B中的指向虚基类的指针+C中的指向虚基类的指针。 //如果B,C不是虚继承,而是普通继承的话,那么A,B,C的大小都是8(没有指向虚基类的指针了),而D由于不是虚继承,因此包含两个A副本,大小为16. 注意此时虽然D的大小和虚继承一样,但是内存布局却不同。 } class T1 { }; class T2 : public T1 { }; class T3 : public T1 { int a; }; class M1 { virtual void GetM()=0; }; class M2 { virtual void GetM2()=0; virtual void Get(); }; class M3 : public M1, public M2 { int a; }; void test4() { cout<<"sizeof(T2)="<<sizeof(T2)<<endl;//1 继承一个空父类,这时sizeof(父类)=0 cout<<"sizeof(T3)="<<sizeof(T3)<<endl;//4 同上 cout<<"sizeof(M1)="<<sizeof(M1)<<endl;// 4 cout<<"sizeof(M2)="<<sizeof(M2)<<endl;// 4 cout<<"sizeof(M3)="<<sizeof(M3)<<endl;// 12 抽象函数与虚函数一样 } void main() { test1(); test2(); test3(); test4(); } /*虚函数表(http://www.uml.org.cn/c++/200811143.asp) C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。 对C++了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。 在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了 这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。 这里我们着重看一下这张虚函数表。C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。 debug时,在局部变量窗口中可以看到,虚函数表 */cout << “K的大小” << sizeof(K) << endl;//4
类K的静态数据成员被编译器放在程序的一个global data members中,它是类的一个数据成员.但是它不影响类的大小,不管这个类实际产生 了多少实例,还是派生了多少新的类,静态成员数据在类中永远只有一个实体存在,而类的非静态数据成员只有被实例化的时候,他们才存在.但是类的静态数据成员一旦被声明,无论类是否被实例化,它都已存在.可以这么说,类的静态数据成员是一种特殊的全局变量.小总结:
从以上的几个例子不难发现类的大小: 1.为类的非静态成员数据的类型大小之和. 2.由编译器额外加入的成员变量的大小,用来支持语言的某些特性(如:指向虚函数的指针). 3.为了优化存取效率,进行的边缘调整(对齐). 4 与类中的构造函数,析构函数以及其他的成员函数无关.自己写的一个例子:
char pp1[] = "a\\\n"; cout << sizeof(pp1) << " " << strlen(pp1) << endl;//4 3(注意\\和\n分别代表\和换行,其实分别表示一个 return 0;一些朋友刚开始时把sizeof当作了求数组元素的个数,现在,你应该知道这是不对的,那么应该怎么求数组元素的个数呢?Easy,通常有下面两种写法: char a1[] = “abc”; int c1 = sizeof( a1 ) / sizeof( char ); // 总长度/单个元素的长度 int c2 = sizeof( a1 ) / sizeof( a1[0] ); // 总长度/第一个元素的长度 void foo3(char a3[3]) { int c3 = sizeof( a3 ); // c3 == 4 } void foo4(char a4[]) { int c4 = sizeof( a4 ); // c4 == 4 } 这里函数参数a3已不再是数组类型,而是蜕变成指针,相当于char* a3,为什么?仔细想想就不难明白,我们调用函数foo1时,程序会在栈上分配一个大小为3的数组吗?不会!数组是“传址”的,调用者只需将实参的地址传递过去,所以a3自然为指针类型(char*),c3的值也就为4。
第四个例子:结构体的sizeof 1为什么需要字节对齐? 计算机组成原理教导我们这样有助于加快计算机的取数速度,否则就得多花指令周期了。为此,编译器默认会对结构体进行处理(实际上其它地方的数据变量也是如此),让宽度为2的基本数据类型(short等)都位于能被2整除的地址上,让宽度为4的基本数据类型(int等)都位于能被4整除的地址上,以此类推。这样,两个数中间就可能需要加入填充字节,所以整个结构体的sizeof值就增长了。 2字节对齐的细节和编译器实现相关,但一般而言,满足三个准则: 1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除; 2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。 struct s1 { char a; double b; int c; char d; }; struct s2 { char a; char b; int c; double d; }; cout<<sizeof(s1)<<endl; // 24 cout<<sizeof(s2)<<endl; // 16 //同样是两个char类型,一个int类型,一个double类型,但是因为对齐问题,导致他们的大小不同。计算结构体大小可以采用元素摆放法,我举例子说明一下:首先,CPU判断结构体的对界,根据上面的结论,s1和s2的对界都取最大的元素类型,也就是double类型的对界8。然后开始摆放每个元素(满足准则1)。 //对于s1,首先把a放到8的对界,假定是0,此时下一个空闲的地址是1,但是下一个元素d是double类型,要放到8的对界上,离1最接近的地址是8了,所以d被放在了8(满足准则2),此时下一个空闲地址变成了16,下一个元素c的对界是4,16可以满足,所以c放在了16,此时下一个空闲地址变成了20,下一个元素d需要对界1,也正好落在对界上,所以d放在了20,结构体在地址21处结束。由于s1的大小需要是8的倍数,所以21-23的空间被保留,s1的大小变成了24(满足准则3)。 //对于s2,首先把a放到8的对界,假定是0,此时下一个空闲地址是1,下一个元素的对界也是1,所以b摆放在1,下一个空闲地址变成了2;下一个元素c的对界是4,所以取离2最近的地址4摆放c,下一个空闲地址变成了8,下一个元素d的对界是8,所以d摆放在8,所有元素摆放完毕,结构体在15处结束,占用总空间为16,正好是8的倍数。 通过上面的叙述,我们可以得到一个公式: 结构体的大小等于最后一个成员的偏移量加上其大小再加上末尾的填充字节数目,即: sizeof( struct ) = offsetof( last item ) + sizeof( last item ) + sizeof( trailing padding ) 2) 基本类型是指前面提到的像char、short、int、float、double这样的内置数据类型,这里所说的“数据宽度”就是指其sizeof的大小。由于结构体的成员可以是复合类型,比如另外一个结构体,所以在寻找最宽基本类型成员时,应当包括复合类型成员的子成员,而不是把复合成员看成是一个整体。但在确定复合类型成员的偏移位置时则是将复合类型作为整体看待。 看下面的例子: struct s1 { char a[8]; }; struct s2 { double d; }; struct s3 { s1 s; char a; }; struct s4 { s2 s; char a; }; cout<<sizeof(s1)<<endl; // 8 cout<<sizeof(s2)<<endl; // 8 cout<<sizeof(s3)<<endl; // 9 cout<<sizeof(s4)<<endl; // 16; s1和s2大小虽然都是8,但是s1的对齐方式是1,s2是8(double),所以在s3和s4中才有这样的差异。 所以,在自己定义结构体的时候,如果空间紧张的话,最好考虑对齐因素来排列结构体里的元素。4)“空结构体”(不含数据成员)的大小不为0,而是1。试想一个“不占空间”的变量如何被取地址、两个不同的“空结构体”变量又如何得以区分呢?于是,“空结构体”变量也得被存储,这样编译器也就只能为其分配一个字节的空间用于占位了。如下: struct S5 { }; sizeof( S5 ); // 结果为1
第五个例子:含位域结构体的sizeof 位域成员不能单独被取sizeof值,我们这里要讨论的是含有位域的结构体的sizeof,只是考虑到其特殊性而将其专门列了出来。 C99规定int、unsigned int和bool可以作为位域类型,但编译器几乎都对此作了扩展,允许其它类型类型的存在。 使用位域的主要目的是压缩存储,其大致规则为: 1) 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止; 2) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍; 3) 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式,Dev-C++采取压缩方式; 4) 如果位域字段之间穿插着非位域字段,则不进行压缩; 5) 整个结构体的总大小为最宽基本类型成员大小的整数倍。 示例1: struct BF1 { char f1 : 3; char f2 : 4; char f3 : 5; }; 其内存布局为: | f1 | f2 | | f3 | | --------------------------------- | | | | | | | | | | | | | | | | | --------------------------------- 0 3 7 8 13 16 (byte) 位域类型为char,第1个字节仅能容纳下f1和f2,所以f2被压缩到第1个字节中,而f3只能从下一个字节开始。因此sizeof(BF1)的结果为2。 示例2: struct BF2 { char f1 : 3; short f2 : 4; char f3 : 5; }; | f1 | f2 | | f3 | | --------------------------------- | | | | | | | | | | | | | | | | | --------------------------------- 0 3 7 8 13 16 (byte) 压缩 f1 f2 不压缩时,首先把f1放到1的对界,假定是0,此时下一个空闲的地址是1,但是下一个元素f2是short类型,要放到2的对界上,离1最接近的地址是2了,所以f2被放在了2(满足准则2),此时下一个空闲地址变成了4,下一个元素 f3的对界是1,4可以满足,所以 f3放在了4,此时下一个空闲地址变成了5,结构体在地址5处结束。由于BF2的大小需要是2的倍数,所以5的空间被保留,BF2的大小变成了6(满足准则3)。 由于相邻位域类型不同,在VC6中其sizeof为6,在Dev-C++中为2。 示例3: struct BF3 { char f1 : 3; char f2; char f3 : 5; }; 非位域字段穿插在其中,不会产生压缩,在VC6和Dev-C++中得到的大小均为3。 补充:不要让double干扰你的位域 在结构体和类中,可以使用位域来规定某个成员所能占用的空间,所以使用位域能在一定程度上节省结构体占用的空间。不过考虑下面的代码: struct s1 { int i: 8; int j: 4; double b; int a:3; }; struct s2 { int i; int j; double b; int a; }; struct s3 { int i; int j; int a; double b; }; struct s4 { int i: 8; int j: 4; int a:3; double b; }; cout<<sizeof(s1)<<endl; // 24 cout<<sizeof(s2)<<endl; // 24 cout<<sizeof(s3)<<endl; // 24 cout<<sizeof(s4)<<endl; // 16 可以看到,有double存在会干涉到位域(sizeof的算法参考上一节),所以使用位域的的时候,最好把float类型和double类型放在程序的开始或者最后。第七个例子:联合体的sizeof 结构体在内存组织上是顺序式的,联合体则是重叠式,各成员共享一段内存,所以整个联合体的sizeof也就是每个成员sizeof的最大值。结构体的成员也可以是复合类型,这里,复合类型成员是被作为整体考虑的。 所以,下面例子中,U的sizeof值等于sizeof(s)。 union U { int i; char c; S1 s; };