简介

因为CPU在设计时,对于不同基础类型变量的地址有倍数于类型本身的寻址的约束,所以需要引入内存对齐以提升CPU运算效率或避免运行错误;通过内存对齐,还能调整结构体占用的内存大小。

规则

基本规则

  1. 每种基本类型(char/short/int/double/...)的 地址P 都是其 占用空间S 的倍数,即 其中 k 是正整数。比如char可以任意存放,而int只能存放在 倍数的地址空间,也就是int的地址满足 其中k是正整数。
  2. 结构体类型内部的基本类型地址P同样也适用规则1,比如 struct { char a; int b; }; 中 b成员的地址是其类型int所占用空间大小 的整数倍,也就是b的地址 其中 k 是正整数。
  3. 结构体的大小是内部基本类型中,占用空间最大的类型的大小的整数倍,即 其中k是正整数。比如大小为的结构体内部有 char/short/int类型,而其中占用空间最大的类型是int类型,大小为,也就是 ,那么结构体的大小的整数倍,也就是 其中k是正整数。
  4. 结构体内部嵌套结构体成员的情况下,结构体成员的起始地址要是结构体成员内部定义的最大基本类型大小的整数倍,也就是 其中 k是正整数。

受参数影响的规则

  1. 如果指定了 #pragma pack(n) ,定义各个基本类型的空间大小为S,则各个基本类型的起始地址P为 的整数倍,也就是 其中 k 是正整数,n可取1,2,4,8,16。比如在64位操作系统中,定义 #pragma pack(2) 的情况下,对于 char 类型而言,, ,此时char类型地址 其中 k是正整数,也就是说#pragma pack(n)不会影响char类型起始地址;同样对于 int类型而言, ,此时 int 类型地址 其中k是正整数,也就说int类型的地址是必须是2的倍数。
  2. 如果指定了 __attribute__((aligned(m))) 用于修饰变量,则表示变量的地址P是 m 的整数倍,也就是 其中k是正整数,m必须是2的正幂数。如 int __attribute__((aligned(4))) a;表示a变量地址是 4 的整数倍。
  3. 如果指定了__attribute__((aligned(m))) 用于修饰结构体,则表示结构体的大小是m的整数倍,也就是 其中k是正整数,m必须是2的正幂数。如 typedef struct { int a; } __attribute__((aligned(16))) s_t; 情况下,s_t的结构体大小 其中k是正整数,表示结构体 s_t 的大小为16的倍数。
  4. 背后跟着 #pragma pack() 表示撤销原先#pragma pack(n)的修改;__attribute__((packed)) 表示使用变量实际占用的大小,不进行对齐优化,也就是用1字节对齐,和 #pragma pack(1) 效果一样。

例子

#include <stdio.h>
 
#pragma pack(4)
typedef struct {
	char a;
	short b;
	char c;
	char __attribute__((aligned(2))) d;
	double e;
	int f;
} __attribute__((aligned(16))) p_t;
#pragma pack()
 
typedef struct __attribute__((packed)) {
	char a;
	int b;
} q_t;
 
int main(int argc, char *argv[])
{
	p_t p = {0};
	q_t q = {0};
 
	printf("sizeof(p_t) = %zu\n", sizeof(p_t));
	printf("offset p.b = %zu\n", (char *)&p.b - (char *)&p.a);
	printf("offset p.c = %zu\n", (char *)&p.c - (char *)&p.a);
	printf("offset p.d = %zu\n", (char *)&p.d - (char *)&p.a);
	printf("offset p.e = %zu\n", (char *)&p.e - (char *)&p.a);
	printf("offset p.f = %zu\n", (char *)&p.f - (char *)&p.a);
	printf("\n");
	
	printf("sizeof(q_t) = %zu\n", sizeof(q_t));
	printf("offset q.b = %zu\n", (char *)&q.b - (char *)&q.a);
	
	return 0;
}

分析

在64位linux操作系统中,
对于 q_t 而言, 定义了#pragma pack(4),也就是 n = 4,现在假定结构体的起始地址是0x0,现在来看内部结构。
a是char,占用1字节,也就是说,a的起始地址 其中k是正整数。a是第一个成员,现在默认首地址成员已经地址对齐,为了分析方便,让首地址成员的地址为0,取 内存分布示意图如下:

a1 char

b是short,占用2字节, 其中k是正整数。因为内存是连续存放的,签名已经占用1字节,因此k取1, 示意图如下(0表示未使用字节空间):

a1 char
0
b1 short
b2

c是char, 其中k是正整数。前面已经占用4字节,取k=4, 示意图如下:

a1 char
0
b1 short
b2
c1 char

d是char,但是d使用了 __attribute__((aligned(2)))修饰,m=2, 其中k是正整数。前面已经占用5字节,取k=3, 示意图如下:

a1 char
0
b1 short
b2
c1 char
0
d1 char

e是double, 其中 k 是正整数,前面已经占用7字节,取k=2, 示意图如下:

a1 char
0
b1 short
b2
c1 char
0
d1 char
0
e1 double
e2
e3
e4
e5
e6
e7
e8

f是int, 其中 k 是正整数,前面已经占用16字节,取k=4, 示意图如下:

a1 char
0
b1 short
b2
c1 char
0
d1 char
0
e1 double
e2
e3
e4
e5
e6
e7
e8
f1 int
f2
f3
f4

根据规则,结构体的大小满足 其中k是正整数,因为结构体成员本身已经占用20字节,结构体内部最大成员是double,若不考虑其他约束,则此时的结构体大小应该为 其中k是正整数,取k=3的时候可以得到结构体大小 ,但是因为定义了结构体类型的 __attribute__((aligned(16))) ,m=16,因此 q_t 结构体的大小应该满足 其中k是正整数,因为结构体的成员本身已经占用20字节,因此取k=2, 最终p_t结构体大小 ,图略。

结构体定义结束后,使用了#pragma pack()恢复默认对齐方式。

q_t的分析已略。

输出

sizeof(p_t) = 32
offset p.b = 2
offset p.c = 4
offset p.d = 6
offset p.e = 8
offset p.f = 16
 
sizeof(q_t) = 5
offset q.b = 1 

参考

*****