c语言 __align(); #pragma pack(8) 数据对齐

本文详细解释了C语言中结构体的字节对齐原理及其对性能的影响,包括对齐的作用、如何更改对齐方式及具体的例子。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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。


对齐方式

程序编译器对结构的存储的特殊处理确实提高CPU存储 变量的速度,但是有时候也带来了一些麻烦,我们也屏蔽掉变量默认的对齐方式,自己可以设定变量的对齐方式。
编译器中提供了 #pragma pack(n)来设定 变量以n 字节对齐方式。n 字节对齐就是说 变量存放的起始地址的 偏移量有两种情况:第一、如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式,第二、如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。结构的总大小也有个约束条件,分下面两种情况:如果n大于所有 成员变量类型所占用的字节数,那么结构的总大小必须为占用空间最大的变量占用的空间数的倍数;否则必须为n的倍数。
下面举例说明其用法。
#pragma pack(push) //保存对齐状态
#pragma pack(4)//设定为4 字节对齐
struct test
{
char m1;
double m4;
int m3;
};
#pragma pack(pop)//恢复对齐状态
以上 结构体的大小为16,下面分析其存储情况,首先为m1分配空间,其 偏移量为0,满足我们自己设定的对齐方式(4 字节对齐),m1大小为1个字节。接着开始为m4分配空间,这时其 偏移量为4,需要补足3个字节,这样使偏移量满足为n=4的倍数(因为sizeof(double)大于4),m4占用8个字节。接着为m3分配空间,这时其 偏移量为12,满足为4的倍数,m3占用4个字节。这时已经为所有 成员变量分配了空间,共分配了16个字节,满足为n的倍数。如果把上面的 #pragma pack(4)改为 #pragma pack(8),那么我们可以得到结构的大小为24。

对齐用法详解编辑

什么是对齐,以及为什么要对齐: 现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的 变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的 内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。 对齐的作用和原因:各个硬件平台对 存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低 字节进行拼凑才能得到该int数据。显然在读取效率上下降很多。这也是空间和时间的博弈。
对齐的实现:通常,我们写程序的时候,不需要考虑对齐问题。 编译器会替我们选择适合目标平台的对齐策略。当然,我们也可以通知给 编译器传递 预编译指令而改变对指定数据的对齐方法。 但是,正因为我们一般不需要关心这个问题,所以因为 编辑器对数据存放做了对齐,而我们不了解的话,常常会对一些问题感到迷惑。最常见的就是struct 数据结构的sizeof结果,出乎意料。为此,我们需要对对齐算法所了解。
作用:指定 结构体、联合以及类成员的packing alignment;
语法: #pragma pack( [show] | [push | pop] [, identifier], n )
说明:1,pack提供数据声明级别的控制,对定义不起作用;2,调用pack时不指定参数,n将被设成默认值;3,一旦改变 数据类型的alignment,直接效果就是占用memory的减少,但是performance会下降;
语法具体分析:1,show:可选参数;显示当前packing aligment的 字节数,以warning message的形式被显示;2,push:可选参数;将当前指定的packing alignment数值进行压栈操作,这里的栈是the internal compiler stack,同时设置当前的packing alignment为n;如果n没有指定,则将当前的packing alignment数值压栈;3,pop:可选参数;从internal compiler stack中删除最顶端的record;如果没有指定n,则当前栈顶record即为新的packing alignment数值;如果指定了n,则n将成为新的packing aligment数值;如果指定了identifier,则internal compiler stack中的record都将被pop直到identifier被找到,然后pop出identitier,同时设置packing alignment数值为当前栈顶的record;如果指定的identifier并不存在于internal compiler stack,则pop操作被忽略;4,identifier:可选参数;当同push一起使用时,赋予当前被压入栈中的record一个名称;当同pop一起使用时,从internal compiler stack中pop出所有的record直到identifier被pop出,如果identifier没有被找到,则忽略pop操作;5,n:可选参数;指定packing的数值,以字节为单位;缺省数值是8,合法的数值分别是1、2、4、8、16。
重要规则:1,复杂类型中各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个类型的地址相同;2,每个成员分别对齐,即每个成员按自己的方式对齐,并最小化长度;规则就是每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数中较小的一个对齐;3,结构、联合或者类的 数据成员,第一个放在偏移为0的地方;以后每个数据成员的对齐,按照 #pragma pack指定的数值和这个数据成员自身长度两个中比较小的那个进行;也就是说,当#pragma pack指定的值等于或者超过所有数据成员长度的时候,这个指定值的大小将不产生任何效果;4,复杂类型(如结构)整体的对齐<注意是“整体”>是按照结构体中长度最大的数据成员和#pragma pack指定值之间较小的那个值进行;这样在成员是复杂类型时,可以最小化长度;5,结构整体长度的计算必须取所用过的所有对齐参数的整数倍,不够补空字节;也就是取所用过的所有对齐参数中最大的那个值的整数倍,因为对齐参数都是2的n次方;这样在处理 数组时可以保证每一项都 边界对齐
对齐的算法: 由于各个平台和 编译器的不同,现以本人使用的gcc version 3.2.2 编译器(32位x86平台)为例子,来讨论编译器对struct 数据结构中的各成员如何进行对齐的。
在相同的对齐方式下, 结构体内部数据定义的顺序不同, 结构体整体占据内存空间也不同,如下: 设结构体如下定义: struct A { int a; char b; short c; }; 结构体A中包含了4 字节长度的int一个,1 字节长度的char一个和2字节长度的short型数据一个。所以A用到的空间应该是7 字节。但是因为编译器要对 数据成员在空间上进行对齐。所以使用sizeof(strcut A)值为8。 现在把该结构体调整 成员变量的顺序。 struct B { char b; int a; short c; }; 这时候同样是总共7个字节的 变量,但是sizeof(struct B)的值却是12。 下面我们使用 预编译指令 #pragma pack (value)来告诉 编译器,使用我们指定的对齐值来取代缺省的。 #pragma pack (2) /*指定按2 字节对齐,等价于#pragma pack(push,2)*/ struct C { char b; int a; short c; }; #pragma pack () /*取消指定对齐,恢复缺省对齐,等价于#pragma pack(pop)*/ sizeof(struct C)值是8。 修改对齐值为1: #pragma pack (1) /*指定按1 字节对齐*/ struct D { char b; int a; short c; }; #pragma pack () /*取消指定对齐,恢复缺省对齐*/ sizeof(struct D)值为7。 对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,单位 字节
这里面有四个概念值: 1. 数据类型自身的对齐值:就是上面交代的基本数据类型的自身对齐值。 2.指定对齐值: #pragma pack (value)时的指定对齐值value。 3. 结构体或者类的自身对齐值:其 数据成员中自身对齐值最大的那个值。 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的自身对齐值为其 变量中最大对齐值(这里是a)和指定对齐值(这里是4)中较小的那个,所以就是4,所以 结构体的有效对齐值也是4。根据 结构体圆整的要求,0x0009到0x0000=10 字节,(10+2)%4=0。所以0x0000A到0x000B也为 结构体B所占用。故B从0x0000到0x000B共有12个 字节,sizeof(struct B)=12; 同理,分析上面例子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.


<think>我们正在处理一个关于C语言底层内存管理的问题。用户希望使用`#pragma section`和`__at`(或类似扩展)来指定变量存储在特定的内存段和绝对地址。这通常用于嵌入式系统开发,其中需要精确控制变量的存储位置。 根据引用[1]和引用[3]提到的`#pragma`用法,特别是引用[3]中关于`#pragma pack`对齐的说明,我们可以推断`#pragma`指令可以用来控制内存布局。虽然标准C没有直接提供`__at`关键字,但许多编译器(如IAR、Keil、GCC等)提供了类似的扩展。 步骤: 1. 使用`#pragma section`定义一个新的段(section)。 2. 使用编译器特定的扩展(如`__attribute__`或`__at`)将变量放置在指定的段内,或者直接指定绝对地址。 由于不同编译器支持不同的语法,这里我们以常见的编译器为例: 示例1:IAR编译器 ```c // 定义一个名为MySection的段,并设置其属性(如可读、可写、无初始化等) #pragma section = "MySection" // 将变量放置在MySection段中,并指定地址0x1000 __no_init int myVar @ "MySection"; // 或者使用绝对地址 __no_init int myVar @ 0x1000; ``` 示例2:Keil MDK(ARMCC) ```c // 使用__attribute__指定段 int myVar __attribute__((section("MySection"), at(0x1000))); // 或者使用分散加载文件(scatter file)来定义段,然后在代码中使用__attribute__((section("MySection"))) ``` 示例3:GCC(如用于ARM的gcc) ```c // 在链接脚本中定义内存区域和段,然后在代码中: int myVar __attribute__((section(".mysection"))); // 然后通过链接脚本将.mysection段定位到0x1000地址 ``` 但是,如果编译器支持`__at`关键字(如Keil和IAR),也可以直接使用: ```c int myVar __at(0x1000); ``` 注意:直接指定绝对地址需要确保该地址是可用的,并且不会与其他变量或代码段冲突。 另外,引用[2]提到了一种直接通过指针赋值的方法,但这种方法是在运行时赋值,而上述方法是在编译时就将变量定位到固定地址。 综合以上,我们可以给出一个通用的方法(以GCC为例,因为GCC的`__attribute__`较为通用)和IAR/Keil的示例。 具体步骤: 1. 定义段(如果编译器需要提前定义段): 在链接脚本(如GCC的ld文件)中定义内存区域和段。例如: ``` MEMORY { MYRAM (rwx) : ORIGIN = 0x1000, LENGTH = 0x1000 } SECTIONS { .mysection : { . = ALIGN(4); *(.mysection) . = ALIGN(4); } > MYRAM } ``` 2. 在C代码中,使用`__attribute__`将变量放入该段: ```c int myVar __attribute__((section(".mysection"))); ``` 3. 如果使用IAR,可以直接使用`#pragma location`和`__no_init`等: ```c #pragma location = 0x1000 __no_init int myVar; ``` 4. 如果使用Keil,可以使用`__at`: ```c int myVar __at(0x1000); ``` 注意:`__no_init`(IAR)表示该变量在启动时不会被初始化,这在一些嵌入式系统中很重要。 由于用户特别提到了`#pragma section`和`__at`,我们应优先展示这两种方法。 下面是一个综合示例(假设使用IAR编译器): ```c // 使用#pragma section定义一个段 #pragma section = "MY_SECTION" // 将变量放在这个段的特定地址(0x1000)上 __no_init volatile int myVar @ "MY_SECTION"; // 或者直接指定地址 __no_init volatile int myVar2 @ 0x2000; // 如果不需要指定绝对地址,只放在段内,可以这样: #pragma location = "MY_SECTION" __no_init volatile int myVar3; ``` 注意:`volatile`关键字用于告诉编译器不要优化该变量,因为它的值可能会被硬件改变。 另外,引用[1]中提到了包含文件的保护宏和模块化问题,这里我们不需要考虑,因为这是关于内存地址的操作,通常在一个模块中定义即可。 总结: 使用`#pragma section`和`__at`(或编译器等效的扩展)可以将变量放置在特定的内存段和绝对地址。具体语法取决于所使用的编译器。在嵌入式开发中,这常用于访问硬件寄存器或分配变量到特定的内存区域(如非易失性内存)。 最后,根据用户要求,生成相关问题: §§相关问题§§ 1. 在嵌入式系统中,为什么要将变量分配到特定的内存地址? 2. 不同编译器(如GCC、IAR、Keil)在内存分配上有哪些不同的扩展语法? 3. 如何确保在特定内存地址分配的变量不会与其他变量冲突? 4. 在C语言中,使用绝对地址访问硬件寄存器有哪些注意事项?</think>### 在C语言中使用`#pragma section`和`__at`指定变量存储段与地址 在嵌入式系统或底层开发中,通过`#pragma section`定义自定义内存段,再结合编译器扩展(如`__at`、`__attribute__`)将变量定位到绝对地址,可实现精确的内存控制。以下是具体方法: --- #### 1. **使用 `#pragma section` 定义自定义段** ```c #pragma section = "MY_SECTION" // 定义名为MY_SECTION的段 #pragma data_seg("MY_SECTION") // 指定后续变量存储到该段 volatile int myVar; // 变量将位于MY_SECTION段内 #pragma data_seg() // 恢复默认段 ``` **关键点**: - `data_seg` 控制数据段,`code_seg` 控制代码段(引用[1])。 - 段名需在链接脚本中声明其内存区域(如RAM特定区域)。 --- #### 2. **使用 `__at` 或等效扩展定位绝对地址** 编译器扩展语法示例: ```c // Keil MDK (ARMCC) 语法 volatile int __at(0x20001000) reg_value; // 变量定位到0x20001000 // IAR EWARM 语法 volatile int __no_init __at(0x20001000) reg_value; // GCC (__attribute__语法) volatile int reg_value __attribute__((section(".my_section"))); // 需在链接脚本中指定.my_section的地址:.my_section 0x20001000 { *(.my_section) } ``` **关键点**: - 地址需对齐(如4字节对齐),避免未定义行为(引用[3])。 - `volatile` 防止编译器优化对硬件寄存器的访问(引用[2])。 --- #### 3. **完整示例:硬件寄存器映射** ```c #include <stdint.h> // 步骤1: 定义自定义段 #pragma section = "HW_REGS" // 步骤2: 将变量绑定到段和地址 #pragma data_seg("HW_REGS") volatile uint32_t __at(0x40020000) GPIOA_MODER; // GPIOA模式寄存器 volatile uint32_t __at(0x40020004) GPIOA_ODR; // GPIOA输出寄存器 #pragma data_seg() int main() { GPIOA_MODER = 0xAB00; // 直接操作硬件寄存器 GPIOA_ODR |= 0x01; // 置位引脚0 return 0; } ``` --- #### 4. **注意事项** 1. **内存冲突**:确保地址不被其他变量或系统占用。 2. **编译器支持**: - `__at` 是编译器扩展(非标准C),需查阅文档(如ARMCC、IAR)。 - GCC需配合链接脚本(`.ld`文件)实现绝对定位。 3. **初始化**:使用 `__no_init`(IAR)避免启动代码初始化绝对地址变量。 4. **对齐要求**:结构体成员按 `#pragma pack` 和自身长度对齐(引用[3])。 > 此技术广泛用于嵌入式外设寄存器访问、共享内存通信、Bootloader设计等场景。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值