本文主要包括二个部分,第一部分重点介绍在VC中,怎么样采用sizeof来求结构的大小,以及容易出现的问题,并给出解决问题的方法,第二部分总结出VC中sizeof的主要用法。 1 、 sizeof应用在结构上的情况请看下面的结构: struct MyStruct ... {double dda1;char dda;int type} ;对结构MyStruct采用sizeof会出现什么结果呢? sizeof (MyStruct)为多少呢?也许你会这样求: sizeof (MyStruct) = sizeof ( double ) + sizeof ( char ) + sizeof ( int ) = 13 但是当在VC中测试上面结构的大小时,你会发现sizeof(MyStruct)为16。你知道为什么在VC中会得出这样一个结果吗? 其实,这是VC对变量存储的一个特殊处理。为了提高CPU的存储速度,VC对一些变量的起始地址做了“对齐”处理。在默认情况下,VC规定各成员变量存放的起始地址相对于结构的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数。下面列出常用类型的对齐方式(vc6. 0 ,32位系统)。类型对齐方式(变量存放的起始地址相对于结构的起始地址的偏移量)Char偏移量必须为sizeof( char )即1的倍数 int 偏移量必须为sizeof( int )即4的倍数 float 偏移量必须为sizeof( float )即4的倍数 double 偏移量必须为sizeof( double )即8的倍数Short偏移量必须为sizeof( short )即2的倍数各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节VC会自动填充。同时VC为了确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节。下面用前面的例子来说明VC到底怎么样来存放结构的。 struct MyStruct ... {double dda1; char dda;int type;} ;为上面的结构分配空间的时候,VC根据成员变量出现的顺序和对齐方式,先为第一个成员dda1分配空间,其起始地址跟结构的起始地址相同(刚好偏移量0刚好为sizeof( double )的倍数),该成员变量占用sizeof( double ) = 8个字节;接下来为第二个成员dda分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为8,是sizeof( char )的倍数,所以把dda存放在偏移量为8的地方满足对齐方式,该成员变量占用sizeof( char ) = 1个字节;接下来为第三个成员type分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为9,不是sizeof( int ) = 4的倍数,为了满足对齐方式对偏移量的约束问题,VC自动填充3个字节(这三个字节没有放什么东西),这时下一个可以分配的地址对于结构的起始地址的偏移量为12,刚好是sizeof( int ) = 4的倍数,所以把type存放在偏移量为12的地方,该成员变量占用sizeof( int ) = 4个字节;这时整个结构的成员变量已经都分配了空间,总的占用的空间大小为: 8 + 1 + 3 + 4 = 16 ,刚好为结构的字节边界数(即结构中占用最大空间的类型所占用的字节数sizeof( double ) = 8 )的倍数,所以没有空缺的字节需要填充。所以整个结构的大小为: sizeof (MyStruct) = 8 + 1 + 3 + 4 = 16 ,其中有3个字节是VC自动填充的,没有放任何有意义的东西。下面再举个例子,交换一下上面的MyStruct的成员变量的位置,使它变成下面的情况: struct MyStruct ... {char dda;double dda1; int type} ;这个结构占用的空间为多大呢?在VC6.0环境下,可以得到sizeof(MyStruc)为24。结合上面提到的分配空间的一些原则,分析下VC怎么样为上面的结构分配空间的。(简单说明) struct MyStruct ... {char dda;//偏移量为0,满足对齐方式,dda占用1个字节;double dda1;//下一个可用的地址的偏移量为1,不是sizeof(double)=8//的倍数,需要补足7个字节才能使偏移量变为8(满足对齐//方式),因此VC自动填充7个字节,dda1存放在偏移量为8//的地址上,它占用8个字节。int type;//下一个可用的地址的偏移量为16,是sizeof(int)=4的倍//数,满足int的对齐方式,所以不需要VC自动填充,type存//放在偏移量为16的地址上,它占用4个字节。} ; // 所有成员变量都分配了空间,空间总的大小为1+7+8+4=20,不是结构 // 的节边界数(即结构中占用最大空间的类型所占用的字节数sizeof // (double)=8)的倍数,所以需要填充4个字节,以满足结构的大小为 // sizeof(double)=8的倍数。 所以该结构总的大小为: sizeof (MyStruc)为1 + 7 + 8 + 4 + 4 = 24 。其中总的有7 + 4 = 11个字节是VC自动填充的,没有放任何有意义的东西。VC对结构的存储的特殊处理确实提高CPU存储变量的速度,但是有时候也带来了一些麻烦,我们也屏蔽掉变量默认的对齐方式,自己可以设定变量的对齐方式。VC中提供了 #pragma pack(n)来设定变量以n字节对齐方式。n字节对齐就是说变量存放的起始地址的偏移量有两种情况:第一、如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式,第二、如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。结构的总大小也有个约束条件,分下面两种情况:如果n大于所有成员变量类型所占用的字节数,那么结构的总大小必须为占用空间最大的变量占用的空间数的倍数; 否则必须为n的倍数。下面举例说明其用法。 #pragma pack(push) // 保存对齐状态 #pragma pack(4) // 设定为4字节对齐 struct test ... {char m1;double m4;int m3;} ; #pragma pack(pop) // 恢复对齐状态 以上结构的大小为16,下面分析其存储情况,首先为m1分配空间,其偏移量为0,满足我们自己设定的对齐方式(4字节对齐),m1占用1个字节。接着开始为m4分配空间,这时其偏移量为1,需要补足3个字节,这样使偏移量满足为n = 4的倍数(因为sizeof( double )大于n),m4占用8个字节。接着为m3分配空间,这时其偏移量为12,满足为4的倍数,m3占用4个字节。这时已经为所有成员变量分配了空间,共分配了16个字节,满足为n的倍数。如果把上面的 #pragma pack(4)改为#pragma pack(16),那么我们可以得到结构的大小为24。(请读者自己分析) 2 、 sizeof用法总结在VC中,sizeof有着许多的用法,而且很容易引起一些错误。下面根据sizeof后面的参数对sizeof的用法做个总结。A. 参数为数据类型或者为一般变量。例如sizeof( int ), sizeof ( long )等等。这种情况要注意的是不同系统系统或者不同编译器得到的结果可能是不同的。例如int类型在16位系统中占2个字节,在32位系统中占4个字节。B. 参数为数组或指针。下面举例说明. int a[ 50 ]; // sizeof(a)=4*50=200; 求数组所占的空间大小 int * a = new int [ 50 ]; // sizeof(a)=4; a为一个指针,sizeof(a)是求指针 // 的大小,在32位系统中,当然是占4个字节。 C. 参数为结构或类。Sizeof应用在类和结构的处理情况是相同的。但有两点需要注意,第一、结构或者类中的静态成员不对结构或者类的大小产生影响,因为静态变量的存储位置与结构或者类的实例地址无关。第二、没有成员变量的结构或类的大小为1,因为必须保证结构或类的每一个实例在内存中都有唯一的地址。下面举例说明,Class Test ... {int a;static double c} ; // sizeof(Test)=4. Test * s; // sizeof(s)=4,s为一个指针。 Class test1 ... { } ; // sizeof(test1)=1; D. 参数为其他。下面举例说明。 int func( char s[ 5 ]); ... {cout<//数组的参数在传递的时候系统处理为一个指针,所//以sizeof(s)实际上为求指针的大小。return 1;} sizeof (func(“ 1234 ”)) = 4 // 因为func的返回类型为int,所以相当于 // 求sizeof(int). 以上为sizeof的基本用法,在实际的使用中要注意分析VC的分配变量的分配策略,这样的话可以避免一些错误。 class 和 struct 的问题是类似的 /**/ // 另外一篇文章 C / C ++ 语言struct深层探索 作者:宋宝华 出处:PConline 1 . struct的巨大作用 面对一个人的大型C / C ++ 程序时,只看其对struct的使用情况我们就可以对其编写者的编程经验进行评估。因为一个大型的C / C ++ 程序,势必要涉及一些(甚至大量)进行数据组合的结构体,这些结构体可以将原本意义属于一个整体的数据组合在一起。从某种程度上来说,会不会用struct,怎样用struct是区别一个开发人员是否具备丰富开发经历的标志。 在网络协议、通信控制、嵌入式系统的C / C ++ 编程中,我们经常要传送的不是简单的字节流(char型数组),而是多种数据组合起来的一个整体,其表现形式是一个结构体。 经验不足的开发人员往往将所有需要传送的内容依顺序保存在char型数组中,通过指针偏移的方法传送网络报文等信息。这样做编程复杂,易出错,而且一旦控制方式及通信协议有所变化,程序就要进行非常细致的修改。 一个有经验的开发者则灵活运用结构体,举一个例子,假设网络或控制协议中需要传送三种报文,其格式分别为packetA、packetB、packetC: struct structA ... {int a;char b;} ; struct structB ... {char a;short b;} ; struct structC ... {int a;char b;float c;} 优秀的程序设计者这样设计传送的报文: struct CommuPacket ... {int iPacketType; //报文类型标志union //每次传送的是三种报文中的一种,使用union...{ struct structA packetA; struct structB packetB; struct structC packetC;}} ; 在进行报文传送时,直接传送struct CommuPacket一个整体。 假设发送函数的原形如下: // pSendData:发送字节流的首地址,iLen:要发送的长度 Send( char * pSendData, unsigned int iLen);发送方可以直接进行如下调用发送struct CommuPacket的一个实例sendCommuPacket:Send( ( char * ) & sendCommuPacket , sizeof (CommuPacket) );假设接收函数的原形如下: // pRecvData:发送字节流的首地址,iLen:要接收的长度 // 返回值:实际接收到的字节数 unsigned int Recv( char * pRecvData, unsigned int iLen); 接收方可以直接进行如下调用将接收到的数据保存在struct CommuPacket的一个实例recvCommuPacket中:Recv( ( char * ) & recvCommuPacket , sizeof (CommuPacket) ); 接着判断报文类型进行相应处理: switch (recvCommuPacket. iPacketType) ... { case PACKET_A: … //A类报文处理 break; case PACKET_B: … //B类报文处理 break; case PACKET_C: … //C类报文处理 break;} 以上程序中最值得注意的是Send( ( char * ) & sendCommuPacket , sizeof (CommuPacket) );Recv( ( char * ) & recvCommuPacket , sizeof (CommuPacket) ); 中的强制类型转换:( char * ) & sendCommuPacket、( char * ) & recvCommuPacket,先取地址,再转化为char型指针,这样就可以直接利用处理字节流的函数。 利用这种强制类型转化,我们还可以方便程序的编写,例如要对sendCommuPacket所处内存初始化为0,可以这样调用标准库函数memset():memset(( char * ) & sendCommuPacket, 0 , sizeof (CommuPacket)); 2 . struct的成员对齐 Intel、微软等公司曾经出过一道类似的面试题: 1 . #include < iostream.h > 2 . #pragma pack(8) 3 . struct example1 4 . ... {5. short a;6. long b;7. } ; 8 . struct example2 9 . ... {10. char c;11. example1 struct1;12. short e; 13. } ; 14 . #pragma pack() 15 . int main( int argc, char * argv[]) 16 . ... {17. example2 struct2;18. cout << sizeof(example1) << endl;19. cout << sizeof(example2) << endl;20. cout << (unsigned int)(&struct2.struct1) - (unsigned int)(&struct2) << endl;21. return 0;22. } 问程序的输入结果是什么? 答案是: 8 16 4 不明白?还是不明白?下面一一道来: 2.1 自然对界 struct是一种复合数据类型,其构成元素既可以是基本数据类型(如int、 long 、float等)的变量,也可以是一些复合数据类型(如array、 struct 、union等)的数据单元。对于结构体,编译器会自动进行成员变量的对齐,以提高运算效率。缺省情况下,编译器为结构体的每个成员按其自然对界(natural alignment)条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。 自然对界(natural alignment)即默认对齐方式,是指按结构体的成员中size最大的成员对齐。 例如: struct naturalalign ... {char a;short b;char c;} ; 在上述结构体中,size最大的是short,其长度为2字节,因而结构体中的char成员a、c都以2为单位对齐, sizeof (naturalalign)的结果等于6; 如果改为: struct naturalalign ... {char a;int b;char c;} ; 其结果显然为12。 2 .2指定对界 一般地,可以通过下面的方法来改变缺省的对界条件: · 使用伪指令 #pragma pack (n),编译器将按照n个字节对齐; · 使用伪指令 #pragma pack (),取消自定义字节对齐方式。 注意:如果 #pragma pack (n)中指定的n大于结构体中最大成员的size,则其不起作用,结构体仍然按照size最大的成员进行对界。 例如: #pragma pack (n) struct naturalalign ... {char a;int b;char c;} ; #pragma pack () 当n为4、 8 、16时,其对齐方式均一样, sizeof (naturalalign)的结果都等于12。而当n为2时,其发挥了作用,使得sizeof(naturalalign)的结果为8。 在VC ++ 6 .0编译器中,我们可以指定其对界方式,其操作方式为依次选择projetct > setting > C / C ++ 菜单,在struct member alignment中指定你要的对界方式。 另外,通过__attribute((aligned (n)))也可以让所作用的结构体成员对齐在n字节边界上,但是它较少被使用,因而不作详细讲解。 2.3 面试题的解答 至此,我们可以对Intel、微软的面试题进行全面的解答。 程序中第2行 #pragma pack (8)虽然指定了对界为8,但是由于struct example1中的成员最大size为4(long变量size为4),故struct example1仍然按4字节对界,struct example1的size为8,即第18行的输出结果; struct example2中包含了struct example1,其本身包含的简单数据成员的最大size为2(short变量e),但是因为其包含了struct example1,而struct example1中的最大成员size为4, struct example2也应以4对界, #pragma pack (8)中指定的对界对struct example2也不起作用,故19行的输出结果为16; 由于struct example2中的成员以4为单位对界,故其char变量c后应补充3个空,其后才是成员struct1的内存空间,20行的输出结果为4。 3 . C和C ++ 间struct的深层区别 在C ++ 语言中struct具有了“类” 的功能,其与关键字class的区别在于struct中成员变量和函数的默认访问权限为public,而class的为private。 例如,定义struct类和class类: struct structA ... {char a;…} class classB ... { char a; …} 则: struct A a;a.a = ' a ' ; // 访问public成员,合法 classB b;b.a = ' a ' ; // 访问private成员,不合法 许多文献写到这里就认为已经给出了C ++ 中struct和class的全部区别,实则不然,另外一点需要注意的是: C ++ 中的struct保持了对C中struct的全面兼容(这符合C ++ 的初衷——“a better c”),因而,下面的操作是合法的: // 定义struct struct structA ... {char a;char b;int c;} ;structA a = ... {'a' , 'a' ,1} ; // 定义时直接赋初值 即struct可以在定义的时候直接以 ... { } 对其成员变量赋初值,而class则不能,在经典书目《thinking C ++ 2nd edition》中作者对此点进行了强调。 4 . struct编程注意事项 看看下面的程序: 1 . #include < iostream.h > 2 . struct structA 3 . ... {4. int iMember;5. char *cMember;6. } ; 7 . int main( int argc, char * argv[]) 8 . ... {9. structA instant1,instant2;10.char c = 'a'; 11. instant1.iMember = 1;12. instant1.cMember = &c; 13.instant2 = instant1;14.cout << *(instant1.cMember) << endl;15.*(instant2.cMember) = 'b';16. cout << *(instant1.cMember) << endl;17. return 0;} 14行的输出结果是:a 16行的输出结果是:b Why ? 我们在15行对instant2的修改改变了instant1中成员的值! 原因在于13行的instant2 = instant1赋值语句采用的是变量逐个拷贝,这使得instant1和instant2中的cMember指向了同一片内存,因而对instant2的修改也是对instant1的修改。 在C语言中,当结构体中存在指针型成员时,一定要注意在采用赋值语句时是否将2个实例中的指针型成员指向了同一片内存。 在C ++ 语言中,当结构体中存在指针型成员时,我们需要重写struct的拷贝构造函数并进行“ = ”操作符重载。