sizeof使用的相关总结

      sizeof是C语言的一种单目操作符,如C语言的其他操作符++、--等。它并不是函数。sizeof操作符以字节形式给出了其操作数的存储大小。操作数可以是一个表达式或括在括号内的类型名。操作数的存储大小由操作数的类型决定。 

一、采用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自动填充的,没有放任何有意义的东西。

        字节对齐的细节和编译器实现相关,但一般而言,满足三个准则:

      1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;

     2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);

     3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。

     对于上面的准则,有几点需要说明:

     1) 前面不是说结构体成员的地址是其大小的整数倍,怎么又说到偏移量了呢?因为有了第1点存在,所以我们就可以只考虑成员的偏移量,这样思考起来简单(想想为什么)。

结构体某个成员相对于结构体首地址的偏移量可以通过宏offsetof()来获得,这个宏也在stddef.h中定义,如下:

#define offsetof(s,m) (size_t)&(((s *)0)->m)

例如,想要获得S2中c的偏移量,方法为

size_t pos = offsetof(S2, c);// pos等于4

      2) 基本类型是指前面提到的像char、short、int、float、double这样的内置数据类型,这里所说的“数据宽度”就是指其sizeof的大小。由于结构体的成员可以是复合类型,比如另外一个结构体,所以在寻找最宽基本类型成员时,应当包括复合类型成员的子成员,而不是把复合成员看成是一个整体。但在确定复合类型成员的偏移位置时则是将复合类型作为整体看待。

     struct S1

  {

  int i;   //起始偏移地址为0,满足对齐方式,占用sizeof(int)=4个字节

  char c;//可用的偏移地址为4,是sizeof(char)=1的倍数,故无需填充,

             //偏移地址为4,占用1个字节

  };         //当前总大小为5个字节,不是最大字节元素sizeof(int)=4的倍数,故需

            //填充3个字节,最终总字节大小为8

    

struct S3

  {

           char  c1;    //起始偏移地址为0,满足对齐方式,占用sizeof(char)=1个字节

           S1  s;          //可用的起始偏移地址为1,最为一个整体,不是sizeof(int)=4

                            //倍数,故需填充3个字节,起始偏移地址为4,占用sizeof(S1)=8个字节

           char  c2;    //可用的起始偏移地址为12,是sizeof(char)=1的倍数,故无需填充,起

                            //始偏移地址为12,占用sizeof(char)=1个字节

  };                //当前总大小为13个字节,不是最大元素sizeof(int)=4的倍数,故需填充3个

                   //字节,总字节大小为16个字节

          S1的最宽简单成员的类型为int,S3在考虑最宽简单类型成员时是将S1“打散”看的,所以S3的最宽简单类型为int,这样,通过S3定义的变量,其存储空间首地址需要被4整除,整个sizeof(S3)的值也应该被4整除。c1的偏移量为0,s的偏移量呢?这时s是一个整体,它作为结构体变量也满足前面三个准则,所以其大小为8,偏移量为4,c1与s之间便需要3个填充字节,而c2与s之间就不需要了,所以c2的偏移量为12,算上c2的大小为13,13是不能被4整除的,这样末尾还得补上3个填充字节。最后得到sizeof(S3)的值为16。

再例如:

     struct MyDef

  {

           short  a;                       //起始偏移地址为0,满足对其方式,占用sizeof(short)=2字节

           char  b[3];                   //可用的偏移地址为2,是sizeof(char)=1的倍数,故无需填充,

                                              //偏移地址为2,占用3*sizeof(char)=3个字节

           int  c[2];                       //可用的偏移地址为5,不是sizeof(int)=4的倍数,故需填充3个字

                                              //节,偏移地址为8,占用2*sizeof(int)=8个字节

           double  d[3];          //可用的偏移地址为16,是sizeof(double)=8的倍数,故无需填充,

                                        //偏移地址为16,占用3*sizeof(double)=24个字节

  };                         //当前字节总数为40,是最大字节元素sizeof(double)=8的倍数,故总计占

                           //用40个字节

二、含位域结构体的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       1316

位域类型为char,第1个字节仅能容纳下f1和f2,所以f2被压缩到第1个字节中,而f3只能从下一个字节开始。因此sizeof(BF1)的结果为2。示例2:

struct BF2

{

char f1 : 3;     //起始偏移地址为0,占用1个字节(前3位)

short f2 : 4;    //可用的偏移地址为1,不是sizeof(short)=2的倍数,需

//填充1个字节,故偏移地址为2,占用2个字节(前4位)

char f3 : 5;    //可用的偏移地址为4,是sizeof(char)=1的倍数,故起始偏移

//地址为4,占用1个字节(前5位)

};  //分配完内存后需要5个字节,不是sizeof(short)=2的倍数,故需填充1个

//字节,最终总的字节数为6

由于相邻位域类型不同,在VC6中其sizeof为6,在Dev-C++中为2。示例3:

struct BF3

{

char f1 : 3;

char f2;

char f3 : 5;

};

非位域字段穿插在其中,不会产生压缩,在VC6和Dev-C++中得到的大小均为3。

三、 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;    //起始偏移地址为0,占用1个字节

     double m4;   //可用的起始偏移地址为1,不是sizeof(double)的倍数,应补齐为4的

                          //倍数,故需填充3个字节,起始偏移地址为4,占用8个字节

   int  m3;    //可用的起始偏移地址为12,是sizeof(int)的倍数,占用4个字节

  };     //全部分配空间后占用的字节为16个字节,是sizeof(double)的倍数,故最终的总

        // 字节大小为16个字节

  #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。(请读者自己分析)

     #pragma pack(push) //保存对齐状态

  #pragma pack(16)//设定为16字节对齐

  struct test

  {

           char  m1;     //起始偏移地址为0,占用1个字节

           double  m4;       //可用的起始偏移地址为1,不是sizeof(double)的倍数,应补齐为8的

                               //倍数,故需填充7个字节,起始偏移地址为8,占用8个字节

           int  m3;      //可用的起始偏移地址为16,是sizeof(int)的倍数,占用4个字节

  };      //全部分配空间后占用的字节为20个字节,不是sizeof(double)的倍数,应填

         //充4个字节,故最终的总字节大小为24个字节

  #pragma pack(pop)//恢复对齐状态

  

四、 sizeof用法总结

         在VC中,sizeof有着许多的用法,而且很容易引起一些错误。下面根据sizeof后面的参数对sizeof的用法做个总结

         A)、参数为数据类型或者为一般变量。例如sizeof(int),sizeof(long)等等。这种情况要注意的是不同系统或者不同编译器得到的结果可能是不同的。例如int类型在16位系统中占2个字节,在32位系统中占4个字节。

         B)、参数为数组或指针。下面举例说明.

inta[50];        //sizeof(a)=sizeof(int)*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);   //这里将输出4,本来s为一个数组,但由于做为函

                     //数的参数在传递的时候系统处理为一个指针,所

                     //以sizeof(s)实际上为求指针的大小。

     return 1;

}

 

sizeof(func("1234"))=4  //因为func的返回类型为int,所以相当于求sizeof(int).

 

posted @ 2017-11-28 13:22  Histring  阅读(249)  评论(0编辑  收藏  举报