编译器
attribute 是 GCC 编译器的扩展机制,用于为函数、变量、类型添加特殊属性,控制编译器的行为。
__attribute__
是 GCC 编译器的扩展机制,用于为函数、变量、类型添加特殊属性,控制编译器的行为。
基本语法
__attribute__((属性1, 属性2, ...))
/* 双层括号是必须的! */
__attribute__((section(".mydata"))) /* 正确 */
__attribute__(section(".mydata")) /* 错误!缺少括号 */
1. 变量声明
/* 位置1:类型之后,变量名之前(推荐) */
int __attribute__((section(".mydata"))) var = 5;
/* 位置2:整个声明之前 */
__attribute__((section(".mydata"))) int var = 5;
/* 位置3:变量名之后 */
int var __attribute__((section(".mydata"))) = 5;
/* 多个属性 */
uint32_t __attribute__((section(".dma_buf"), aligned(32))) dma_buffer[256];
2. 函数声明
/* 位置1:返回类型之前 */
__attribute__((section(".ramfunc"))) void func(void);
/* 位置2:返回类型之后 */
void __attribute__((section(".ramfunc"))) func(void);
/* 位置3:函数名之后,参数列表之前 */
void func(void) __attribute__((section(".ramfunc")));
/* 位置4:函数定义 */
__attribute__((weak, alias("Default_Handler")))
void USART1_IRQHandler(void);
3. 结构体/类型定义
/* 结构体属性 */
struct __attribute__((packed)) MyStruct {
uint8_t a;
uint32_t b;
};
/* typedef 中使用 */
typedef struct {
uint8_t data[32];
} __attribute__((aligned(4))) AlignedBuffer;
常用属性详解
1. section - 指定存储段
/* 变量放入指定段 */
uint32_t __attribute__((section(".ccmram"))) fast_var;
uint8_t __attribute__((section(".dma_buf"))) dma_buffer[1024];
/* 函数放入指定段 */
__attribute__((section(".itcm")))
void critical_function(void) {
// 代码放入ITCM RAM执行
}
/* 只读数据放入指定段 */
const uint32_t __attribute__((section(".rodata.custom")))
lookup_table[] = {0x1234, 0x5678};
2. aligned - 对齐方式
/* 32字节对齐(DMA常用) */
uint8_t __attribute__((aligned(32))) dma_buffer[1024];
/* 结构体对齐 */
struct __attribute__((aligned(16))) {
uint32_t field1;
uint32_t field2;
} aligned_struct;
/* Cache line对齐(通常32字节) */
__attribute__((aligned(32)))
uint8_t cache_aligned_buffer[256];
packed - 紧凑存储
/* 取消结构体对齐(节省空间) */
struct __attribute__((packed)) {
uint8_t a; // 1字节
uint32_t b; // 4字节
uint8_t c; // 1字节
} compact; // 总共6字节(不是12字节)
/* 用于协议数据结构 */
typedef struct __attribute__((packed)) {
uint8_t header;
uint16_t length;
uint32_t data;
uint8_t checksum;
} ProtocolPacket; // 确保与协议规定完全一致
weak - 弱符号
/* 弱函数(可被覆盖) */
__attribute__((weak))
void Default_Handler(void) {
while(1);
}
/* HAL库回调函数 */
__attribute__((weak))
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
/* 用户可以重新定义此函数 */
UNUSED(huart);
}
used/unused - 使用标记
/* 防止被优化掉 */
__attribute__((used))
static const char version[] = "V1.0.0";
/* 消除未使用警告 */
__attribute__((unused))
static void debug_function(void) {
// 仅在调试时使用
}
/* 参数未使用 */
void callback(__attribute__((unused)) void *param) {
// param参数未使用,不会产生警告
}
** noinit - 不初始化**
/* 不在启动时初始化(节省启动时间) */
__attribute__((section(".noinit")))
uint8_t large_buffer[65536];
/* RAM保持数据 */
__attribute__((section(".backup_sram"), noinit))
uint32_t retain_data[256];
interrupt - 中断处理
/* ARM Cortex-M中断处理 */
__attribute__((interrupt("IRQ")))
void USART1_IRQHandler(void) {
// 编译器自动生成中断进入/退出代码
}
optimize - 优化控制
/* 特定函数优化级别 */
__attribute__((optimize("O3")))
void performance_critical(void) {
// 最大优化
}
__attribute__((optimize("O0")))
void debug_function(void) {
// 不优化,便于调试
}
always_inline/noinline
/* 强制内联 */
__attribute__((always_inline))
static inline void fast_function(void) {
// 总是内联展开
}
/* 禁止内联 */
__attribute__((noinline))
void no_inline_function(void) {
// 永不内联
}
宏定义
后缀 | 含义 | 示例 |
---|---|---|
u 或 U | 无符号(unsigned) | 100u |
l 或 L | 长整型(long) | 100L |
ul 或 UL | 无符号长整型(unsigned long) | 100UL |
ll 或 LL | 长长整型(long long) | 100LL |
ull 或 ULL | 无符号长长整型(unsigned long long) | 100ULL |
写法 | 类型 | 位数(通常) |
---|---|---|
16 | int (有符号) | 16/32 位 |
16u | unsigned int | 16/32 位 |
16UL | unsigned long | 32 位 |
16ULL | unsigned long long | 64 位 |
关键字
extern
在C语言中使用这种方式(头文件中extern声明,源文件中定义)是一种标准做法,主要有以下原因:
-
避免重复定义错误:
- 如果在头文件中直接定义变量
TIM_HandleTypeDef htim2;
- 当多个C文件包含该头文件时,会导致多次定义同一个变量
- 这会引发链接时的"multiple definition"错误
- 如果在头文件中直接定义变量
-
extern的作用:
extern
关键字告诉编译器"这个变量在其他地方已定义,这里只是声明"- 声明只是告知编译器变量存在,但不分配内存空间
- 定义则会真正分配内存空间
-
一处定义规则:
- 在C语言中,全局变量应该只在一个源文件中定义
- 其他需要使用该变量的文件通过extern声明引用它
- 遵循"一次定义,多处声明"的原则
编程注意项
- 在C语言中,全局作用域(函数外部)只能进行声明和静态初始化,而不能执行运行时的赋值操作。这些uart = huart2;和tim = htim2;是运行时操作,必须放在函数内部执行。
// 全局声明
UART_HandleTypeDef *uart;
// 错误:在全局域直接赋值操作
*uart = huart2; // 这行导致编译错误`
- C语言头文件包含顺序问题笔记
在C语言中,头文件的包含顺序非常重要,这是因为编译器按顺序处理每个头文件。如果一个头文件使用了另一个头文件中定义的类型或宏,但没有包含那个头文件,就会产生"未知类型"的编译错误。
问题示例:
mb_m.h
文件中使用了eMBErrorCode
、UCHAR
等类型- 但这些类型实际上是在
mb.h
中定义的 mb_m.h
自身没有包含mb.h
- 解决方法:在包含
mb_m.h
之前先包含mb.h
最佳实践:
- 头文件应自包含(包含它所依赖的所有头文件)
- 当遇到头文件依赖问题时,确保按正确顺序包含头文件
- 对于库开发者:使用头文件保护和前向声明避免这类问题
结构体
基础
C99 引入的指定初始化器
(designated initializer) 语法,用来按成员名而不是按顺序给结构体/联合体成员赋值。左边 .member = value
中的点号(.
)就是“成员选择符”;未出现的成员会被自动置 0
(或 NULL
、0.0
等,视类型而定)。
/* 结构体声明 */
typedef struct {
int a;
char b;
double c;
} Foo;
/* 指定初始化器 */
Foo f = {
.b = 'X',
.c = 3.14,
/* .a 未写出,自动置 0 */
};
运算符
位运算
示例
#define BIT_SET(reg, bit) ((reg) |= (1U << (bit)))
#define BIT_CLEAR(reg, bit) ((reg) &= ~(1U << (bit)))
#define BIT_TOGGLE(reg, bit) ((reg) ^= (1U << (bit)))
#define BIT_READ(reg, bit) (((reg) >> (bit)) & 1U)