【stm32】理解弱引用

🔥铅笔小新z:个人主页

🎬博客专栏:stm32学习

💫滴水不绝,可穿石;步履不休,能至渊。

理解弱引用 ------ __attribute__((weak)) 到底是个啥

面向初学者的通俗解读,配合逐行注释的代码,以及每个知识点后的"一句话总结"。

本文带你彻底搞懂 __weak 的来龙去脉,以及它和 HAL 库回调函数的隐秘联系。


目录

  1. 从一个问题说起:为什么需要"弱引用"
  2. [什么是 __attribute__((weak))](#什么是 attribute((weak)))
  3. 强符号与弱符号:链接器怎么选
  4. [弱定义 vs 弱引用:两种不同的用法](#弱定义 vs 弱引用:两种不同的用法)
  5. 完整示例:默认死循环与用户覆盖
  6. 编译结果对比:两种情况
  7. [HAL 库里的 __weak:你早就用过了](#HAL 库里的 __weak:你早就用过了)
  8. 应用场景总结

1. 从一个问题说起:为什么需要"弱引用"

设想这样一个场景:你写了一个库(或者用 ST 提供的 HAL 库),库里面有一个函数 Loop(),默认它只打印一句"默认实现"。但你希望用户能用自己的函数替换它 ,而不用修改库的源代码。

普通做法会碰壁:如果用户也在自己的工程里写了一个同名函数 Loop(),链接阶段就会出现 "重复定义"(multiple definition) 错误,链接失败。

有没有一种办法,让链接器"通情达理"一点:

  • 如果用户定义了 自己的 Loop() → 用用户的。
  • 如果用户没定义 → 用库里的默认版本。

这正是 __attribute__((weak)) 解决的问题。它让一个符号(函数或变量)变成 "弱符号" :有更强的同名符号出现时,自动让位;没有时,就用自己的。

一句话:weak 是"备胎"机制------正主来了我让位,正主没来我顶上。


📌 本知识点一句话总结

__attribute__((weak)) 解决的核心问题是:在不修改库源代码的前提下,让用户能用自己的同名函数"覆盖"库里的默认实现。它把一个函数或变量标记为"弱符号",链接时如果有同名"强符号"(普通定义),强符号优先;如果没有,弱符号才被采用。这样就避免了"重复定义"错误,实现"默认实现 + 用户可覆盖"的灵活设计。


2. 什么是 __attribute__((weak))

2.1 官方定义

  • __attribute__((weak)) 是 ARM GCC 的扩展语法 (本质是 GCC 的扩展),用于给函数或变量设置一个"弱引用"或"弱定义"的属性。
  • 它的核心作用是:允许你在不修改原始函数代码的情况下,用自己的函数"覆盖"掉它,从而让代码在编译和链接时更灵活。

注意:__attribute__ 是 GCC 系编译器(含 ARM GCC、用于 STM32 的 arm-none-eabi-gcc)的扩展。STM32 的 HAL 库工程默认用的就是 GCC(MDK-ARM 用的是 ARMCC,但 ST 也做了兼容宏),所以在 STM32 里 __weak 很常见。

2.2 语法

c 复制代码
// 给函数定义加 weak 属性
__attribute__((weak)) void Func(void)
{
    // 默认实现
}

在 STM32 的 HAL 库里,ST 定义了一个简写宏(在 stm32f1xx_hal_def.h 里):

c 复制代码
#define __weak   __attribute__((weak))

所以你在 HAL 源码里看到的 __weak void HAL_xxx(...) 其实就是上面那种写法。


📌 本知识点一句话总结

__attribute__((weak)) 是 GCC/ARM GCC 的编译器扩展,用来把函数或变量标记为"弱符号"。STM32 的 HAL 库把它封装成了宏 #define __weak __attribute__((weak))。标记为 weak 的符号可以在链接阶段被用户的同名普通函数(强符号)覆盖,实现"不改库代码就能定制行为"。


3. 强符号与弱符号:链接器怎么选

这是理解 weak 的关键背景知识。链接器(Linker)在把多个 .o 文件合并成最终程序时,会遇到"多个文件里有同名符号"的情况,它按以下规则处理:

3.1 符号的分类

类型 说明 例子
强符号(Strong Symbol) 普通定义的函数或已初始化的全局变量 int x = 10;、void f(){}
弱符号(Weak Symbol) 带 __attribute__((weak)) 的函数/变量 __weak void f(){}
未初始化全局变量(COMMON) 未初始化的全局变量,也算一种"次强" int y;

3.2 链接规则

  1. 强符号可以覆盖弱符号:一个强符号 + 一个弱符号 → 选强符号。
  2. 两个强符号 → 报"重复定义"错误(这就是普通情况下的报错)。
  3. 两个弱符号 → 选其中占用空间大的那个(一般不常见)。
  4. 只有弱符号、没有强符号 → 使用弱符号(这是"默认实现"被启用的场景)。

对应到我们的例子:

复制代码
库文件 xxx.c:  __weak void Defalut_Loop() { 默认死循环 }   ← 弱符号
用户 main.c:   void Defalut_Loop() { 自定义功能 }          ← 强符号
                              ↓ 链接
结果:采用用户的强符号(自定义功能生效),弱符号被"挤掉"

如果用户注释掉自己的实现,只剩下库里的弱符号,链接器就采用弱符号的默认实现。

这就是"正主来了我让位,正主没来我顶上"的底层原理。


📌 本知识点一句话总结

链接器处理同名符号遵循"强弱规则":普通定义是强符号,__weak 标记的是弱符号。强符号优先于弱符号------有强符号就用强符号(实现覆盖),没有强符号才用弱符号(默认实现兜底)。如果两个都是强符号,则直接报"重复定义"错误。这套规则是 weak 机制能够在链接期自动"二选一"的底层依据。


4. 弱定义 vs 弱引用:两种不同的用法

严格来说,__attribute__((weak)) 有两种语义,容易混淆,值得分清楚:

4.1 弱定义(Weak Definition)

给定义加 weak,表示"这是我提供的一个默认实现,可以被覆盖"。

c 复制代码
// xxx.c
__weak void Defalut_Loop(void)   // 弱定义:提供默认行为
{
    printf("默认死循环\n");
    while (1) {}
}
  • 链接时如果出现同名强符号,强符号胜出;否则用这个弱定义。
  • 本课 PDF 讲的、以及 HAL 库回调用的,都是这种"弱定义"。

4.2 弱引用(Weak Reference)

给声明 加 weak(或使用 __attribute__((weakref))),表示"这个符号可能不存在,如果链接时找不到,不要报错"。

c 复制代码
extern void Func(void) __attribute__((weak));  // 弱引用:Func 可能不存在

int main(void)
{
    if (Func)      // 判断函数指针是否为空
        Func();    // 存在才调用
}
  • 如果最终程序里没有 Func 的定义,链接不报错 ,Func 的地址是 NULL。
  • 适用场景:可选的插件/回调,没有也不影响程序运行。

对比记忆:

  • 弱定义 = "我给你一个默认值,你可以覆盖"(关键词:可覆盖)。
  • 弱引用 = "这个符号可能没有,没有就算了"(关键词:可缺失)。

本课 PDF 标题叫"理解弱引用",但例子实际演示的是弱定义(默认实现被覆盖)。这俩在日常交流里经常被混着说,我们心里清楚区别即可。


📌 本知识点一句话总结

__attribute__((weak)) 有两种语义:① 弱定义 ------给函数/变量的定义加 weak,表示提供一个可被覆盖的默认实现(关键词:可覆盖);② 弱引用 ------给声明加 weak(或 weakref),表示该符号可能不存在,链接时不报错、地址为 NULL(关键词:可缺失)。HAL 库回调属于"弱定义",可选的插件/回调属于"弱引用"。判断方法很简单:weak 加在"定义"上就是弱定义,加在"声明"上就是弱引用。


5. 完整示例:默认死循环与用户覆盖

下面用一个完整例子演示"弱定义"的覆盖机制。核心是一个叫 Defalut_Loop 的函数(注:原名是 "Default" 的拼写笔误,这里按原文保留)。

5.1 库文件提供"默认实现"

xxx.c ------ 用 __weak 定义默认实现:

c 复制代码
// xxx.c:库文件,提供弱定义(默认行为)
__weak void Defalut_Loop(void)      // 弱定义:这是一个"可被覆盖"的默认实现
{
  printf("错误: 进入默认死循环\n");  // 提示进入了默认版本
  while (1)                          // 死循环,程序停在这里
  {
  }
}

xxx.h ------ 声明时不需要 带 __weak:

c 复制代码
// xxx.h:头文件,声明时不用带 __weak(weak 只写在定义处即可)
void Defalut_Loop(void);

关键点:__weak 写在定义 处(.c 文件),声明处(.h 文件)保持普通声明即可。

5.2 用户在 main 模块重新实现(强符号覆盖)

main.c ------ 用户重新定义同名函数(不写 __weak,就是强符号):

c 复制代码
// main.c:用户重新实现同名函数,覆盖库里的默认版本
void Defalut_Loop(void)             // 注意:这里没有 __weak,是"强符号"
{
  printf("Hello World - 用户自定义功能正常运行!\n");  // 提示进入用户版本
  while (1)
  {
    printf("同名函数,使用新的 Defalut_Loop,覆盖旧方法\n"); // 每 1 秒打印一次
    HAL_Delay(1000);                // 延时 1 秒
  }
}

5.3 主函数调用

c 复制代码
// 主函数 main.c
#include "xxx.h"                     // 引入声明(不带 __weak 的那个头文件)

int main(void)
{
    BSP_Usart_Init(&huart1);         // 初始化串口(用于 printf 输出)
    Defalut_Loop();                  // 调用该函数:到底走哪个版本,由链接结果决定

    return 0;
}

main 里写的是统一的调用 Defalut_Loop(),但最终执行哪个实现,是链接阶段就定好的------这就是 weak 的巧妙之处:调用方无需感知,行为却可以切换。


📌 本知识点一句话总结

弱定义的使用分三步:① 库文件用 __weak 定义默认实现(如 __weak void Defalut_Loop(void){...});② 头文件正常声明(不带 __weak);③ 用户在任意 .c 文件重新定义同名函数(普通定义,即强符号)。main 里的调用代码完全不变,链接器会自动决定采用强符号(用户版)还是弱符号(默认版)。注意 __weak 只写在定义处,声明处保持普通写法。


6. 编译结果对比:两种情况

6.1 情况 1:只链接默认实现(用户没定义)

把用户自己的实现注释掉:

c 复制代码
// void Defalut_Loop(void)
// {
//   printf("Hello World - 用户自定义功能正常运行!\n");
//   while (1)
//   {
//     printf("同名函数,使用新的Defalut_Loop,覆盖旧方法\n");
//     HAL_Delay(1000);
//   }
// }

此时工程里只有库文件 xxx.c 里的弱定义。链接器找不到强符号,就采用弱符号。程序运行时打印:

复制代码
错误: 进入默认死循环

然后进入死循环。说明默认实现被启用了。

6.2 情况 2:链接用户自定义实现(用户有定义)

把用户实现放开注释:

c 复制代码
void Defalut_Loop(void)
{
  printf("Hello World - 用户自定义功能正常运行!\n");
  while (1)
  {
    printf("同名函数,使用新的Defalut_Loop,覆盖旧方法\n");
    HAL_Delay(1000);
  }
}

此时工程里同时有库的弱定义和用户的强定义。链接器遵循"强覆盖弱"规则,采用用户版本。程序运行时每 1 秒打印:

复制代码
Hello World - 用户自定义功能正常运行!
同名函数,使用新的Defalut_Loop,覆盖旧方法
同名函数,使用新的Defalut_Loop,覆盖旧方法
...

说明用户实现成功覆盖了默认实现。

6.3 两种情况的对比表

情况 用户是否定义 链接结果 运行表现
情况 1 未定义(注释掉) 用库的弱定义 打印"默认死循环",卡死
情况 2 已定义 用用户的强符号 打印"用户自定义功能",每秒循环

整个过程中,库的源码和 main 的调用代码一个字都没改,只是"是否提供用户实现"这一个变量,就决定了最终行为。这就是 weak 的灵活性。


📌 本知识点一句话总结

weak 的覆盖效果可以用"两个链接场景"验证:用户不定义 同名函数时,链接器采用库里的弱定义(默认实现),程序走默认逻辑;用户定义了同名函数时,链接器优先采用强符号(用户实现),默认实现被"挤掉"。整个过程库源码和调用代码完全不变,仅由"是否存在用户实现"决定最终行为,这正是 weak 机制的核心价值。


7. HAL 库里的 __weak:你早就用过了

其实在前面几课(USART/SPI/I2C 中断)里,你已经在大量使用 weak 机制,只是没意识到。

回忆一下:我们重写了 HAL_UART_RxCpltCallback、HAL_SPI_TxRxCpltCallback、HAL_I2C_MemRxCpltCallback...... 这些函数,HAL 库本身已经提供了默认实现,而我们"重新定义"它们却没有报"重复定义"错误。为什么?就是因为 HAL 库把它们定义成了 weak。

7.1 HAL 库的默认回调都是弱定义

在 stm32f1xx_hal_uart.c 里,你能看到这样的默认实现:

c 复制代码
// HAL 库提供的"默认"接收完成回调(弱定义)
__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  /* 默认什么都不做:这是一个占位,用户可覆盖 */
  UNUSED(huart);   // 防止编译警告"参数未使用"
}

类似的还有发送完成、错误、半完成等一大堆回调,都是 __weak:

c 复制代码
__weak void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { UNUSED(huart); }
__weak void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)   { UNUSED(huart); }
// ... 等等

7.2 我们"重写"回调 = 提供强符号覆盖

当你在自己的 main.c(或其他文件)里写了:

c 复制代码
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    // 你的业务逻辑
}

这就是一个强符号,链接时覆盖掉 HAL 库里那个空的弱定义。所以你能"接管"中断完成事件,而不会报重复定义。

7.3 为什么 HAL 要这么设计

因为 ST 不知道用户拿到库之后想干什么。它只能:

  • 提供一个"空实现"作为默认行为(什么都不做),保证链接一定能通过。
  • 用 __weak 标记,让用户能按需覆盖,实现自定义逻辑。

这样,用户只需要重写自己关心的那几个回调,不关心的就用默认空实现,互不干扰。这正是"缺省行为 + 按需覆盖"的设计哲学。


📌 本知识点一句话总结

HAL 库的所有中断回调(如 HAL_UART_RxCpltCallback、HAL_SPI_TxRxCpltCallback)都被定义为 __weak 的空函数,这是"默认实现(什么都不做)+ 用户可覆盖"的设计。用户在自己的工程里重新定义同名回调,就是提供一个强符号去覆盖库里的弱定义,因此不会报"重复定义"错误。这就是为什么我们之前能自由重写回调而不需要改 HAL 源码------本质用的就是弱定义覆盖机制。


8. 应用场景总结

从应用角度看,弱引用(这里主要指弱定义)本质是给软件或函数提供"缺省行为"。常见场景:

  1. 回调 / 钩子函数(HAL 库典型用法) :库定义 __weak 空回调,用户按需覆盖。
  2. 中断处理入口 :提供默认的空 XXX_IRQHandler,用户覆盖后接管中断。
  3. 驱动默认实现:硬件驱动提供默认初始化/操作,用户针对特殊硬件覆盖。
  4. 可插拔模块:用弱引用让"可选功能"缺失时程序也能正常链接运行。
  5. 调试/兜底:如示例里的"默认死循环",提示用户忘了实现某个关键函数。

一句话概括 weak 的价值:它把"必须由用户提供"变成"可选由用户提供",在保证程序一定能链接、能运行的前提下,最大程度保留了定制的灵活性。


📌 本知识点一句话总结

弱引用/弱定义的本质是为软件提供"缺省行为",把"必须实现"变成"可选实现"。典型应用包括:HAL 库的 __weak 空回调(用户按需覆盖)、默认中断处理入口、驱动默认实现、可插拔可选模块(弱引用)、以及调试用的兜底函数(如默认死循环提示)。它的核心价值是:保证程序总能链接运行的同时,保留最大程度的用户定制灵活性。


全文总结(速记卡)

  1. 是什么 :__attribute__((weak)) 是 GCC/ARM GCC 扩展,把函数/变量标记为弱符号;STM32 里简写为 __weak。
  2. 解决什么:让用户在不改库源码的情况下,用自己的同名函数覆盖默认实现。
  3. 底层机制:链接器遵循"强符号覆盖弱符号",没有强符号才用弱符号,两个强符号会报重复定义。
  4. 两种语义:弱定义(可覆盖,HAL 回调用的这种)vs 弱引用(可缺失,地址可为 NULL)。
  5. 写法 :__weak 写在定义处,头文件声明不带 __weak。
  6. 与 HAL 的关系 :HAL 所有回调都是 __weak 空函数,我们重写回调就是提供强符号覆盖。
  7. 本质:提供"缺省行为",把"必须实现"变成"可选实现",兼顾兼容性与灵活性。

相关推荐
凤城老人1 小时前
从PyQt6到Electron:一款英语字帖生成器的跨平台重构实战与踩坑全记录
javascript·重构·electron
铅笔小新z1 小时前
【stm32】DMA
stm32·单片机·嵌入式硬件
Brilliantwxx2 小时前
【STM32】 DMA中断机制、NVIC 联动与配置实战前章
stm32·单片机·嵌入式硬件·mcu
Jason_zhao_MR9 小时前
米尔电子参展预告_中国工博会2026-外发版
嵌入式硬件·机器人·工业控制
Zw-awa9 小时前
电机家族:有的电机听“目标”,有的电机听“力气”
stm32
星栈12 小时前
pnpm 12 升级实测
前端·javascript
FPGA小徐13 小时前
STM32五路气敏传感器阵列电路详解:从HHC1000分压到AD8606跟随器和ADC采样
stm32·单片机·嵌入式硬件
Ai-_Man13 小时前
您您这可以把Microsofat Copilot的多个会话比如说。左侧的多个会话一次性导出吗?不是单条会话里面的多次会对话。AI导出鸭
javascript·人工智能·ai·小程序·电脑·copilot
Horn Still Sounds15 小时前
IMX6ULL裸机|I2C通信协议 + ADC模数转换学习笔记
arm开发·笔记·单片机·嵌入式硬件