滴水三期:day16.1-字节对齐

本文详细介绍了C语言的关键字typedef用于数据类型重命名及一维、二维数组和结构体的定义,同时涵盖了sizeof操作符用于获取数据类型和变量的字节宽度。重点讲解了字节对齐的概念、原因及其在内存分配中的应用,以VC默认8字节对齐为例,通过实例解析了不同结构体的内存分配规律。

一、关键字

1.typedef

  • typedef为C语言的关键字,作用是为一种数据类型定义一个新名字。这里的数据类型包括内部数据类型(int,char等)和自定义的数据类型(struct等)

  • 对已有类型定义别名:

    typedef unsigned char BYTE;
    typedef unsigned short WORD;
    typedef unsigned int DWORD;
    
  • 一维数组类型的定义格式:(不常用)

    typedef int vector[10];	 //定义一个int型大小为10的数组直接用vector即可
    	
    int main(int argc, char* argv[]){	
    	vector v;   //相当于int v[10];
    	v[0] = 1;
    	v[1] = 2;
    	v[2] = 3;
    	v[3] = 4;
    	v[4] = 5;
    	return 0;
    }	
    
  • 二维数组类型的定义格式:

    typedef int name[5][5];	
    typedef int nameTable[5][5][5];	
    	
    int main(int argc, char* argv[]){	
    	name v;
    	nameTable n;
    	v[0][0] = 1;
    	v[0][1] = 2;
    	v[0][2] = 3;
    	v[0][3] = 4;
    	v[0][4] = 5;
    	n[0][0][0] = 1;
    	return 0;
    }	
    
  • 结构体的定义格式:

    typedef struct student{
    	int x;
    	int y;
    }stu;  //这里
    int main(int argc, char* argv[]){	
    	stu s;   //相当于定义了一个student类型的结构体类型变量s
        s.x = 1;
    	return 0;
    }
    
    //这里的student也可以不写,因为这里强调的是struct结构体类型变量名叫stu,有没有student这个名字不重要了
    typedef struct{
        int x;
        int y;
    }stu;
    

2.sizeof

  • 基本数据类型的sizeof,可以传入一个数据类型,也可以传入基本类型的变量。以字节为单位显示类型或者变量的数据宽度

    void Func(){
    	printf("%d",sizeof(int));  //4
    	printf("%d",sizeof(char));  //1
    	printf("%d",sizeof(short));  //2
    	printf("%d",sizeof(long));  //4
    	printf("%d",sizeof(float));  //4
    	printf("%d",sizeof(double));  //8
    	printf("%d",sizeof(__int64));  //8   long long
        int x = 10;
        printf("%d",sizeof(x))  //4
    }
    
  • 数组类型的sizeof,可以放入数组名判断整个数组占的宽度,以字节为单位;也可以放入数组中的一个元素,显示元素的类型宽度;也可以通过sizeof(数组名)/sizeof(数组中的一个元素)显示数组中元素的个数

    • 但是注意:如果数组是作为参数传入函数,在函数中要使用sizeof(数组名),打印的就不是数组占的宽度了!
    char arr1[10] = {0};
    int arr2[10] = {0};
    printf("%d\n",sizeof(arr1));   //10
    printf("%d\n",sizeof(arr2));   //40
    printf("%d\n",sizeof(arr1[2]));  //1
    printf("%d\n",sizeof(arr2[10]));  //虽然下标越界了,但是编译器还是会认为就是算arr2数组中下标为10的元									   素,而且已知arr2中数据类型为int型,所以会打印4
    printf("%d",sizeof(arr2)/sizeof(arr2[0]));  //10
    
  • 结构体类型的sizeof,可以放入结构体类型,显示结构体中定义的所有变量占的宽度,字节为单位显示(注意结构体中的不同类型的变量排列顺序不同可能导致占用内存的宽度不同);也可以放入此结构体类型的变量,同样的效果

    struct S1	
    {	
    	char a;
    	int b;
    	char c;
    };	
    struct S2	
    {	
    	int a;
    	char b;
    	char c;
    };	
    int main(int argc, char* argv[])	
    {	
    	S1 x;
    	S2 y;
    	
    	printf("%d\n",sizeof(x));  //12
    	printf("%d\n",sizeof(y));  //8
    	
    	printf("%d\n",sizeof(S1)); //12
    	printf("%d\n",sizeof(S2)); //8
    	
    	return 0;
    }
    

    先看下面的字节对齐笔记,然后反过来再分析一下为什么这两种结构体分配的内存会有差异?

    • 第一种:

      struct S1	//默认的结构体字节对齐数为8
      {	
      	char a;
      	int b;
      	char c;
      };	
      
      • 先分配char a1字节内存,其实地址设为offset0
      • int b宽度为4字节,和字节对齐数8比较,取最小值4作为b的字节对齐数。那么b的分配起始地址为4的整数倍,那么就从offset4开始存储(a和b之间空了3字节内存),分配内存大小为4字节。
      • 最后char c宽度为1字节,和字节对齐数8比较,取最小值1作为c的字节对齐数。那么c的起始地址为1的整数倍,所以紧跟着存储就可以,分配的内存大小为1字节;但是由于是最后一个成员,所以还要补3字节内存,使最后结构体总大小为结构体中成员的最大类型宽度4(int)的整数倍
      • 综上S1分配的内存大小为12
    • 第二种:

      struct S2	
      {	
      	int a;
      	char b;
      	char c;
      };	
      
      • 先分配int a4字节内存,起始地址为offset0
      • char b宽度为1字节,小于字节对齐数8,则取1作为b的字节对齐数。那么b的起始地址为1的整数倍,那么就从offset4挨着存即可,分配内存大小为1字节
      • 最后存char c宽度为1字节,小于字节对齐数8,则取1作为c的字节对齐数。那么c的起始地址为1的整数倍,那么就从offset5挨着存即可,分配内存大小为1字节。但是由于是最后一个成员,所以还要补2字节内存,使最后结构体总大小为结构体中成员的最大类型宽度4(int)的整数倍
      • 综上S2分配的内存大小为8

二、字节对齐(结构体对齐)

1.为什么要字节对齐

  • 引出字节对齐:上面的例子中,结构体中定义了相同类型和个数的变量,但是顺序不同,导致了内存分配的空间大小也不同,这就是字节对齐导致的
  • 为什么要有字节对齐:本质上是因为效率问题。编译器在存储数据时要考虑两个因素:效率和空间。有时为了查找数据时更快而舍弃了空间占用小的原则;有时候为了使占用空间最小不浪费而舍弃了存与读取的效率问题

2.给结构体指定字节对齐数

  • **#pragma pack**的基本用法为:

    /*
    #pragma pack(n)	
    结构体。。。
    #pragma pack()
    */
    
    #pragma pack(8)
    struct Test{
    	int a ;
    	__int64 b ;
    	char c ;
    };
    #pragma pack()
    
    • n为字节对齐数,其取值为1、2、4、8,VC6默认是8

3.四种字节对齐数分配结构体内存的通用规律

  • 字节对齐数:

    • 1,2,4,8字节对齐数规则总结

      1. 为结构体成员分配内存时最终使用的字节对齐数是–结构体的字节对齐数与结构体成员的sizeof值的那个

      2. 第一个数据成员放在offset为0的地方,以后每个结构体成员存储的起始位置是此成员最终使用的字节对齐数的整数倍。

        前面如果有空出来的字节就空出来不管

      3. 1如果结构体中所有成员的最大数据宽度(字节为单位) >= 结构体的字节对齐数(如果成员是数组要看数组中元素类型的宽度如果成员是结构体要看结构体中成员的类型宽度,下面只要对成员宽度的判断都是这个规则)

        • 最后一个存入的结构体成员按照规则1、2分配好内存后,必须满足加上要补的内存大小后结构体的总大小 = 结构体的字节对齐数的整数倍,所以如果大小不够,要把后面的内存算上。此时所有内存加在一起才是整个结构体份分配的内存

          最后一个只是把后面的内存算到结构体的sizeof,而不是真的专门分配给了最后一个成员,所以结构体的最后一个补上去的内存中数值原来是多少就是多少,不会被最后一个元素影响。但是如果最后一个成员刚好长度够了不需补,那这些都不用考虑了

      4. 2如果结构体中所有成员的最大数据宽度 < 结构体的字节对齐数(如果使用VC默认的结构体字节对齐数8,直接用这个规则即可,不用考虑那么多!因为8已经是最大的了)

        • 最后一个存入的结构体成员按照规则1、2分配好内存后,必须满足加上要补的内存大小后结构体的总大小 = 所有成员中最大的数据宽度的整数倍,所以如果大小不够,要把后面的内存算上。此时所有内存加在一起才是整个结构体份分配的内存
      5. 如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储

        比如:struct a里存有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始存储

    • 如果字节对齐数为1,比如:

      #pragma pack(1)  //字节对齐数为1
      struct Test{
      	int a;
      	__int64 b;
      	char c;
      };
      #pragma pack()    //按照1字节对齐数分配内存给结构体,一共分配了4 + 8 + 1 = 13字节内存
      

      Test结构体中先后定义了int、__int64、char类型变量:

      由于字节对齐数为1,那么数据类型的宽度肯定都是大于等于1的,所以结构体中的所有变量都以1为字节对齐数,即用1字节为单位分配给成员内存空间;而且后面所有元素都从1字节的整数倍地址开始存储。所以字节对齐数为1相当于挨着依次往后存

      • int宽度为4字节,以1字节1字节内存为单位分配给int a,那么从offset0开始的4字节内存存放int a的值

      • 接着从offset4(相对于结构体第一个成员在内存中的起始地址的偏移量为4)内存开始存,__int64宽度为8字节,还是一个字节一个字节的分配,最后直到分了8个字节的内存存储__int64 b的值

      • 最后char宽度为1字节,那么一个字节一个字节分配,刚好分配一个字节内存给char c的值

    • 如果字节对齐数为2,比如:

      #pragma pack(2)  //字节对齐数为2
      struct Test{
      	int a;
      	char c;
      };
      #pragma pack()  //按照2字节对齐数分配内存给结构体,一共分配了4 + 2 = 6字节内存
      

      Test结构体中按照此顺序定义2个变量:

      • 为第一个成员分配空间时:由于字节对齐数为2,int宽度为4字节,那么以最小值2为字节对齐数,以2字节为单位分配给int a,那么用4字节内存存储int a的值,起始地址为offset0

      • 为第二个成员分配空间时:char的宽度1和2比较,则此时的字节对齐数选择1。根据规则2,第二个元素的起始地址为1的整数倍,那么就从offset4开始存储(前面4字节内存已经分配给int a)。但是由于此时为最后一个成员,而且成员最大宽度4 > 结构体字节对齐数2,那么就要满足规则3.1,由于结构体的字节对齐数此时为2,那么就要补成两个字节,所以最终结构体分配的内存为4 + 2 = 6字节!

        image-20211210150256492
    • 如果字节对齐数为4,比如:

      #pragma pack(4)  //字节对齐数为4
      struct Test{
      	int a;
          char b
      	__int64 c;
      	char d;
      };
      #pragma pack()  //按照4字节对齐数分配内存给结构体,一共分配了4 + 1 + 3 + 8 + 1 + 3 = 20字节内存
      

      同样的道理:

      • int宽度为4,和结构体的字节对齐数4相等,所以字节对齐数就是4。即以4字节为单位分配给int a,刚好存下,其实地址为offset0

      • char宽度为1,结构体字节对齐数为4,所以取最小值1作为此成员的字节对齐数。那么起始地址为offset4,往后分配1字节内存

      • 下一个__int64宽度为8,与结构体字节对齐数比较取最小值4作为此成员的字节对齐数。而且c的起始地址必须是4(最小值哦)的倍数,那么只能空出来3个字节内存从offset8开始存,一共分配8字节内存

      • 最后一个char宽度为1,与结构体字节对齐数比较取最小值1作为此成员的字节对齐数。c的起始地址是1的倍数,所以挨着前一个成员内存往后存即可,即从offset16开始,往后分配1字节内存。但是由于结构体当中的成员最大数据宽度8 > 结构体的字节对齐数4,所以加上要补的内存大小后结构体的总大小 = 结构体的字节对齐数4的整数倍,即最后要补3字节。那么最终结构体分配的内存大小为4 + 1 + 3 + 8 + 1 + 3 = 20字节

        image-20211210151158868
    • 如果字节对齐数为8,比如:

      #pragma pack(8)  //字节对齐数为8
      struct Test{
      	int a;
      	__int64 b;
      	char c;
      };
      #pragma pack()  //按照8字节对齐数分配内存给结构体,一共分配了4 + 4 +8 + 1 + 7 = 24字节内存
      

      同理:

      • 分配int a的内存时,int宽度为4,结构体字节对齐数为8,则取最小值4作为此成员的字节对齐数。那么前4个字节内存从offset0开始分配给int a

      • 接着分配__int64 b的内存,__int64宽度为8字节,结构体字节对齐数为8,相等则取8作为此成员的字节对齐数。但是由于__int64 b的起始地址要为8的整数倍,所以只能空出来4字节内存,从offset8开始存储,分配8字节内存

      • 最后分配char c的内存,char宽度为1字节,结构体字节对齐数为8,取最小值1作为此成员的字节对齐数,起始地址为1的倍数,所以直接接着上一个成员的内存分配即可,即从offset16开始存储,分配1字节内存。但是由于结构体的字节对齐数8 = 成员最大宽度8,满足规则3.1,那么最后要补7字节。综上最终结构体的分配的内存大小为4 + 4 +8 + 1 + 7 = 24字节

        image-20211210151921591
  • 知道字节对齐规则后,建议以后定义结构体中成员变量的顺序:按照数据类型由小到大的顺序进行书写,这样浪费的内存空间更多少。

4.使用VC默认的结构体字节对齐数8,给结构体分配内存过程总结

  1. 第一个成员按照类型宽度分配就可以了(把此成员存储的地址叫offset0

  2. 往后的成员(除最后一个外)先用此成员类型的宽度和结构体字节对齐数比较,取最小值作为此成员的字节对齐数

  3. 接着得出此成员存放的起始地址:满足起始地址(相对于offset0的地址)是此成员的字节对齐数的整数倍,得到起始地址再按照类型宽度存入即可(如果起始地址和上一个元素分配的内存之间空了几字节内存,不会补0,直接就是空过去)

  4. 最后一个成员依然先得出此成员的字节对齐数再得到存放的起始地址。之后还要判断是否需要补内存、补几字节内存:满足加上要补的内存大小后结构体的总大小 = 所有成员中最大的数据宽度的整数倍(最后补的几字节内存只是为了方便查找结构体中数据而补进去的,不属于最后一个成员的内存)

补充特例:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大成员大小的整数倍地址开始存储;如果成员是数组要看数组中元素类型的宽度。

这里有一点问题,当结构体中只有char a[10]时,会发现虽然sizeof(结构体)=10,但是在缓冲区中存储时,存储char a[10]的内存和ebp之间空了两个字节内存,不是挨着存的,即从[ebp - 0xC]到[ebp - 0x3]存储字符数组,而[ebp - 0x2]和[ebp - 0x1]是空出来的,不知道为什么。经验证:如果第一个成员是char数组,必须从ebp - n * 4的地址开始存储第一个元素。

三、作业

  • 分析下面结构体的内存分配

    struct S1{
    	char c;
    	double i;
    };
    
    struct S2{
    	int c1;
    	char c2[10];
    };
    
    struct S3{
    	char c1;
    	S1 s;  
    	char c2;
    	char c3;
    };
    
    struct S4{
    	char c1;
    	S1 s;   
    	char c2;
    	double c3;
    };
    void main(int argc,char* argv[]){
        S1 a;
        S2 b;
        S3 c;
        S4 d;
        printf("%d %d %d %d",sizeof(S1),sizeof(S2),sizeof(S3),sizeof(S4));  //验证一下
        //16 16 32 40
    }
    

    因为这里都是使用VC默认的结构体字节对齐数8,所以不用用通用规律,直接使用上述字节对齐数为8的规则即可

    • 分析S1结构体内存分配:

      • 先分配char c:c是char类型,起始地址offset0(是结构体的第一个成员,所以逻辑起始地址可设为offset0,真实就是从低地址开始分配,存储在main的缓冲区中),分配1字节内存
      • 最后分配double i:i是double类型,宽度为8字节,VC默认的结构体字节对齐数为8,取最小值则i的字节对齐数对8;接着因为i的起始地址必须是8的倍数,所以只能空出7字节内存,从offset8地址开始分配,分配8字节。此时不用补内存,因为最后一个成员分配后结构体的总长度刚好是所有成员中最大的数据宽度的整数倍
      • 综上,S1结构体变量一共分配了16字节内存
    • 分析S2结构体内存分配:

      • 先分配int c1:c1是int类型,宽度为4,则起始地址为offset0,分配4字节内存
      • 最后分配char c2[10]:c2是数组型,(判断c2的字节对齐数不能用数组的总长度判断,而要用数组中元素的类型,包括最后判断是否需要补内存时也要用1判断)宽度为1,与字节对齐数8比较取最小值1作为c2数组存储的字节对齐数;继而得出c2的起始地址必须是1的倍数;那么挨着第一个成员分配的内存后面存即可,即起始地址为offset4,分配10字节内存。但是由于结构体的总长度要是所有成员中最大的数据宽度的整数倍(因为成员1的数据宽度为4,成员2的数据宽度为1!!所以最大数据宽度为4),所以还要补2字节内存
      • 综上,S2结构体变量一共分配了16字节内存
    • 分析S3结构体内存分配:

      • 先分配char c1:c1是char类型,宽度为4,则起始地址为offset0,分配1字节内存

      • 再分配S1 s:s是结构体S1类型,我们使用的规则是:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。因为S1中成员最大宽度为double的8字节,所以要从8的整数倍地址开始存储,即要空出7字节内存,从offset8开始存储,分配16字节内存(上面已经分析过S1结构体变量分配的内存大小)

      • 再分配char c2:c2是char类型,宽度为1,小于字节对齐数8,则取最小值1作为c2的字节对齐数;继而得出c2的起始地址为1的整数倍,那么挨着前一个成员分配的内存存储即可,即从offset24开始,分配1字节内存

      • 最后分配char c3:c3是char类型,同理挨着存,从offset25开始,分配1字节内存。但是要满足结构体的总长度是所有成员中最大的数据宽度的整数倍,而此结构体中最大的成员宽度为成员2–S1结构体中的double类型8字节,所以结构体的分配内存总大小为8的整数倍,所有再补6字节内存

      • 综上,S3结构体变量一共分配了32字节内存

    • 分析S4结构体内存分配:

      • 前三个成员的内存分配和S3的前三个成员的内存分配一模一样
      • 最后分配double c3,c3宽度为8字节,则起始地址为8的整数倍,由于上一个存的是char c2,c2的起始地址为offset24,占了1字节,所以c3的存储要先空出来7字节内存,从offset32开始存储,分配8字节内存
      • 综上,S4结构体变量一共分配了40字节内存

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值