精华内容
下载资源
问答
  • C语言结构对齐

    2014-05-06 22:55:25
    这是一个关于C语言的结构体内存对齐问题概述
  • C语言 结构对齐

    2016-10-27 18:56:08
    字节对齐一直困扰着我,具体为什么需要字节对齐也不是特别清楚,貌似是因为什么CPU的读取相关的问题导致的。 我最近研究了一下,发现一个非常简单的对齐计算方式。 假设其实地址是0,那么以后的每个变量的开始...

    字节对齐一直困扰着我,具体为什么需要字节对齐也不是特别清楚,貌似是因为什么CPU的读取相关的问题导致的。


    我最近研究了一下,发现一个非常简单的对齐计算方式。


    假设其实地址是0,那么以后的每个变量的开始地址必须是其自身的长度的整数倍,还有,为了保证结构体中套用结构体的方式,所以结构体本身的长度需要是其最长元素的整数倍。


    struct A

    {

    short a;

    int b;

    short c;

    double d;

    char e;

    };


    A::a 占用2个字节,总字节数为2

    A::b 占用4个字节,总字节数不够4的整数倍,需要补齐2个字节,总字节数为4+4=8

    A::c 占用2个字节,总字节数为10

    A::d 占用8个字节,总字节数不够8的整数倍,需要补齐6个字节,总字节数为16+8=24

    A::e占用1个字节,总字节数为25

    A中最长的变量字节数为8,总字节数不够8的整数倍,需要补齐7个字节,总字节数为32


    struct B

    {

    struct A a;

    char b;

    };


    B::a 占用32个字节,总字节数为32

    B::b 占用1个字节,总字节数为33

    B中最长的变量字节数为8,总字节数不够8的整数倍,需要补齐7个字节,总字节数为40


    展开全文
  • 简单回顾一下,对于我们常用的小端对齐方式,一个数据类型其高位数据存放在地址高位,地位数据在地址低位,如下图所示↓ 这种规律对于我们的基本数据类型是很好理解的,但是对于像结构、联合等一类聚合类型...

    【@.1 结构体对齐】

    @->1.1

    如果你看过我的这一篇博客,一定会对字节的大小端对齐方式有了重新的认识。简单回顾一下,对于我们常用的小端对齐方式,一个数据类型其高位数据存放在地址高位,地位数据在地址低位,如下图所示↓

     image

    这种规律对于我们的基本数据类型是很好理解的,但是对于像结构、联合等一类聚合类型(Aggregate)来说,存储时在内存的排布是怎样的?大小又是怎样的?我们来做实验。

    *@->我们会经常用到下面几个宏分别打印变量地址、大小、格式化值输出、十六进制值输出↓

       #define Prt_ADDR(var)   printf("addr:  0x%p  \'"#var"\'\n",&(var))
       #define Prt_SIZE(var)   printf("Size of \'"#var"\': %dByte\n",(int)sizeof(var))
       #define Prt_VALU_F(var,format)   printf(" valu: "#format"  \'"#var"\'\n",var)
       #define Prt_VALU(var)   Prt_VALU_F(var,0x%p)

    *@->如果你没有C语言编译环境可以参考我的博客配置一个命令行gcc编译环境,或者基于gcc的eclipse

    考虑下面代码,

     

    #include <stdio.h>
    
    #define Prt_ADDR(var) printf("addr:  0x%p  \'"#var"\'\n",&(var))
    #define Prt_SIZE(var) printf("Size of \'"#var"\': %dByte\n",(int)sizeof(var))
    #define Prt_VALU_F(var,format) printf(" valu: "#format"  \'"#var"\'\n",var)
    #define Prt_VALU(var) Prt_VALU_F(var,0x%p)
    
    typedef struct{
        char a;
        char b;
        char c;
        char d;
    } MyType,*pMyType;    //含有四个char成员的结构
    
    int main()
    {
        pMyType pIns;    //结构指针实例
        int final;        //拼接目标变量
    
        pIns->a=0xAA;
        pIns->b=0xBB;
        pIns->c=0xCC;
        pIns->d=0xDD;
    
        final = *(unsigned int *)pIns;    //拼接结构到int类型变量
        Prt_VALU(final);
        return 0;
    }

    上面代码定义了一个含有4个char成员的结构,MyType和其指针pMyTYpe。新建一个实例pIns,赋值内部的四个成员,再将整体拼接到int类型的变量final中。MyType中只有四个char类型,所以该结构大小为4Byte(可以用sizeof观察),而32位CPU中int类型也是4Byte所以大小正好合适,就看顺序,你认为最终的顺序是“0xAABBCCDD”,还是“0xDDCCBBAA”?

    下面是输出结果(我用的eclipse+CDT)。

    image

    为什么?

    结构体中地址的高低位对齐的规律是什么?

    我们说,局部变量都存放在栈(stack)里,程序运行时栈的生长规律是从地址高到地址低。C语言到头来讲是一个顺序运行的语言,随着程序运行,栈中的地址依次往下走。遇到自定义结构MyType的变量Ins时(我们程序里写的是指针pIns,道理一样),首先计算出MyType所需的大小,这里是4Byte,在栈里开辟一片4Byte的空间,其最低端就是这个结构的入口地址(而不是最上端!)。进入这个结构后,依次往上放结构中的成员,因此结构中第一个成员a在最下面,d在最上面。联系到我们的小端(little-endian)对齐,因此最后输出的结果是按照高位到低位,d-c-b-a的顺序输出一个完整的数。因此最终的final=0xDDCCBBAA。

    image

    IN A NUTSHELL

    结构体中的成员按照定义的顺序其存储地址依次增长。

    @->1.2

    之前我们提到一句,遇到一个结构体时首先计算其大小,再从栈上开辟相应区域。那么这个大小是怎么计算的?

    typedef struct{
        char a;
        int b;
        char c;
        char d;
    } T1,*pT1;
    
    typedef struct{
        char a;
        char b;
        char c;
        int d;
    } T2,*pT2;

    现在计算上面定义的两个结构体T1,T2的大小是多少?可以通过下面代码打印

    #include <stdio.h>
    
    #define Prt_ADDR(var) printf("addr:  0x%p  \'"#var"\'\n",&(var))
    #define Prt_SIZE(var) printf("Size of \'"#var"\': %dByte\n",(int)sizeof(var))
    #define Prt_VALU_F(var,format) printf(" valu: "#format"  \'"#var"\'\n",var)
    #define Prt_VALU(var) Prt_VALU_F(var,0x%p)
    
    typedef struct{
        char a;
        int b;
        char c;
        char d;
    } T1,*pT1;
    
    typedef struct{
        char a;
        char b;
        char c;
        int d;
    } T2,*pT2;
    
    int main()
    {
        T1 Ins1;
        T2 Ins2;
        Prt_SIZE(Ins1);
        Prt_SIZE(Ins2);
    }

    其结果如下↓

    image

    参考这篇文章,总结结构对齐原则是:

    原则1、数据成员对齐规则:结构(struct或联合union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储)。

    原则2、结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。(struct a里存有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始存储。)

    原则3、收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐。

    很明显按照以上原则,分析之前T1,T2结构的存储方式如图所示,打X的是按照规则之后的补充位↓

    image

    好了,现在可以考虑将结构T2改为:

      typedef struct{

        char a;

        char b;

        char c;

        int d;

        T1 e;    //T1类型成员e

      }T2, *pT2

    结构T2的大小是多大?(20Byte

    而如果改为:

      typedef struct{

        char a;

        char b;

        char c;

        int d;

        pT1 e; //pT1类型成员e

      }T2, *pT2

    结构T2的大小是多大?(12Byte

    这些情况均可以用上面三原则进行分析。

    因此,按照上面原则可以总结出一条经验性的习惯:将结构中数据类型大的成员往后放可以节省空间。

    【@.2 变量存放地址,堆、栈,及内存分配】

    我们先考虑一下局部变量在内存中的分布及顺序,考虑如下代码:

    #include <stdio.h>
    
    #define Prt_ADDR(var) printf("addr:  0x%p  \'"#var"\'\n",&(var))
    #define Prt_SIZE(var) printf("Size of \'"#var"\': %dByte\n",(int)sizeof(var))
    #define Prt_VALU_F(var,format) printf(" valu: "#format"  \'"#var"\'\n",var)
    #define Prt_VALU(var) Prt_VALU_F(var,0x%p)
    
    int ga=32;
    int gb=777;
    int gc;
    int gd;
    int main()
    {
        int a=23;
        int b;
        const char c='m';
        static int ss1;
        static int ss2=0;
        static int ss3=81;
        int * php1 = (int*)malloc(8*sizeof(int));
        int * php2 = (int*)malloc(sizeof(int));
        int hp3=malloc(sizeof(int));    //不好的写法
    
        char _pause;
        Prt_ADDR(a);
        Prt_ADDR(b);
        Prt_ADDR(c);
        Prt_ADDR(ss1);
        Prt_ADDR(ss2);
        Prt_ADDR(ss3);
    
        Prt_ADDR(php1);Prt_ADDR(*php1);
        Prt_ADDR(php2);Prt_ADDR(*php2);
        Prt_ADDR(hp3); Prt_VALU(hp3);    //hp3内部存放分配的地址值
    
        Prt_ADDR(ga);
        Prt_ADDR(gb);
        Prt_ADDR(gc);
        Prt_ADDR(gd);
        _pause=getchar();
    }

    这段代码用于测试变量所分配的地址值,其中包含了局部变量(a,b,c),静态局部变量(ss,ss2),全局变量(ga,gb,gc,gd)。变量_pause仅仅用于在VC中调试方便。

    参考这篇博客里的解释,内存通常可分为如下几块:

    BSS段:BSS段(bss segment)通常是指用来存放程序中未初始化,或初始化为0的全局变量,静态局部变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。

    数据段:数据段(data segment)通常是指用来存放程序中已初始化为非0的全局变量的一块内存区域。数据段属于静态内存分配。

    代码段:代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

    堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)

    栈(stack):栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。

    highest address
    =========
    | stack |
    | vv |
    | |
    | |
    | ^^ |
    | heap |
    =========
    | bss |
    =========
    | data |
    =========
    | text |
    =========
    address 0

    另外,栈(stack)的增长方向往地址低方向走,具有先进先出特点,栈顶指针位于低地址,随着程序运行在不断变化。堆(heap)的增长方向往地址高方向走,堆是一个类似于链表的结构,因此并不见得是个连续的空间。

    好了,我们通常的理解就到此为,运行上面代码结果如下(前者Visual Studio,后者eclipse调用gcc的编译结果如下)。

    image  image

    每次程序运行这些变量的绝对地址可能变化,所以分析时我们注重观察变量的相对地址变化。

    变量a,b,c均为局部变量,不管初始化与否,都被分配在栈上,而且顺序是按照从低至高向地址低分配的。其中变量c我添加了一个const是想说明,在修饰变量时,const对于地址分配无关,仅仅表示此变量是readonly的。另外,在VS系的编译器中,这些局部变量所占的空间大小比本身数据结构大,而gcc编译时的每个变量是地址上一个接着一个排,并且对齐方式也可以用前面的结构体对齐规律解释。

    变量ss1,ss2,ss3就有区别了。ss1是未初始化的静态局部变量,ss2初始化为0,将被分配到BSS区,而且二者在gcc或VC编译后都是紧挨着的而不是像栈时有区别(后面会解释)。ss3初始化了的静态局部变量,分配在data段。

    接下来的php1,php2和hp3变量用于演示堆(heap)操作。堆是由程序员自己控制并释放的,一般由malloc()等内存函数进行申请,最后需要用free进行释放(我在程序中没有用free了,最后将由系统释放)。这篇文章对内存操作有较详细的描述(这也是一篇比较优秀的在线C教程,而且是一页流)。mallloc()返回void*类型的指针,指向在堆中开辟的一片区域。注意并没有初始化这片区域,所以其中的值可能是任意的。

    我这里之所以打印了php1和*php1的地址是想说明,php本身是指针,其本身存在于栈中,而通过malloc分配之后,保存了一块分配好大小的堆的地址值。比较上面VS和gcc的编译结果,堆中的*php1和*php2分配的地址并不连续,而且地址增长方向也不同。虽然说堆是按照地址从低到高增长的,但是实际使用上堆相当于链表,一块链下一块,所以堆的地址增长方式我们可以不用太纠结。

    hp3演示了一个非常规的堆的申请,malloc本身返回一个void*类型指针,赋值给int类型的hp3,严格意义上即使强制转换也不允许的。那么int hp3=malloc(sizeof(int)); 这句话做了什么?通过后面Prt_VALU()打印其值可知,由于void*类型的特殊性,hp3中保存了分配好的堆的地址值。

    全局变量,ga,gb初始化为非0,分配在data段,而gc,gd未初始化,分配在BSS段。以上可以通过观察打印出来的地址理解。

    最后,总结一些有趣的实验现象如下:

    @-> 栈的地址位于所有区域的地址最下面,跟理论上栈位于地址高位有出入。

    @-> 堆的增长方向不见得是从地址低到高。gcc中是低到高,而VS中是高到低。

    @-> 在BSS区域,未初始化(或初始化为0)的全局变量(gc,gd)按地址从高到低分配,而静态局部变量(ss1,ss2)按地址从低到高分配。

    @-> 初始化的全局变量和静态局部变量(ga,gb,ss3)分配在Data段,从低到高分配,且地址上连续。

    那么,为什么堆栈(stack、heap)上的地址分配并不见得是一个挨着一个(VS编译下的局部变量a,b,c),而DATA段,BSS段往往是一个挨着一个的呢?这个问题我想其实很多新手并没有太深究(比如我),包括关于所谓静态区域和非静态区域到底意味着什么。

    【@.3 可执行文件包含的区域】

    前面一直在提到内存可分为BSS段、堆、栈、DATA、TEXT,那么对于程序经过编译后的可执行文件,如.out,.exe,.hex等,我们运行时是需要加载到内存中区的,那么他们的代码所占的段有哪些?是全部都包含了么?当然不是。

    对于这点,1997年出版的著名的《Expert C Programming: Deep C Secrets》中有一个详细的解释。对于如下图中左侧source file中的源代码,经过编译后到out文件时的变量存储区域如图所示。

    image

    当程序运行时,a.out加载到实际内存中去的分布如下图↓

    image

    OK,有了这两张图已经很能说明问题了!(上图没标明堆 heap)

    main函数中的局部变量,在编译时是不会编译到out文件,而是将申明变量的这条语句作为机器码放在text段,直到运行时再从栈或堆中分配内存。所以如果做实验发现,申明了局部变量之后发现编译后的文件变大了(有时又不会变大),以为是因为为局部变量分配了内存,其实应该是增加了申明局部变量这句话的操作的机器码。而BSS段虽然在输出文件里有,但是本身不占大小,仅仅是包含了一段最终所需BSS段的大小的信息,在运行时(runtime)会扩张为相应大小。因此

    @-> 初始化为非0的全局变量和静态局部变量会直接在输出文件中分配地址,运行时直接拷贝到内存data段。

    @-> 未初始化或初始化为0的全局变量和静态局部变量在输出文件中不占大小,仅仅记录下最终需要的BSS段大小,运行时扩张到内存中的BSS段初始化为0。

    @-> 局部变量,仅仅体现在申明时所执行操作语句的大小上,本身不占大小,运行时动态申请栈或堆。

    @.[FIN]      @.date->Dec 6, 2012      @.author->apollius

    转载于:https://www.cnblogs.com/apollius/archive/2012/12/06/2803339.html

    展开全文
  • C语言字节对齐

    2016-08-04 17:05:00
    C语言字节对齐 标签: c语言struct编译器数据结构alignment 2011-08-29 16:06 81709人阅读 评论(89) 收藏 举报  分类: C基础(85)  版权声明:本文为博主原创文章,未经博主允许...

    C语言字节对齐

    标签: c语言struct编译器数据结构alignment
     81709人阅读 评论(89) 收藏 举报
     分类:
     

    文章最后本人做了一幅图,一看就明白了,这个问题网上讲的不少,但是都没有把问题说透。

      一、概念 
       
       对齐跟数据在内存中的位置有关。如果一个变量的内存地址正好位于它长度的整数倍,他就被称做自然对齐。比如在32位cpu下,假设一个整型变量的地址为0x00000004,那它就是自然对齐的。
       
      二、为什么要字节对齐
       
       需要字节对齐的根本原因在于CPU访问数据的效率问题。假设上面整型变量的地址不是自然对齐,比如为0x00000002,则CPU如果取它的值的话需要访问两次内存,第一次取从0x00000002-0x00000003的一个short,第二次取从0x00000004-0x00000005的一个short然后组合得到所要的数据,如果变量在0x00000003地址上的话则要访问三次内存,第一次为char,第二次为short,第三次为char,然后组合得到整型数据。而如果变量在自然对齐位置上,则只要一次就可以取出数据。一些系统对对齐要求非常严格,比如sparc系统,如果取未对齐的数据会发生错误,举个例:
       
      char ch[8];
      char *p = &ch[1];
      int i = *(int *)p;
       
      
      运行时会报segment error,而在x86上就不会出现错误,只是效率下降。
      
      三、正确处理字节对齐
      
       对于标准数据类型,它的地址只要是它的长度的整数倍就行了,而非标准数据类型按下面的原则对齐:
      
      数组 :按照基本数据类型对齐,第一个对齐了后面的自然也就对齐了。 
      联合 :按其包含的长度最大的数据类型对齐。 
      结构体: 结构体中每个数据类型都要对齐。
      比如有如下一个结构体:
      
      struct stu{
       char sex;
       int length;
       char name[10];
      };
      struct stu my_stu;
       
      
      由于在x86下,GCC默认按4字节对齐,它会在sex后面跟name后面分别填充三个和两个字节使length和整个结构体对齐。于是我们sizeof(my_stu)会得到长度为20,而不是15.
      
      四、__attribute__选项
      
      我们可以按照自己设定的对齐大小来编译程序,GNU使用__attribute__选项来设置,比如我们想让刚才的结构按一字节对齐,我们可以这样定义结构体
      
      struct stu{
       char sex;
       int length;
       char name[10];
      }__attribute__ ((aligned (1))); 
      
      struct stu my_stu;
       
      
      则sizeof(my_stu)可以得到大小为15。
      
      上面的定义等同于
      
      struct stu{
       char sex;
       int length;
       char name[10];
      }__attribute__ ((packed)); 
      struct stu my_stu;
       
      
      __attribute__((packed))得变量或者结构体成员使用最小的对齐方式,即对变量是一字节对齐,对域(field)是位对齐.
      
      五、什么时候需要设置对齐
      
       在设计不同CPU下的通信协议时,或者编写硬件驱动程序时寄存器的结构这两个地方都需要按一字节对齐。即使看起来本来就自然对齐的也要使其对齐,以免不同的编译器生成的代码不一样.

     

    一、快速理解

    1. 什么是字节对齐?

    在C语言中,结构是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构、联合等)的数据单元。在结构中,编译器为结构的每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。

    为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的”对齐”. 比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除.

    2. 字节对齐有什么作用?

    字节对齐的作用不仅是便于cpu快速访问,同时合理的利用字节对齐可以有效地节省存储空间。

    对于32位机来说,4字节对齐能够使cpu访问速度提高,比如说一个long类型的变量,如果跨越了4字节边界存储,那么cpu要读取两次,这样效率就低了。但是在32位机中使用1字节或者2字节对齐,反而会使变量访问速度降低。所以这要考虑处理器类型,另外还得考虑编译器的类型。在vc中默认是4字节对齐的,GNU gcc 也是默认4字节对齐。

    3. 更改C编译器的缺省字节对齐方式

    在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变缺省的对界条件:
    · 使用伪指令#pragma pack (n),C编译器将按照n个字节对齐。
    · 使用伪指令#pragma pack (),取消自定义字节对齐方式。

    另外,还有如下的一种方式:
    · __attribute((aligned (n))),让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。
    · __attribute__ ((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。

    4. 举例说明

    例1

    struct test
    {
    char x1;
    short x2;
    float x3;
    char x4;
    };

    由于编译器默认情况下会对这个struct作自然边界(有人说“自然对界”我觉得边界更顺口)对齐,结构的第一个成员x1,其偏移地址为0,占据了第1个字节。第二个成员x2为short类型,其起始地址必须2字节对界,因此,编译器在x2和x1之间填充了一个空字节。结构的第三个成员x3和第四个成员x4恰好落在其自然边界地址上,在它们前面不需要额外的填充字节。在test结构中,成员x3要求4字节对界,是该结构所有成员中要求的最大边界单元,因而test结构的自然对界条件为4字节,编译器在成员x4后面填充了3个空字节。整个结构所占据空间为12字节。

    例2

    #pragma pack(1) //让编译器对这个结构作1字节对齐
    struct test
    {
    char x1;
    short x2;
    float x3;
    char x4;
    };
    #pragma pack() //取消1字节对齐,恢复为默认4字节对齐

    这时候sizeof(struct test)的值为8。

    例3

    #define GNUC_PACKED __attribute__((packed))
    struct PACKED test
    {
    char x1;
    short x2;
    float x3;
    char x4;
    }GNUC_PACKED;

    这时候sizeof(struct test)的值仍为8。

    二、深入理解

    什么是字节对齐,为什么要对齐?
    TragicJun 发表于 2006-9-18 9:41:00 现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
          对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。显然在读取效率上下降很多。
    二.字节对齐对程序的影响:

            先让我们看几个例子吧(32bit,x86环境,gcc编译器):
    设结构体如下定义:
    struct A
    {
            int a;
            char b;
            short c;
    };
    struct B
    {
            char b;
            int a;
            short c;
    };
    现在已知32位机器上各种数据类型的长度如下:
    char:1(有符号无符号同)   
    short:2(有符号无符号同)   
    int:4(有符号无符号同)   
    long:4(有符号无符号同)   
    float:4        double:8
    那么上面两个结构大小如何呢?
    结果是:
    sizeof(strcut A)值为8
    sizeof(struct B)的值却是12

    结构体A中包含了4字节长度的int一个,1字节长度的char一个和2字节长度的short型数据一个,B也一样;按理说A,B大小应该都是7字节。
    之所以出现上面的结果是因为编译器要对数据成员在空间上进行对齐。上面是按照编译器的默认设置进行对齐的结果,那么我们是不是可以改变编译器的这种默认对齐设置呢,当然可以.例如:
    #pragma pack (2) /*指定按2字节对齐*/
    struct C
    {
            char b;
            int a;
            short c;
    };
    #pragma pack () /*取消指定对齐,恢复缺省对齐*/
    sizeof(struct C)值是8。
    修改对齐值为1:
    #pragma pack (1) /*指定按1字节对齐*/
    struct D
    {
            char b;
            int a;
            short c;
    };
    #pragma pack () /*取消指定对齐,恢复缺省对齐*/
    sizeof(struct D)值为7。
    后面我们再讲解#pragma pack()的作用.

    三.编译器是按照什么样的原则进行对齐的?

            先让我们看四个重要的基本概念:


    1.数据类型自身的对齐值:
          对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,单位字节。
    2.结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。
    3.指定对齐值:#pragma pack (value)时的指定对齐值value。
    4.数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值。
    有了这些值,我们就可以很方便的来讨论具体数据结构的成员和其自身的对齐方式。有效对齐值N是最终用来决定数据存放地址方式的值,最重要。有效对齐N,就是表示“对齐在N上”,也就是说该数据的"存放起始地址%N=0".而数据结构中的数据变量都是按定义的先后顺序来排放的。第一个数据变量的起始地址就是数据结构的起始地址。结构体的成员变量要对齐排放,结构体本身也要根据自身的有效对齐值圆整(就是结构体成员变量占用总长度需要是对结构体有效对齐值的整数倍,结合下面例子理解)。这样就不能理解上面的几个例子的值了。
    例子分析:
    分析例子B;
    struct B
    {
            char b;
            int a;
            short c;
    };
    假设B从地址空间0x0000开始排放。该例子中没有定义指定对齐值,在笔者环境下,该值默认为4。第一个成员变量b的自身对齐值是1,比指定或者默认指定对齐值4小,所以其有效对齐值为1,所以其存放地址0x0000符合0x0000%1=0.第二个成员变量a,其自身对齐值为4,所以有效对齐值也为4,所以只能存放在起始地址为0x0004到0x0007这四个连续的字节空间中,复核0x0004%4=0,且紧靠第一个变量。第三个变量c,自身对齐值为2,所以有效对齐值也是2,可以存放在0x0008到0x0009这两个字节空间中,符合0x0008%2=0。所以从0x0000到0x0009存放的都是B内容。再看数据结构B的自身对齐值为其变量中最大对齐值(这里是b)所以就是4,所以结构体的有效对齐值也是4。根据结构体圆整的要求,0x0009到0x0000=10字节,(10+2)%4=0。所以0x0000A到0x000B也为结构体B所占用。故B从0x0000到0x000B共有12个字节,sizeof(struct B)=12;其实如果就这一个就来说它已将满足字节对齐了,因为它的起始地址是0,因此肯定是对齐的,之所以在后面补充2个字节,是因为编译器为了实现结构数组的存取效率,试想如果我们定义了一个结构B的数组,那么第一个结构起始地址是0没有问题,但是第二个结构呢?按照数组的定义,数组中所有元素都是紧挨着的,如果我们不把结构的大小补充为4的整数倍,那么下一个结构的起始地址将是0x0000A,这显然不能满足结构的地址对齐了,因此我们要把结构补充成有效对齐大小的整数倍.其实诸如:对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,这些已有类型的自身对齐值也是基于数组考虑的,只是因为这些类型的长度已知了,所以他们的自身对齐值也就已知了.
    同理,分析上面例子C:
    #pragma pack (2) /*指定按2字节对齐*/
    struct C
    {
            char b;
            int a;
            short c;
    };
    #pragma pack () /*取消指定对齐,恢复缺省对齐*/
    第一个变量b的自身对齐值为1,指定对齐值为2,所以,其有效对齐值为1,假设C从0x0000开始,那么b存放在0x0000,符合0x0000%1=0;第二个变量,自身对齐值为4,指定对齐值为2,所以有效对齐值为2,所以顺序存放在0x0002、0x0003、0x0004、0x0005四个连续字节中,符合0x0002%2=0。第三个变量c的自身对齐值为2,所以有效对齐值为2,顺序存放
    在0x0006、0x0007中,符合0x0006%2=0。所以从0x0000到0x00007共八字节存放的是C的变量。又C的自身对齐值为4,所以C的有效对齐值为2。又8%2=0,C只占用0x0000到0x0007的八个字节。所以sizeof(struct C)=8.

    四.如何修改编译器的默认对齐值?

    1.在VC IDE中,可以这样修改:[Project]|[Settings],c/c++选项卡Category的Code Generation选项的Struct Member Alignment中修改,默认是8字节。
    2.在编码时,可以这样动态修改:#pragma pack .注意:是pragma而不是progma.

    五.针对字节对齐,我们在编程中如何考虑?
            如果在编程的时候要考虑节约空间的话,那么我们只需要假定结构的首地址是0,然后各个变量按照上面的原则进行排列即可,基本的原则就是把结构中的变量按照类型大小从小到大声明,尽量减少中间的填补空间.还有一种就是为了以空间换取时间的效率,我们显示的进行填补空间进行对齐,比如:有一种使用空间换时间做法是显式的插入reserved成员:
                 struct A{
                   char a;
                   char reserved[3];//使用空间换时间
                   int b;
    }

    reserved成员对我们的程序没有什么意义,它只是起到填补空间以达到字节对齐的目的,当然即使不加这个成员通常编译器也会给我们自动填补对齐,我们自己加上它只是起到显式的提醒作用.

    六.字节对齐可能带来的隐患:

            代码中关于对齐的隐患,很多是隐式的。比如在强制类型转换的时候。例如:
    unsigned int i = 0x12345678;
    unsigned char *p=NULL;
    unsigned short *p1=NULL;

    p=&i;
    *p=0x00;
    p1=(unsigned short *)(p+1);
    *p1=0x0000;
    最后两句代码,从奇数边界去访问unsignedshort型变量,显然不符合对齐的规定。
    在x86上,类似的操作只会影响效率,但是在MIPS或者sparc上,可能就是一个error,因为它们要求必须字节对齐.

    七.如何查找与字节对齐方面的问题:

    如果出现对齐或者赋值问题首先查看
    1. 编译器的big little端设置
    2. 看这种体系本身是否支持非对齐访问
    3. 如果支持看设置了对齐与否,如果没有则看访问时需要加某些特殊的修饰来标志其特殊访问操作

    举例:

    [cpp] view plain copy
    1. #include <stdio.h>  
    2. main()  
    3. {  
    4. struct A {  
    5.     int a;  
    6.     char b;  
    7.     short c;  
    8. };  
    9.   
    10. struct B {  
    11.     char b;  
    12.     int a;  
    13.     short c;  
    14. };  
    15.   
    16. #pragma pack (2) /*指定按2字节对齐*/  
    17. struct C {  
    18.     char b;  
    19.     int a;  
    20.     short c;  
    21. };  
    22. #pragma pack () /*取消指定对齐,恢复缺省对齐*/  
    23.   
    24.   
    25.   
    26. #pragma pack (1) /*指定按1字节对齐*/  
    27. struct D {  
    28.     char b;  
    29.     int a;  
    30.     short c;  
    31. };  
    32. #pragma pack ()/*取消指定对齐,恢复缺省对齐*/  
    33.   
    34. int s1=sizeof(struct A);  
    35. int s2=sizeof(struct B);  
    36. int s3=sizeof(struct C);  
    37. int s4=sizeof(struct D);  
    38. printf("%d\n",s1);  
    39. printf("%d\n",s2);  
    40. printf("%d\n",s3);  
    41. printf("%d\n",s4);  
    42. }  


    输出:

    8

    12

    8

    7

     

    修改代码:

    struct A {
       // int a;
        char b;
        short c;
    };

    struct B {
        char b;
       // int a;
        short c;
    };

    输出:

    4

    4

    输出都是4,说明之前的int影响对齐!

    看图就明白了

    展开全文
  • 一、为什么要字节对齐? 32位CPU是以双字(DWORD)为单位进行数据传输的,因此我们的数据无论是8位或16位都是以双字进行数据传输。 比如,一个int类型4字节的数据如果放在上图内存地址1开始的位置,那么这个数据...

    一、为什么要字节对齐?

    32位CPU是以双字(DWORD)为单位进行数据传输的,因此我们的数据无论是8位或16位都是以双字进行数据传输。
    比如,一个int类型4字节的数据如果放在上图内存地址1开始的位置,那么这个数据占用的内存地址为1~4,那么这个数据就被分为了2个部分,一个部分在地址0~3中,另外一部分在地址4~7中,又由于32位CPU以双字进行传输,所以,CPU会分2次进行读取,一次先读取地址0~3中内容,再一次读取地址4~7中数据,最后CPU提取并组合出正确的int类型数据,舍弃掉无关数据。那么反过来,如果我们把这个int类型4字节的数据放在上图从地址0开始的位置会怎样呢?读到这里,也许你明白了,CPU只要进行一次读取就可以得到这个int类型数据了。没错,就是这样,这次CPU只用了一个周期就得到了数据,由此可见,对内存数据的摆放是多么重要啊,摆放正确位置可以减少CPU的使用资源。

    二、内存对齐原则

    那么,内存对齐有哪些原则呢?我总结了一下大致分为三条:
    第一条:第一个成员的首地址为0
    第二条:每个成员的首地址是自身大小的整数倍
           第二条补充:以4字节对齐为例,如果自身大小大于4字节,都以4字节整数倍为基准对齐。
    第三条:最后以结构总体对齐。
            第三条补充:以4字节对齐为例,取结构体中最大成员类型倍数,如果超过4字节,都以4字节整数倍为基准对齐。(其中这一条还有个名字叫:“补齐”,补齐的目的就是多个结构变量挨着摆放的时候也满足对齐的要求。)
        上述的三原则听起来还是比较抽象,那么接下来我们通过一个例子来加深对内存对齐概念的理解,下面是一个结构体,我们动手算出下面结构体一共占用多少内存?假设我们以32位平台并且以4字节对齐方式:

    #pragma pack(4)
    typedef struct MemAlign
    {
    	char a[18];
    	double b;	
    	char c;
    	int d;	
    	short e;	
    }MemAlign;

    下图为对齐后结构如下:

    我们就以这个图来讲解是如何对齐的:
    第一个成员(char a[18]):首先,假设我们把它放到内存开始地址为0的位置,由于第一个成员占18个字节,所以第一个成员占用内存地址范围为0~18。
    第二个成员(double b):由于double类型占8字节,又因为8字节大于4字节,所以就以4字节对齐为基准。由于第一个成员结束地址为18,那么地址18并不是4的整数倍,我们需要再加2个字节,也就是从地址20开始摆放第二个成员。
    第三个成员(char c):由于char类型占1字节,任意地址是1字节的整数倍,所以我们就直接将其摆放到紧接第二个成员之后即可。
    第四个成员(int d):由于int类型占4字节,但是地址29并不是4的整数倍,所以我们需要再加3个字节,也就是从地址32开始摆放这个成员。
    第五个成员(short e):由于short类型占2字节,地址36正好是2的整数倍,这样我们就可以直接摆放,无需填充字节,紧跟其后即可。
        这样我们内存对齐就完成了。但是离成功还差那么一步,那是什么呢?对,是对整个结构体补齐,接下来我们就补齐整个结构体。那么,先让我们回顾一下补齐的原则:“以4字节对齐为例,取结构体中最大成员类型倍数,如果超过4字节,都以4字节整数倍为基准对齐。”在这个结构体中最大类型为double类型(占8字节),又由于8字节大于4字 节,所以我们还是以4字节补齐为基准,整个结构体结束地址为38,而地址38并不是4的整数倍,所以我们还需要加额外2个字节来填充结构体,如下图红色的就是补齐出来的空间:、

    我们可以通过一个程序来观察内存变化:

    #include <stdio.h>
    #include <memory.h>
     
    // 通过预编译来通知编译器我们以4字节对齐
    #pragma pack(4)
     
    // 用于测试的结构体
    typedef struct MemAlign
    {
    	char a[18];	// 18 bytes
    	double b;	// 08 bytes	
    	char c;		// 01 bytes
    	int d;		// 04 bytes
    	short e;	// 02 bytes
    }MemAlign;
     
    int main()
    {
    	// 定义一个结构体变量
    	MemAlign m;
    	// 定义个以指向结构体指针
    	MemAlign *p = &m;
    	// 依次对各个成员进行填充,这样我们可以
    	// 动态观察内存变化情况
    	memset( &m.a, 0x11, sizeof(m.a) );
    	memset( &m.b, 0x22, sizeof(m.b) );
    	memset( &m.c, 0x33, sizeof(m.c) );
    	memset( &m.d, 0x44, sizeof(m.d) );
    	memset( &m.e, 0x55, sizeof(m.e) );
    	// 由于有补齐原因,所以我们需要对整个
    	// 结构体进行填充,补齐对齐剩下的字节
    	// 以便我们可以观察到变化
    	memset( &m, 0x66, sizeof(m) );
    	// 输出结构体大小
    	printf( "sizeof(MemAlign) = %d", sizeof(m) );
    }

     

    展开全文
  • 一、字节对齐字节对齐-1变量字节对齐-2结构体二、结构体链表 https://www.cnblogs.com/corvoh/p/5595130.html负数的原码和反码,补码&数据类型强制转换负数的源码=首位1+对应正数后七位源码:比如-128源码=1+000...
  • C语言结构体对齐规则与0字节数组@[TOC](C语言结构体对齐规则与0字节数组)C语言结构体对齐规则对齐规则说明 C语言结构体对齐规则 不同的编译器和系统默认的对齐规则会有差异,这里我使用的32bit的MinGW。 对齐规则...
  • 首先我们先看看下面的C语言的结构体:[cpp] view plaincopytypedef stru...
  • C语言内存对齐

    2019-09-04 09:33:42
    1. 结构(struct)(或联合(union))体的数据成员,第一个数据成员放在offset为0的地方,如果第一个数据成员为某个复合结构的子成员,则要根据子成员的类型存放在对应的整数倍的地址上。 2. 结构体成员按自身长度 “ ...
  • C语言中,结构是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构、联合等)的数据单元。在结构中,编译器为结构的每个成员按其自然...
  • C语言对齐

    2012-09-12 17:48:37
    上面主要考点在于c语言的sizeof 以及 C语言中的对齐的概念。其中主要的思想是读取数据的时候通过空间换取时间的策略。 1. 什么是字节对齐? 在C语言中,结构是一种复合数据类型,其构成元素既可以是基本数据类型...
  • C语言 字节对齐

    2016-09-21 19:19:12
    原文地址:http://blog.csdn.net/andy572633/article/details/7213465空间换时间结构体内成员按自身长度自对齐#### 结构体的总大小为结构体的有效对齐值的整数倍结构体的有效对齐值的确定(会有编译器优化问题):1)...
  • c语言结构体对齐

    2010-11-03 14:02:00
    <br />C语言结构体对齐也是老生常谈的话题了。基本上是面试题的必考题。结构体到底怎样对齐?下面总结了对齐原则,在没有#pragma pack宏的情况下: 原则1、普通数据成员对齐规则:第一个数据成员放在...
  • C语言中,结构是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构、联合等)的数据单元。
  • C语言字节对齐

    2013-10-26 15:36:25
    c语言struct编译器数据结构alignment 文章最后本人做了一幅图,一看就明白了,这个问题网上讲的不少,但是都没有把问题说透。  一、概念 对齐跟数据在内存中的位置有关。如果一个变量的内存地址正好位于它长度...
  • C语言边界对齐问题

    2018-04-17 20:01:51
    如果体系结构是不对齐的,成员将会一个挨一个存储,显然对齐更浪费了空间。那么为什么要使用对齐呢?体系结构对齐和不对齐,是在时间和空间上的一个权衡。对齐节省了时间。假 设一个体系结构的字长为w,那么它同时...
  • C语言字节对齐

    2019-07-29 20:23:22
    带有#pragmapack(n)指令情况下的字节对齐 根据百度百科的解释,编译器中提供了#pragmapack(n)来设定变量以n字节对齐方式。n字节对齐就是说变量存放的起始地址的偏移量有两种情况...结构的总大小也有个约束条件,分...
  • 接上一篇:C语言内存对齐详解(1)  VC对结构的存储的特殊处理确实提高CPU存储变量的速度,但是有时候也带来了一些麻烦,我们也屏蔽掉变量默认的对齐方式,自己可以设定变量的对齐方式。VC 中提供了#pragma pack(n...
  • C语言结构体对齐也是老生常谈的话题了。基本上是面试题的必考题。结构体到底怎样对齐?下面总结了对齐原则,在没有#pragma pack宏的情况下: 原则1、普通数据成员对齐规则:第一个数据成员放在offset为0的地方,以后...
  • C语言内存对齐和pack pragma lxd_0401 【转】转帖:typedef __PACKED struct字节对齐详解 关键字:#pragma pack typedef __PACKED struct 结构 字节 对齐 一.什么是字节对齐,为什么要对齐...
  • C语言字节对齐 什么是字节对齐 现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 703
精华内容 281
关键字:

c语言结构对齐

c语言 订阅