C语言精要总结-内存地址对齐与struct大小判断篇

在笔试时,经常会遇到结构体大小的问题,实际就是在考内存地址对齐。在实际开发中,如果一个结构体会在内存中高频地分配创建,那么掌握内存地址对齐规则,通过简单地自定义对齐方式,或者调整结构体成员的顺序,可以有效地减少内存使用。另外,一些不用边界对齐、可以在任何地址(包括奇数地址)引用任何数据类型的的机器,不在本文讨论范围之内。

什么是地址对齐

计算机读取或者写入存储器地址时,一般以字(因系统而异,32位系统为4个字节)大小(N)的块来执行操作。数据对齐就是将数据存储区的首地址对齐字大小(N)的某个整数倍地址。为了对齐数据,有时需要在物理上相邻的两个数据之间保留或者插入一些无意义的字节。内存对齐本事编译器考虑是事情,但在C、C++语言中,可以人为修改对齐方式。

为什么要地址对齐

计算机会保证存储器字的大小,至少要大于等于计算机支持的最大原始数据类型的大小。

这样,一个原始数据类型就一定可以存放在一个存储器字中,如果保证了数据是地址对齐的,那么访问一个原始数据就可以保证只访问一个存储器字,这有利于提高效率。如下图

反之,如果一个数据不是按字大小内存对齐的(也就是最高字节与最低字节落在两个字中),那么,这个数据很可能落在两个存储器字中。如下图

这时,计算机必须将数据访问分割成多个存储器字访问,这需要更多复杂的操作。甚至,当这两个字都不存在一个存储器页中是,处理器还必须在执行指令之前验证两个页面是否存在,否则可能会发生未命中错误。另外,对一个存储器字的操作是原子的,如果拆分成两次访问,也可能引发一些并发问题,比如从两个字读出来的数据段拼起来可能不是真实的数据,因为有另外的设备在写。

起始地址约束(对齐系数)

C++11 引入 alignof 运算符,该运算符返回指定类型的对齐系数(以字节为单位),其中宏__alignof在linux gcc或者windows都有定义。

下面一段程序取几个常用的基本数据类型。

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 int main(){
 4      printf("char: %d\n",__alignof(char));
 5      printf("short: %d\n",__alignof(short));
 6      printf("int: %d\n",__alignof(int));
 7      printf("long: %d\n",__alignof(long));
 8      printf("double: %d\n",__alignof(double));
 9      return 0;
10 }

分别在linux和windows下编译运行,得到如下结果

类型 Linux Windows
char 1 1
short 2 2
int 4 4
long 8 4
double 8 8

可以看到Linux下与Windows下,long类型对齐系数不一样。并且对齐系数与类型自身所占的大小也基本一致。

地址对齐对struct大小的影响

地址对齐主要影响到一些复杂的数据结构,比如struct结构体,因为有了内存地址对齐,大多数的struct实际占用的大小显得有些诡异。(注意,一个结构体的大小很可能超过存储器字大小,这时跨字读取数据已不可避免。但结构体本身及其成员还是需要继续遵守对齐规则)

拿一个很简单的结构体align1为例

1 struct align1
2 {
3     char a;
4     int b;
5     char c;
6 } sim[2];

如果不考虑任何对齐问题,只考虑结构体中每个成员应该占用的大小,很显然每个结构align1定义的变量是1(char)+4(int)+1(char)共6个字节。但是实际上(至少在windows上)它占用了12个字节,原因就在于它有按照一定的规则进行内存地址对齐。下面是笔者参考各方面资料总结的四点结构体边界对齐需满足的要点:

  1. 结构体变量本身的起始位置,必须是结构成员中对边界要求最严格(对齐系数最大)的数据类型所要求的位置

    1. 比如double类型的起始地址约束(对齐系数)为8 ,那如果一个结构体包含double类型,则结构体变量本身的起始地址要能被8整除
  2. 成员必须考虑起始地址约束(对齐系数)和本身的大小,在windows和linux下,都可以使用__alignof(type)来查看type类型(原始基本类型)的起始地址约束(对齐系数)。
  3. 如果成员也是struct union之类的类型,则整体要照顾到部分,整体要满足成员能符合起始地址约束
  4. 结构体可能需要在其所有成员之后填充一些字节,以保证在分配结构体数组之后,每个数组元素要满足起始地址约束。

让我们再来仔细研究下结构体 align1定义的实例数组 sim[2]。我们先约定:占用即表示本身大小及其后的空余空间。

按要点1,则sim[0]的起始地址必须能被4整除,假设这个其实地址是4n,其中成员a的起始地址也是sim[0]的起始地址(按要点2,因为a 为char类型,对齐系数为1,放哪都可以),a占用一个字节。

按要点2,成员b的起始地址必须能被4整除,很显然不能直接放在成员a的后面(起始地址是4n+1,不能被4整除),所以需要跳过3个字节存放b,那么成员a实际占用了4个字节(我们的约定)。

同理,成员c可以直接放在b成员后面(起始地址是(4(n+2)),而且肯定可以被1整除)。

至此,sim[0]已经占用了9个字节了,但按照要点4,因为数组是连续的,为了保证其后的数组成员sim[1]也符合首地址能被4整除,必须将sim[0]的空间先后延长3个字节至(4(n+3))。所以sim[0]实际要占用12个字节。

当然一个结构体不能有两个大小,哪怕其后不再放align1类型的变量,系统也要为这个变量分配最大的12个字节空间。

用一个简单的占位符来表示存储,可表示为

1 // --sim[0]---- ----sim[1]--
2 // a---bbbbc--- a---bbbbc---

用图片描述如图(一个正方形表示一个字节空间)

很显然,这个结构体对空间利用率不高,有50%的空间浪费。通过调整成员定义的顺序,完全可以优化空间利用。个人的经验是,本身占用空间大的(如double类型)应该尽量往前面放。下面我们将int b;调整到第一位定义

1 struct align2
2 {
3     int b;
4     char a;
5     char c;
6 } sim[2];

通过分析不难发现,新的结构占用8个字节的空间。如图

空间利用率提高到75%。当一个结构体足够复杂时,通过调整顺序或者自定义对齐方式,压缩带来的空间是非常可观的。虽然,随着内存越做越大,一般情况下开发已经不需要考虑这种问题。但是在海量服务下,如何死抠性能和减少资源占用依然是开发需要考虑的问题。就像现在单机几十万并发tcp连接已经不难做到,为什么还是有很多人在研究C10M(单机千万连接)。

下面的程序是基于以上四项要点做的测试,特别注意MyStruct7,因为其中的成员包含数组。至于成员包含union的就比较简单了,一般可以直接把union用union中最大的成员替换考虑,另外注意考虑要点3。另外,在一个位段定义中使用非int 、signed int 、或者unsigned int类型,位段定义将变成一个普通的结构体,对齐原则也就遵从结构体的对齐原则。

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
/************************************************************************/
/* 这里约定:占用即表示本身大小及其后的空余空间
/************************************************************************/
struct MyStruct1 // 起始地址能被8整除
{
char a; // 8
double b; // 8
float c; // 4
int d; // 4
} m1; // 24

struct MyStruct2 // 起始地址能被8整除
{
int a; // 4
float b; // 4
char c; // 8 // 后面 double的起始地址要能被8 整除,所以c补齐8个字节
double d; // 8
} m2; // 24

struct MyStruct3 // 起始地址能被8整除
{
short a; // 2
char b; // 6 // 同理,后面的元素的起始地址要能被 8 整除,所以b只要占用6
double c; // 8
int d; // 8 // 需要在其后填充一些字节,以保证在分配数组之后,每个数组元素要满足起始地址约束
} m3; // 24

struct MyStruct4
{
char a; // 2 // 能被4整除的地址 +2之后能被2整除,所以a只要补1个字节
short b; // 2
int c; // 4
} m4; // 8

struct MyStruct5 // 起始地址能被8整除
{
double a; // 8
float b; // 4
int c; // 4
short d; // 2
char e; // 6 因为后面紧紧挨着的MyStruct5 变量(在分配数组的时候)起始地址也要能被8整除,所以这个结构体总的大小必须是8的整数倍
} m5; // 24

struct MyStruct6 // 除4对齐
{
short a; // 2
char b; // 2
long c; // 4
short d; // 4 // 保证数组后面的元素也符合规则 (结构体首地址可以除4)
} m6; // 12

struct MyStruct7 // 4 对齐
{
int a; // 4
char b; // 2
short c; // 2
char d[6]; // 8
} m7; // 16

int main(){
printf("m1 size : %d\n",sizeof m1);
printf("m2 size : %d\n",sizeof m2);
printf("m3 size : %d\n",sizeof m3);
printf("m4 size : %d\n",sizeof m4);
printf("m5 size : %d\n",sizeof m5);
printf("m6 size : %d\n",sizeof m6);
printf("m7 size : %d\n",sizeof m7);

// offsetof 函数用来计算成员离结构体首地址偏移的字节数
printf("MyStruct1 b offset : %d\n",offsetof(struct MyStruct1,b)); // b偏移8个字节,所以成员a占用8个字节
printf("MyStruct2 d offset : %d\n",offsetof(struct MyStruct2,d)); // d偏移了16个字节
printf("MyStruct3 c offset : %d\n",offsetof(struct MyStruct3,c)); // 偏移8
printf("MyStruct4 b offset : %d\n",offsetof(struct MyStruct4,b)); // 偏移2
printf("MyStruct5 e offset : %d\n",offsetof(struct MyStruct5,e)); // 偏移16
printf("MyStruct6 c offset : %d\n",offsetof(struct MyStruct6,c)); // 偏移4
printf("MyStruct7 c offset : %d\n",offsetof(struct MyStruct7,c)); // 偏移
system("pause");
return 0;
}

测试代码

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <stddef.h>
 4 /************************************************************************/
 5 /* 这里约定:占用即表示本身大小及其后的空余空间
 6 /************************************************************************/
 7 struct MyStruct1    // 起始地址能被8整除
 8 {
 9     char a;            // 8
10     double b;        // 8
11     float c;        // 4
12     int d;            // 4
13 } m1;                // 24
14
15 struct MyStruct2    // 起始地址能被8整除
16 {
17     int a;            // 4
18     float b;        // 4
19     char c;            // 8 // 后面 double的起始地址要能被8 整除,所以c补齐8个字节
20     double d;        // 8
21 } m2;                // 24
22
23 struct MyStruct3    // 起始地址能被8整除
24 {
25     short a;        // 2
26     char b;            // 6 // 同理,后面的元素的起始地址要能被 8 整除,所以b只要占用6
27     double c;        // 8
28     int d;            // 8 // 需要在其后填充一些字节,以保证在分配数组之后,每个数组元素要满足起始地址约束
29 } m3;                // 24
30
31 struct MyStruct4
32 {
33     char a;            // 2 // 能被4整除的地址 +2之后能被2整除,所以a只要补1个字节
34     short b;        // 2
35     int c;            // 4
36 } m4;                // 8
37
38 struct MyStruct5    // 起始地址能被8整除
39 {
40     double a;        // 8
41     float b;        // 4
42     int c;            // 4
43     short d;        // 2
44     char e;            // 6 因为后面紧紧挨着的MyStruct5 变量(在分配数组的时候)起始地址也要能被8整除,所以这个结构体总的大小必须是8的整数倍
45 } m5;                // 24
46
47 struct MyStruct6    // 除4对齐
48 {
49     short a;        // 2
50     char b;            // 2
51     long c;            // 4
52     short d;        // 4  // 保证数组后面的元素也符合规则 (结构体首地址可以除4)
53 } m6;                // 12
54
55 struct MyStruct7    // 4 对齐
56 {
57     int a;            // 4
58     char b;            // 2
59     short c;        // 2
60     char d[6];        // 8
61 } m7;                    // 16
62
63 int main(){
64     printf("m1 size : %d\n",sizeof m1);
65     printf("m2 size : %d\n",sizeof m2);
66     printf("m3 size : %d\n",sizeof m3);
67     printf("m4 size : %d\n",sizeof m4);
68     printf("m5 size : %d\n",sizeof m5);
69     printf("m6 size : %d\n",sizeof m6);
70     printf("m7 size : %d\n",sizeof m7);
71
72     // offsetof 函数用来计算成员离结构体首地址偏移的字节数
73     printf("MyStruct1 b offset : %d\n",offsetof(struct MyStruct1,b));    // b偏移8个字节,所以成员a占用8个字节
74     printf("MyStruct2 d offset : %d\n",offsetof(struct MyStruct2,d));    // d偏移了16个字节
75     printf("MyStruct3 c offset : %d\n",offsetof(struct MyStruct3,c));    // 偏移8
76     printf("MyStruct4 b offset : %d\n",offsetof(struct MyStruct4,b));    // 偏移2
77     printf("MyStruct5 e offset : %d\n",offsetof(struct MyStruct5,e));    // 偏移16
78     printf("MyStruct6 c offset : %d\n",offsetof(struct MyStruct6,c));    // 偏移4
79     printf("MyStruct7 c offset : %d\n",offsetof(struct MyStruct7,c));    // 偏移
80     system("pause");
81     return 0;
82 }
时间: 2024-10-10 17:38:40

C语言精要总结-内存地址对齐与struct大小判断篇的相关文章

嵌入式Linux C语言(六)——内存字节对齐

嵌入式Linux C语言(六)--内存字节对齐 一.内存字节对齐简介 1.内存字节对齐 计算机中内存空间都是按照字节划分的,从理论上讲对任何类型的变量的访问可以从任何地址开始,但是在程序实际编译过程中,编译器会对数据类型在编译过程中进行优化对齐,编译器会将各种类型数据按照一定的规则在空间上排列,而不是顺序的排放,这就是内存字节对齐. 2.内存字节对齐原因 不同硬件平台对存储空间的处理是不同的.一些平台对某些特定类型的数据只能从某些特定地址开始存取.比如某些架构的CPU在访问一个没有进行对齐的变量

嵌入式C语言自我修养 07:地址对齐那些事儿

7.1 属性声明:aligned GNU C 通过 attribute 来声明 aligned 和 packed 属性,指定一个变量或类型的对齐方式.这两个属性用来告诉编译器:在给变量分配存储空间时,要按指定的地址对齐方式给变量分配地址.如果你想定义一个变量,在内存中以8字节地址对齐,就可以这样定义. int a __attribute__((aligned(8)); 通过 aligned 属性,我们可以直接显式指定变量 a 在内存中的地址对齐方式.aligned 有一个参数,表示要按几字节对齐

【转】内存地址对齐运算

做地址对齐的代码: #define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) ) //为了满足需要内存对齐的系统 这段代码做的事情就是,给定一个变量n,算出这个变量对齐到某个字长(整型的字节数)整数倍的字节数.这段代码有些难以理解.那么慢慢分析下吧. 假设有一个地址n,要把n按m对齐,无非就是找到大于等于n且整除m的最小的那个数. 我们定义一个宏函数F,它计算n按m对齐的结果,则按照上段代码的逻辑,F定义为: #def

[C/C++]_[中级]_[数据地址对齐]

场景: 1. 有些频繁使用的指针变量地址不对齐的话运行效率和对齐后的运行效率差别很大,所以在创建堆空间时,有必要对内存地址对齐提高运行效率. 2. 有些音视频处理的代码或者说自定义的malloc基本都是地址对齐的. 3. 使用原子访问的互锁函数时,InterlockedExchangeAdd都需要地址对齐. 4. 主要还是宏APR_ALIGN, 这个说是Apache源码里,就借用一下吧. 解决方案: 1. 其实就是让地址值对对齐量求模为0, 地址值最多增加n-1个偏移地址就可就可以整出n.  &

11.用C对32位内存地址的访问方式

使用一个32位处理器,要对一个32位的内存地址进行访问,可以这样定义 #define RAM_ADDR     (*(volatile unsigned long *)0x0000555F)     然后就可以用C语言对这个内存地址进行读写操作了     读:tmp = RAM_ADDR:     写:RAM_ADDR = 0x55: 定义volatile是因为它的值可能会改变,大家都知道为什么改变了: 如果在一个循环操作中需要不停地判断一个内存数据,例如要等待RAM_ADDR的I标志位置位,因

【信息表示】地址对齐

本节研究地址对齐的相关问题: 地址对齐 说明几点: (1)地址对齐可以简化处理器和存储器系统之间的硬件设计:如果可以保证所有的int类型地址对齐成4的倍数,就可以使用一个存储器操作读或写值,相反如果一个int型的变量存放在奇地址上,那么要进行两次存储器读后进行拼凑成int型变量才可以: (2)在GCC中,2字节数据类型(如short)地址对齐必须是2的倍数,因此地址最低位必须是0,而较大的数据类型(如int.int*.float.包括long long.double.long double)的地

C语言结构体变量内存分配与地址对齐

地址对齐简单来说就是为了提高访问内存的速度. 数组的地址分配比较简单,由于数据类型相同,地址对齐是一件自然而然的事情. 结构体由于存在不同基本数据类型的组合,所以地址对齐存在不同情况,但总体来说有以下规则: 原则1:数据成员对齐规则:结构的数据成员,第一个数据成员放在偏移量(offset)为0的地方,以后每个数据成员存储的起始位置要从该成员大小的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储). 原则2:收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大

C语言:内存字节对齐详解[转载]

一.什么是对齐,以及为什么要对齐: 1. 现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐. 2. 对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同.一些平台对某些特定类型的数据只能从某些特定地址开始存取.其他平台可能没有这种情况, 但是最常见的是如果不按照适合其平台的要求对数据存放进行对齐

C语言 结构体的内存对齐问题与位域

http://blog.csdn.net/xing_hao/article/details/6678048 一.内存对齐 许多计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对 齐,而这个k则被称为该数据类型的对齐模数(alignment modulus).当一种类型S的对齐模数与另一种类型T的对齐模数的比值是大于1的整数,我们就称类型S的对齐要求比T强(严格),而称T比S弱(宽 松).这种强制的要求一来简化了处