跳转到内容
新建笔记

C语言解析

GCC编译器扩展机制 语法 使用

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
写法类型位数(通常)
16int(有符号)16/32 位
16uunsigned int16/32 位
16ULunsigned long32 位
16ULLunsigned long long64 位

重复宏定义问题 是的,() 确实会影响!这是一个重要的区别:

  1. #define TMC4361A_RESET_LOW() - 这是一个宏函数,可以接受参数
  2. #define TMC4361A_RESET_LOW - 这是一个宏常量,不能接受参数

在你的代码中:

#define TMC4361A_RESET_LOW() HAL_GPIO_WritePin(TMC4361A_NRST_PORT, TMC4361A_NRST_PIN, GPIO_PIN_RESET)

这意味着当你调用 TMC4361A_RESET_LOW() 时,它会被替换为:

HAL_GPIO_WritePin(TMC4361A_NRST_PORT, TMC4361A_NRST_PIN, GPIO_PIN_RESET)

如果你之前有:

#define TMC4361A_RESET_LOW 1

那么调用 TMC4361A_RESET_LOW() 就会变成 1(),这会导致编译错误,因为 1 不是一个函数。

所以删除常量定义是正确的解决方案,现在 TMC4361A_RESET_LOW() 可以正确地作为宏函数使用。


  1. extern

在C语言中使用这种方式(头文件中extern声明,源文件中定义)是一种标准做法,主要有以下原因:

1)避免重复定义错误:

  • 如果在头文件中直接定义变量 TIM_HandleTypeDef htim2;
  • 当多个C文件包含该头文件时,会导致多次定义同一个变量
  • 这会引发链接时的”multiple definition”错误

2)extern的作用:

  • extern 关键字告诉编译器”这个变量在其他地方已定义,这里只是声明”
  • 声明只是告知编译器变量存在,但不分配内存空间
  • 定义则会真正分配内存空间

3)一处定义规则:

  • 在C语言中,全局变量应该只在一个源文件中定义
  • 其他需要使用该变量的文件通过extern声明引用它
  • 遵循”一次定义,多处声明”的原则
  1. velotile

volatile 能做什么、不能做什么

  • 能做:
    • 禁止编译器把变量优化进寄存器
    • 每次访问都“真正”读/写内存
  • 不能做:
    • 不保证原子性(比如 ++ 这种读-改-写不是原子的)
    • 不解决并发竞态(ISR 和主循环同时改同一个变量仍可能出现竞争)

什么时候需要 volatile

  • 中断服务函数里写、主循环里读的共享变量
    • 例如你现在的 pb9_exti_triggered
  • 轮询外设寄存器(内存映射寄存器)
    • 在 STM32 头文件里常见的 __IO 宏其实就是 volatile
  • 被 DMA/外设/其他执行上下文修改的内存

中断里修改的变量,如果不声明为 volatile,编译器可能会认为主循环里它永远不会改变,从而优化掉检查!

// 外部中断回调:当配置为外部中断的引脚触发时由HAL调用
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_9)
{
// 简单软件消抖:限制最小触发间隔,过滤机械抖动/毛刺
static uint32_t s_last_tick = 0;
const uint32_t debounce_ms = 20; // 可按需要调整 10~30ms
uint32_t now = HAL_GetTick();
if ((now - s_last_tick) >= debounce_ms)
{
pb9_exti_triggered = 1;
s_last_tick = now;
// 调试:在中断里翻转LED验证中断是否触发
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_12);
}
}
}
// main.c
...
extern volatile uint8_t pb9_exti_triggered;
while(1)
{
if(pb9_exti_triggered)
{
pb9_exti_triggered = 0;
printf("PB9 EXTI triggered\n");
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_10);
}
}
  1. 在C语言中,全局作用域(函数外部)只能进行声明和静态初始化,而不能执行运行时的赋值操作。这些uart = huart2;和tim = htim2;是运行时操作,必须放在函数内部执行。
// 全局声明
UART_HandleTypeDef *uart;
// 错误:在全局域直接赋值操作
*uart = huart2; // 这行导致编译错误`
  1. C语言头文件包含顺序问题笔记

在C语言中,头文件的包含顺序非常重要,这是因为编译器按顺序处理每个头文件。如果一个头文件使用了另一个头文件中定义的类型或宏,但没有包含那个头文件,就会产生”未知类型”的编译错误。

问题示例:

  • mb_m.h文件中使用了eMBErrorCode、UCHAR等类型
  • 但这些类型实际上是在mb.h中定义的
  • mb_m.h自身没有包含mb.h
  • 解决方法:在包含mb_m.h之前先包含mb.h

最佳实践:

  1. 头文件应自包含(包含它所依赖的所有头文件)
  2. 当遇到头文件依赖问题时,确保按正确顺序包含头文件
  3. 对于库开发者:使用头文件保护和前向声明避免这类问题

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)

复合字面量”(compound literal) 语法

跳转到“复合字面量”(compound literal) 语法”

C99 的“复合字面量”(compound literal) 语法,用来在表达式里临时创建一个指定类型的匿名对象。

(uint16_t[]){0x0001, 0x0000}
  • 类型:数组类型uint16_t[2](用 [] 让编译器根据初始值个数自动推断长度)
  • 值:{0x0001, 0x0000}
  • 作用域与生命周期:在当前代码块内具有自动存储期,直到离开所在的花括号作用域为止
  • 用途:简洁地给需要 uint16_t* 的参数传一小段常量数据

等价写法

  • 写成显式的临时数组变量:
    • uint16_t tmp[] = {0x0001, 0x0000};
    • mb_master_10h(2, 0x0064, 2, tmp);
  • 或指定长度也行:
    • (uint16_t[2]){0x0001, 0x0000}
维度典型错误示例嵌入式痛点级别
语法语义未初始化、死代码★☆☆
内存资源越界、泄漏、双重释放、栈溢出★★★
并发时序竞态、死锁、优先级反转★★★
编码规范MISRA/ CERT 违规★★☆(认证强制)
安全可靠性输入未校验、缺省分支、未定义行为★★★
可维护性复杂度高、重复代码、魔术数★★☆

这是静态检查的主要工作内容。

  • 数组越界:int buf[4]; buf[4] = 0;
  • 内存泄漏:early return 前忘了 free;OS 里动态申请的信号量没释放。
  • 重复释放:中断和低优先级任务里 double-free 同一块 RAM。
  • 栈爆炸:递归深度估算 300 字节,但 MCU 只给 256 字节,Coverity 会报 Stack overflow。
  • 竞态条件:全局标志位 g_ready 在 IRQ 里写,主循环里读,未关中断。
  • 死锁:任务 A 先拿 mutex1 再拿 mutex2,任务 B 顺序相反。
  • 优先级反转:低优先级拿着信号量,被中优先级饿死,高优先级永久阻塞。
  • 中断里调用 printf(不可重入)—— MISRA 直接禁止。

来源:CSDN 原文,首次发布于 2025-07-27。