番外篇 - 从 STM32 的 __weak说起弱符号、回调函数与虚函数
番外篇 - 从 STM32 的 __weak说起弱符号、回调函数与虚函数
Sarzn从 STM32 的
__weak 说起:弱符号、回调函数与虚函数
读 STM32 HAL 库的源码时,你会发现一个奇怪的关键字 __weak
比如处理 GPIO 外部中断的这段代码:
1 | void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin) |
HAL_GPIO_EXTI_Callback 是个空函数,前面挂着
__weak,注释还说”别改我,要用就去别处重写”。这到底是什么魔法?它和大家常说的”回调函数”“虚函数”又是什么关系?
一、__weak
是什么:弱符号
__weak 不是 C 语言标准的一部分,而是编译器(ARM GCC /
armclang 等)提供的扩展,等价于 __attribute__((weak))
它把一个函数(或变量)标记为弱符号(weak symbol)
要理解它,先理解链接器眼中的”符号”分两种:
- 强符号(strong symbol):普通的函数/全局变量定义
- 弱符号(weak symbol):加了
__weak标记的定义,优先级低
链接器的规则很简单:
同一个名字,如果既有强定义又有弱定义,用强的、忽略弱的;如果只有弱定义,就用弱的
所以 __weak
的本质是提供一个可被覆盖的默认实现。在这个函数没人强调的的时候顶上,有人提供时就自动让位
注意!!这一切发生在链接阶段,是静态决定的 也就是说程序一旦编译链接完成, 这个函数用的是哪个版本就定死了,运行时不会再变 请记住这一点,后面对比虚函数时会用到
二、HAL 如何用弱符号实现”可重写的回调”
现在回头看 HAL 的设计就通透了:
- HAL 在库里给
HAL_GPIO_EXTI_Callback提供了一个__weak的空默认实现。这样即使你什么都不写,整个工程也能正常链接通过(只是中断里什么都不做) - 当你想处理中断时,只需在自己的文件(比如
main.c)里定义一个同名的、没有__weak的强函数:
1 | // 你在自己的文件里写的强定义,会覆盖掉 HAL 的弱默认实现 |
- 链接器看到有强定义,就把 HAL 那个空的弱版本丢掉,中断真正触发时调用的就是你这个
这就是 C 语言里伪重写的常见手段, C 没有面向对象的重写机制,HAL 借助弱符号,巧妙地给你留了个覆盖默认行为的口子
一个常见误解:
__weak本身不等于”回调函数”。__weak只是”弱符号”这个链接机制;而”回调”是HAL_GPIO_EXTI_Callback这个函数扮演的角色(它被中断处理流程回头调用)。HAL 是”用弱符号这个机制,让回调的默认实现可以被覆盖”。一个弱符号函数未必是回调,回调也未必用弱符号实现。这两个根本就是两个概念
三、那回调函数到底是什么?
回调函数(callback)是一种通用的编程模式,核心思想叫控制反转(Inversion of Control):
打起十分精神看这段话: > 通常是”你的代码调用库”;而回调反过来 – “库在合适的时机回头调用你的代码”。
你只管提供一份实现(填空),由别人决定”什么时候调它”(我联想到了 qsort, 你提供一份比较函数的实现, 而 qsort 决定什么时候调用它) 中断来了调你的处理函数、按钮被点了调你的事件处理、排序时调你的比较函数……都是回调
在 C 语言里,实现回调主要有两种:
函数指针
把函数的地址当参数传出去,让对方调用。比如标准库的
qsort:
1 | int cmp(const void *a, const void *b) { |
弱符号
不传指针,而是约定一个固定的函数名,库里放个弱默认实现,你按名字提供强定义来覆盖
这两种都是回调,只是绑定的方式不同:函数指针是运行期传递、可以随时换不同的函数;弱符号是链接期固定、全程序只能有一个覆盖版本
四、__weak和虚函数是一回事吗?
很多人会把弱函数和 虚函数(virtual function) 搞混,因为两者”感觉上”都像在”重写一个函数”。但它们其实完全不同的概念
虚函数是 C++ 面向对象专属的概念,离不开类和继承:
1 | struct Animal { |
机制是虚函数表(vtable) 再加上运行期动态分派 :通过基类指针调用时,程序在运行时根据对象的实际类型决定调哪个版本。也就是多态(polymorphism)
注意这里的关键词, 运行期、对象的实际类型。这和弱符号的链接期、静态固定, 天差地别
五、__weak vs 虚函数:
__weak |
虚函数 | |
|---|---|---|
| 适用语言 | 通用(C / C++ / JS / Python…) | C++ 面向对象专属 |
| 实现机制 | 弱符号 | vtable,运行期动态分派 |
| 绑定时机 | 链接期, 确定只有一个 | 运行期, 按对象类型决定 |
| 依附对象? | 不需要,独立函数即可 | 必须通过对象/指针调用 |
| 变的是什么? | 你是否提供了覆盖版本 | 对象的实际类型是什么 |
| 能否多种实现并存 | 弱符号全程序只能一个覆盖 | 每个派生类都能有自己的版本 |
最重要的是最后一行:虚函数允许同一段调用代码,对不同对象表现不同(Dog
和 Cat 各汪各喵); 而__weak ,整个程序里
HAL_GPIO_EXTI_Callback 只有一个版本
这就是机制层面的区别
小结
__weak(弱符号): 指链接期默认实现可被同名强定义覆盖- 回调函数: 控制反转,库在合适的时机回头调用你的代码;HAL 用弱符号这个机制来实现可覆盖的回调
- 虚函数: 是 C++ 面向对象里一种多态的手段, 靠 vtable 在运行期按对象类型分派
所以 HAL 里那个 HAL_GPIO_EXTI_Callback,是一个
用弱符号实现的回调函数



