博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
从C语言结构对齐重谈变量存放地址与内存分配
阅读量:7219 次
发布时间:2019-06-29

本文共 7266 字,大约阅读时间需要 24 分钟。

【@.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语言编译环境可以参考我的博客配置一个编译环境,或者。

考虑下面代码,

 

#include 
#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 
#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的是按照规则之后的补充位↓

好了,现在可以考虑将结构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 
#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

你可能感兴趣的文章
***微信公众平台开发: 获取用户基本信息+OAuth2.0网页授权
查看>>
第二章 例题2-2 在屏幕上显示两个短句
查看>>
【转】iOS学习之适配iOS10
查看>>
OC语言BLOCK和协议
查看>>
C++创建一个动态链接库工程
查看>>
(六)maven之本地仓库
查看>>
如何使用 SPICE client (virt-viewer) 来连接远程虚拟机桌面?
查看>>
CentOS7
查看>>
linux高编IO-------tmpnam和tmpfile临时文件
查看>>
微信的机器人开发
查看>>
从零开始学Java(二)基础概念——什么是"面向对象编程"?
查看>>
近期面试总结(2016.10)
查看>>
CodeForces 525D Arthur and Walls :只包含点和星的矩阵,需要将部分星变成点使满足点组成矩形 : dfs+思维...
查看>>
积累_前辈的推荐
查看>>
strcpy和memcpy的区别《转载》
查看>>
在windows平台下electron-builder实现前端程序的打包与自动更新
查看>>
DroidPilot V2.1 手写功能特别版
查看>>
COOKIE欺骗
查看>>
js 强转规范解读
查看>>
ACdream - 1735:输油管道
查看>>