你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

C语言允许你干预“内存对齐”

[复制链接]
gaosmile 发布时间:2020-5-13 13:15
什么是内存对齐

元素是按照定义顺序一个一个放到内存中去的,但并不是紧密排列的。从结构体存储的首地址开始,每个元素放置到内存中时,它都会认为内存是按照自己的大小(通常它为4或8)来划分的,因此元素放置的位置一定会在自己宽度的整数倍上开始,这就是所谓的内存对齐。

编译器为程序中的每个“数据单元”安排在适当的位置上。C语言允许你干预“内存对齐”。如果你想了解更加底层的秘密,“内存对齐”对你就不应该再模糊了。

以一个例子开始了解

理论上,int占4byte,char占一个byte,那么将它们放到一个结构体中应该占4+1=5byte;但是实际上,通过运行程序得到的结果是8 byte,这就是内存对齐所导致的。

#include<stdio.h>
0 B0 @$ v, A5 k4 l0 r
! D. {0 D6 t4 z( R: k1 q% j2 mstruct{, f6 J. f4 x/ d
    int x;, ]- p5 e4 A- k. _; Q1 p% I0 y
    char y;- E0 F+ j; Q- [" e
}Test;
+ G9 b4 D0 T: S$ y1 ~0 w; J1 c6 P
8 y& c! m% q, }' z$ \. lint main()6 S- c0 e3 B" ^. n. z) I/ w
{
; d' d; M4 X. e    printf("%d\n",sizeof(Test)); // 输出8不是5/ Q  Z. M5 R3 U* u
    return 0;
( z# Z) [. b& r0 j  K& \}

& N/ r( u. L- k( Q# j) f为什么要内存对齐
  • 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

  • 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。


    . U! F) f7 I9 _: G, X
  • 假如没有内存对齐机制,数据可以任意存放,现在一个int变量存放在从地址1开始的联系四个字节地址中,该处理器去取数据时,要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),最后留下的两块数据合并放入寄存器。这需要做很多工作。

  • 现在有了内存对齐的,int类型数据只能存放在按照对齐规则的内存中,比如说0地址开始的内存。那么现在该处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作,提高了效率。


    ( F# C- i1 f* H6 x- N% Z
内存对齐规则
  • 基本类型的对齐值就是其sizeof值;

  • 数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行;

  • 结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行;


    * R; |2 Y3 z. T" A* w% D2 _
//2020.05.12 公众号:C语言与CPP编程! u! F0 x, ?. y8 r% A# A  t
#include<stdio.h>
0 B! {  |, U# g9 Q5 Estruct7 }, U6 U- h6 U" M7 Q; ~+ a
{
# V2 R  p: g2 Y9 K, }    int i;
# O  e, A( k9 b; U    char c1;
; s1 I/ y) N2 w" {    char c2;
/ Y- M& N4 C% {; v5 O}Test1;) Y7 |& ~% a- w) u6 G, W4 @7 d

9 ~! t2 ~1 x2 Pstruct{* H2 ?7 G; w( p% s+ f% p8 @9 ^
    char c1;
4 W$ G5 j, G- a9 t, z    int i;
( B$ l& w1 U8 M+ r7 H: D    char c2;
6 f! b. `/ o- y5 a! S) Z9 f3 ]}Test2;% P# w1 N7 g8 w2 O6 P6 `& T( R
6 o0 o' l6 _( O3 ^( y
struct{5 C4 @5 I6 P" K# D/ _
    char c1;
' w+ j( s5 f1 T- }: p! D8 H$ i' J( C    char c2;
5 V- x1 L# ?' i# v    int i;$ k' g) Z0 h2 q& s  n: Z
}Test3;( [. y( k' X& T0 j
/ N3 c, l) a  D3 `
int main()
) a9 n! C. T. X; v* p{, r0 d1 ]* c5 S" d
    printf("%d\n",sizeof(Test1));  // 输出8
- ^9 P: A5 G; [$ ?% @9 C. U. S    printf("%d\n",sizeof(Test2));  // 输出12
, [" l0 O2 U4 h  m+ X/ C    printf("%d\n",sizeof(Test3));  // 输出8
' |0 E) ]! a% _# Q8 P. i. A    return 0;
& t$ [4 `* i: u}

1 m$ D2 O7 ]$ Z$ e0 E) Q  J1 C$ n; r

默认#pragma pack(4),且结构体中最长的数据类型为4个字节,所以有效对齐单位为4字节,下面根据上面所说的规则以第二个结构体来分析其内存布局:首先使用规则1,对成员变量进行对齐:

  • sizeof(c1) = 1 <= 4(有效对齐位),按照1字节对齐,占用第0单元;

  • sizeof(i) = 4 <= 4(有效对齐位),相对于结构体首地址的偏移要为4的倍数,占用第4,5,6,7单元;

  • sizeof(c2) = 1 <= 4(有效对齐位),相对于结构体首地址的偏移要为1的倍数,占用第8单元;


    9 i( q% U2 v8 w

然后使用规则2,对结构体整体进行对齐:

第二个结构体中变量i占用内存最大占4字节,而有效对齐单位也为4字节,两者较小值就是4字节。因此整体也是按照4字节对齐。由规则1得到s2占9个字节,此处再按照规则2进行整体的4字节对齐,所以整个结构体占用12个字节。

根据上面的分析,不难得出上面例子三个结构体的内存布局如下:

微信图片_20200513131322.jpg 例子三个结构体的内存布局

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

在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变缺省的对界条件:

  • 使用伪指令#pragma pack (n),C编译器将按照n个字节对齐。
  • 使用伪指令#pragma pack (),取消自定义字节对齐方式。2 k; @7 X' g/ s# o& l4 X) k

另外,还有如下的一种方式:

  • __attribute((aligned (n))),让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。

  • attribute((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。

    # V1 g5 P7 a% T! H, @

不同平台上编译器的 pragma pack 默认值不同。而我们可以通过预编译命令#pragma pack(n), n= 1,2,4,8,16来改变对齐系数。

例如,对于上个例子的三个结构体,如果前面加上#pragma pack(1),那么此时有效对齐值为1字节,此时根据对齐规则,不难看出成员是连续存放的,三个结构体的大小都是6字节。

微信图片_20200513131326.jpg 有效对齐值为1字节

如果前面加上#pragma pack(2),有效对齐值为2字节,此时根据对齐规则,三个结构体的大小应为6,8,6。内存分布图如下:

微信图片_20200513131329.jpg 有效对齐值为2字节例子

请写出以下代码的输出结果:

#include<stdio.h>
7 Q! n1 r% M- v) z1 ?( G0 Jstruct S1
) v& h& n- V& e4 d1 m, ~0 c{: a3 m$ Z7 V1 A8 m/ d6 W
    int i:8;
" t% V' y1 U" I    char j:4;
0 }* D$ U6 u3 U# l    int a:4;
$ ^3 P) m8 n+ X8 I    double b;
! J3 p- f& @- H+ h- R3 C};
- O: ^4 J3 z# Y
+ m7 {% O7 _5 v, _# h: g) lstruct S29 V6 j: d; R1 k
{) y" D: R% E4 j: C
    int i:8;* T- a3 E) D6 G( P
    char j:4;
7 y4 B3 a) r& o" e9 V    double b;; t2 Z# O' z4 n/ n2 L# R5 N
    int a:4;
  `' k2 N+ {. G; b};
6 w6 J1 P# C' h! _
7 h; ^9 t: G' a1 ]0 {! Nstruct S3
% v$ c- c1 M& A+ n: c: i{
5 y# V1 m8 F& N2 T" P    int i;6 ]7 a/ i4 G7 w4 `
    char j;1 {5 M! N6 D+ v' ]
    double b;
4 Q! k- T1 S, P. F    int a;
* v0 Z4 S7 |& ~};
4 u& z! P) r0 p5 ~) k% z7 C5 q+ \$ y: c* K9 ?8 q" ]  z
int main()" C8 F. V/ n$ T9 j# I- v# G" _
{! A8 v$ M3 s3 s5 s# A2 [# ]1 p! H
    printf("%d\n",sizeof(S1));  // 输出8
: ~/ u$ e' D2 {* N% {& p# J. h    printf("%d\n",sizeof(S1);  // 输出12: J: h; Y2 h' f( S1 B
    printf("%d\n",sizeof(Test3));  // 输出8
8 Q" @. N2 _: G3 I6 ~0 V    return 0;
- F) [6 G; B+ J) c; o}
5 W) [8 e4 j* c% H- u- p! h
分析问题

在存储某些数据时,其实际需求的数据长度可能要小于一个字节,只占一位或几位。为了节省空间,处理方便,在C中引入了另一种结构,称为“位域”或“位段”。

所谓“位域”,就是把一个字节中的“位”按照实际的需求分成不同的区域,表明每个区域位数、区域的域名,并允许程序按照域名进行操作。如此就可以把不同的对象用一个字节来表示。

位域的定义与结构定义相仿,其形式为:

struct 位域的结构体名* H( x% U) ~. p' \7 D( x. d6 k3 M
{+ ^5 L# D4 C7 \$ \
   //位域列表
2 m1 C- B; `: F/ l  z}
$ N3 j5 r* U* A" v* S9 Y. j

位域列表的形式为:【类型说明符】 【位域名】:【位域的长度】例如:

struct ab
- n4 N' X8 b% ^; j( b{0 e# U) @1 \0 ^% B8 y8 N2 f7 S
   int a:8;
- D" |; M5 S9 W% P9 K1 O. J, a   int b:2;
) e* `' p' P5 l   int c:6;% k$ ^! }8 Z1 t
}
8 S' M- G7 T8 J; ~

对于位域的定义,有以下几点说明:

(1)一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。

例如:

struct wy* g9 X- I9 |( Q5 u" X% P$ L  _) _% c
{* s2 S) H5 \) c8 t% E
   unsigned a:6;; w( ~- t( c$ \: S3 n
   unsigned 0;     //空域
$ [) S$ d$ ^, a3 v   unsigned b:4;   //从一单元开始存放
; x+ m3 H1 [) [" |   unsigned c:4;* g; S3 w( P" h: z, u4 h# L" d
}

6 m8 s7 D0 r4 d9 ~7 J

在这个位域定义中,a占第一字节的6位,后2位填0表示不使用,b从第二字节开始,占用4位,c占用4位。内存结构如下图所示。

微信图片_20200513131332.png 存储结构图

(2)由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是不能超过8位二进位。

(3)位域可以无位域名,这时它只用来填充或调整位置。无名的位域是不能使用的。

例如:

struct wk
. l' D$ }" T  ?$ ^8 D5 }# @1 T{
2 m! o7 w6 Q$ d4 p! e* W   int a:1;
7 K- N) W$ C: I, m* |: F6 }& f   int :2;     //不能使用. ?$ h& C; O. L4 L
   int b:3;
- H8 W# p* K" }   int c:2;0 D. }4 b9 @# ?9 ]& t; v
}

! \/ H! S) T( [7 u9 Z: P- G 微信图片_20200513131335.png 存储结构图

从以上述分析可以看出,位域可以看做是一种结构类型,其特点是成员均按二进位分配。

根据以上分析可知,在s1中i在相对0的位置,占8位即第1个字节。j就在相对第2个字节的位置。由于一个位置的字节数是4位的倍数,因此不用对齐,可以就放在那里。a要放在4位倍数关系的位置上,因此不用对齐,就放在那里。

目前总共是16位,2字节,由于double是8字节的,因此要在距相对0位置为8个字节的位置处放下。所以从16位开始到8个字节之间的位置被忽略,直接放在相对第8字节的位置,因此,s1总共占16字节。存储结构如下图所示。

微信图片_20200513131338.png S1存储结构

在s2中,每个数据都要对照结构体内最大数据的最小公倍数补齐。如i,j共12位,小于double的8个字节需按8字节补齐,a位也要按8字节补齐,共24个字节,存储结构如下图所示。

微信图片_20200513131341.png s2的存储结构

在s3中,i是int型数据(按32位机分析)占4个字节,j是char型数据占一个字节,a是int型数据占4个字节,b是double型数据占8个字节。在此b是最大的数据类型,因此i、j、a都要向b的double型对齐,即i、j、a的数据长度要向b对齐为8个字节,四个数据共占据32个字节。s3的存储结构如下图所示。

微信图片_20200513131344.png s3的存储结构答案

sizeof(S1)=16

sizeof(S2)=24

sizeof(S3)=32

说明:结构体作为一种复合数据类型,其构成元素既可以是基本数据类型的变量,也可以是一些复合型类型数据。对此,编译器会自动进行成员变量的对齐以提高运算效率。默认情况下,按自然对齐条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同,向结构体成员中size最大的成员对齐。

许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,而这个k则被称为该数据类型的对齐模数。

6 G% x8 G& b3 }* l
赞 1 收藏 2 评论2 发布时间:2020-5-13 13:15

举报

2个回答
李康1202 回答时间:2020-5-13 14:08:23
谢谢分享
Philleon 回答时间:2020-5-14 09:33:04
干货满满,感谢分享
关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版