ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

【stm32】理解弱引用

【stm32】理解弱引用 铅笔小新z个人主页博客专栏stm32学习滴水不绝可穿石步履不休能至渊。理解弱引用 ——__attribute__((weak))到底是个啥面向初学者的通俗解读配合逐行注释的代码以及每个知识点后的一句话总结。本文带你彻底搞懂__weak的来龙去脉以及它和 HAL 库回调函数的隐秘联系。目录从一个问题说起为什么需要弱引用什么是__attribute__((weak))强符号与弱符号链接器怎么选弱定义 vs 弱引用两种不同的用法完整示例默认死循环与用户覆盖编译结果对比两种情况HAL 库里的__weak你早就用过了应用场景总结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 库工程默认用的就是 GCCMDK-ARM 用的是 ARMCC但 ST 也做了兼容宏所以在 STM32 里__weak很常见。2.2 语法// 给函数定义加 weak 属性__attribute__((weak))voidFunc(void){// 默认实现}在 STM32 的 HAL 库里ST 定义了一个简写宏在stm32f1xx_hal_def.h里#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 链接规则强符号可以覆盖弱符号一个强符号 一个弱符号 → 选强符号。两个强符号→ 报重复定义错误这就是普通情况下的报错。两个弱符号→ 选其中占用空间大的那个一般不常见。只有弱符号、没有强符号→ 使用弱符号这是默认实现被启用的场景。对应到我们的例子库文件 xxx.c __weak void Defalut_Loop() { 默认死循环 } ← 弱符号 用户 main.c void Defalut_Loop() { 自定义功能 } ← 强符号 ↓ 链接 结果采用用户的强符号自定义功能生效弱符号被挤掉如果用户注释掉自己的实现只剩下库里的弱符号链接器就采用弱符号的默认实现。这就是正主来了我让位正主没来我顶上的底层原理。 本知识点一句话总结链接器处理同名符号遵循强弱规则普通定义是强符号__weak标记的是弱符号。强符号优先于弱符号——有强符号就用强符号实现覆盖没有强符号才用弱符号默认实现兜底。如果两个都是强符号则直接报重复定义错误。这套规则是 weak 机制能够在链接期自动二选一的底层依据。4. 弱定义 vs 弱引用两种不同的用法严格来说__attribute__((weak))有两种语义容易混淆值得分清楚4.1 弱定义Weak Definition给定义加 weak表示这是我提供的一个默认实现可以被覆盖。// xxx.c__weakvoidDefalut_Loop(void)// 弱定义提供默认行为{printf(默认死循环\n);while(1){}}链接时如果出现同名强符号强符号胜出否则用这个弱定义。本课 PDF 讲的、以及 HAL 库回调用的都是这种弱定义。4.2 弱引用Weak Reference给声明加 weak或使用__attribute__((weakref))表示这个符号可能不存在如果链接时找不到不要报错。externvoidFunc(void)__attribute__((weak));// 弱引用Func 可能不存在intmain(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定义默认实现// xxx.c库文件提供弱定义默认行为__weakvoidDefalut_Loop(void)// 弱定义这是一个可被覆盖的默认实现{printf(错误: 进入默认死循环\n);// 提示进入了默认版本while(1)// 死循环程序停在这里{}}xxx.h—— 声明时不需要带__weak// xxx.h头文件声明时不用带 __weakweak 只写在定义处即可voidDefalut_Loop(void);关键点__weak写在定义处.c文件声明处.h文件保持普通声明即可。5.2 用户在 main 模块重新实现强符号覆盖main.c—— 用户重新定义同名函数不写__weak就是强符号// main.c用户重新实现同名函数覆盖库里的默认版本voidDefalut_Loop(void)// 注意这里没有 __weak是强符号{printf(Hello World - 用户自定义功能正常运行\n);// 提示进入用户版本while(1){printf(同名函数使用新的 Defalut_Loop覆盖旧方法\n);// 每 1 秒打印一次HAL_Delay(1000);// 延时 1 秒}}5.3 主函数调用// 主函数 main.c#includexxx.h// 引入声明不带 __weak 的那个头文件intmain(void){BSP_Usart_Init(huart1);// 初始化串口用于 printf 输出Defalut_Loop();// 调用该函数到底走哪个版本由链接结果决定return0;}main里写的是统一的调用Defalut_Loop()但最终执行哪个实现是链接阶段就定好的——这就是 weak 的巧妙之处调用方无需感知行为却可以切换。 本知识点一句话总结弱定义的使用分三步① 库文件用__weak定义默认实现如__weak void Defalut_Loop(void){...}② 头文件正常声明不带__weak③ 用户在任意.c文件重新定义同名函数普通定义即强符号。main里的调用代码完全不变链接器会自动决定采用强符号用户版还是弱符号默认版。注意__weak只写在定义处声明处保持普通写法。6. 编译结果对比两种情况6.1 情况 1只链接默认实现用户没定义把用户自己的实现注释掉// void Defalut_Loop(void)// {// printf(Hello World - 用户自定义功能正常运行\n);// while (1)// {// printf(同名函数使用新的Defalut_Loop,覆盖旧方法\n);// HAL_Delay(1000);// }// }此时工程里只有库文件xxx.c里的弱定义。链接器找不到强符号就采用弱符号。程序运行时打印错误: 进入默认死循环然后进入死循环。说明默认实现被启用了。6.2 情况 2链接用户自定义实现用户有定义把用户实现放开注释voidDefalut_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里你能看到这样的默认实现// HAL 库提供的默认接收完成回调弱定义__weakvoidHAL_UART_RxCpltCallback(UART_HandleTypeDef*huart){/* 默认什么都不做这是一个占位用户可覆盖 */UNUSED(huart);// 防止编译警告参数未使用}类似的还有发送完成、错误、半完成等一大堆回调都是__weak__weakvoidHAL_UART_TxCpltCallback(UART_HandleTypeDef*huart){UNUSED(huart);}__weakvoidHAL_UART_ErrorCallback(UART_HandleTypeDef*huart){UNUSED(huart);}// ... 等等7.2 我们重写回调 提供强符号覆盖当你在自己的main.c或其他文件里写了voidHAL_UART_RxCpltCallback(UART_HandleTypeDef*huart){// 你的业务逻辑}这就是一个强符号链接时覆盖掉 HAL 库里那个空的弱定义。所以你能接管中断完成事件而不会报重复定义。7.3 为什么 HAL 要这么设计因为 ST 不知道用户拿到库之后想干什么。它只能提供一个空实现作为默认行为什么都不做保证链接一定能通过。用__weak标记让用户能按需覆盖实现自定义逻辑。这样用户只需要重写自己关心的那几个回调不关心的就用默认空实现互不干扰。这正是缺省行为 按需覆盖的设计哲学。 本知识点一句话总结HAL 库的所有中断回调如HAL_UART_RxCpltCallback、HAL_SPI_TxRxCpltCallback都被定义为__weak的空函数这是默认实现什么都不做 用户可覆盖的设计。用户在自己的工程里重新定义同名回调就是提供一个强符号去覆盖库里的弱定义因此不会报重复定义错误。这就是为什么我们之前能自由重写回调而不需要改 HAL 源码——本质用的就是弱定义覆盖机制。8. 应用场景总结从应用角度看弱引用这里主要指弱定义本质是给软件或函数提供缺省行为。常见场景回调 / 钩子函数HAL 库典型用法库定义__weak空回调用户按需覆盖。中断处理入口提供默认的空XXX_IRQHandler用户覆盖后接管中断。驱动默认实现硬件驱动提供默认初始化/操作用户针对特殊硬件覆盖。可插拔模块用弱引用让可选功能缺失时程序也能正常链接运行。调试/兜底如示例里的默认死循环提示用户忘了实现某个关键函数。一句话概括 weak 的价值它把必须由用户提供变成可选由用户提供在保证程序一定能链接、能运行的前提下最大程度保留了定制的灵活性。 本知识点一句话总结弱引用/弱定义的本质是为软件提供缺省行为把必须实现变成可选实现。典型应用包括HAL 库的__weak空回调用户按需覆盖、默认中断处理入口、驱动默认实现、可插拔可选模块弱引用、以及调试用的兜底函数如默认死循环提示。它的核心价值是保证程序总能链接运行的同时保留最大程度的用户定制灵活性。全文总结速记卡是什么__attribute__((weak))是 GCC/ARM GCC 扩展把函数/变量标记为弱符号STM32 里简写为__weak。解决什么让用户在不改库源码的情况下用自己的同名函数覆盖默认实现。底层机制链接器遵循强符号覆盖弱符号没有强符号才用弱符号两个强符号会报重复定义。两种语义弱定义可覆盖HAL 回调用的这种vs 弱引用可缺失地址可为 NULL。写法__weak写在定义处头文件声明不带__weak。与 HAL 的关系HAL 所有回调都是__weak空函数我们重写回调就是提供强符号覆盖。本质提供缺省行为把必须实现变成可选实现兼顾兼容性与灵活性。
返回列表