22、__attribute__((constructor)) 与 ((destructor)):构造函数与析构函数
说实话,我第一次看到 __attribute__((constructor)) 这个语法时,第一反应是:C语言里怎么还有构造函数?这不是C++的专利吗?
后来在做一个嵌入式启动优化项目时,我才真正体会到它的威力。当时需要在 main 函数之前初始化一些硬件外设,又不想改启动文件。嗯,这个属性就是为这种场景量身定做的。
22.1 基本概念:程序员的"钩子"
简单来说,__attribute__((constructor)) 让一个普通函数在 main() 之前自动执行。__attribute__((destructor)) 则在 main() 退出后自动执行。你想想看,这不就是C语言版的构造函数和析构函数吗?
核心机制:
- constructor:函数在 main 之前被调用,按优先级顺序执行
- destructor:函数在 main 退出后被调用,按优先级逆序执行
- 优先级:数值越小,执行越早(constructor)或越晚(destructor)
22.2 基础用法:先跑起来再说
来看一个最简单的例子。我个人习惯用这种写法来验证执行顺序:
#include <stdio.h>
__attribute__((constructor))
void before_main(void) {
printf("1. 我在 main 之前执行!\n");
}
__attribute__((destructor))
void after_main(void) {
printf("3. 我在 main 之后执行!\n");
}
int main(void) {
printf("2. 我是 main 函数\n");
return 0;
}
输出结果:
1. 我在 main 之前执行!
2. 我是 main 函数
3. 我在 main 之后执行!
为什么会这样?说白了,编译器在生成目标文件时,把这些函数放到了特定的段(section)里。链接器在构建可执行文件时,会安排这些段在启动代码中被依次调用。
22.3 优先级控制:谁先谁后?
当你有多个构造函数时,可以用优先级参数控制顺序。数值范围一般是 0-255,101 是默认值。
#include <stdio.h>
__attribute__((constructor(101)))
void init_default(void) {
printf("默认优先级:101\n");
}
__attribute__((constructor(200)))
void init_low(void) {
printf("低优先级:200\n");
}
__attribute__((constructor(50)))
void init_high(void) {
printf("高优先级:50\n");
}
int main(void) {
printf("main 函数\n");
return 0;
}
输出结果:
高优先级:50
默认优先级:101
低优先级:200
main 函数
注意,析构函数的执行顺序正好相反——优先级数值大的先执行。
我的经验:优先级不要用得太极端。0-100 留给系统级初始化,101-200 给模块级,201-255 给应用级。我曾经见过有人把所有构造函数都设成优先级 0,结果调试时根本分不清谁先谁后。
22.4 实际应用场景
我在项目中遇到过几个典型场景,这里分享给你:
| 场景 | 说明 | 优先级建议 |
|---|---|---|
| 硬件初始化 | GPIO、时钟、外设等底层配置 | 10-50 |
| 内存池初始化 | 预分配静态内存池 | 60-80 |
| 日志系统启动 | 打开日志文件、初始化缓冲区 | 90-100 |
| 模块注册 | 驱动模块、协议栈注册 | 110-150 |
| 资源清理 | 关闭文件、释放锁、刷缓存 | 析构时用 200+ |
22.5 嵌入式实战:硬件初始化
来看一个更贴近底层的例子。假设我们要在 main 之前初始化 UART 和定时器:
#include <stdio.h>
#include <stdint.h>
// 模拟硬件寄存器
volatile uint32_t uart_ready = 0;
volatile uint32_t timer_ready = 0;
__attribute__((constructor(30)))
void uart_init_early(void) {
// 配置 UART 引脚、波特率
uart_ready = 1;
printf("[UART] 初始化完成\n");
}
__attribute__((constructor(70)))
void timer_init(void) {
// 等待 UART 就绪后再初始化定时器
if (!uart_ready) {
printf("[ERROR] UART 未就绪,定时器初始化失败\n");
return;
}
timer_ready = 1;
printf("[TIMER] 初始化完成\n");
}
__attribute__((destructor(200)))
void uart_deinit(void) {
printf("[UART] 关闭串口\n");
uart_ready = 0;
}
__attribute__((destructor(150)))
void timer_deinit(void) {
printf("[TIMER] 停止定时器\n");
timer_ready = 0;
}
int main(void) {
printf("[MAIN] 系统启动完成,开始运行\n");
// 主逻辑...
return 0;
}
注意:在嵌入式环境中,构造函数执行时堆可能还没完全初始化。如果你在构造函数里调用 malloc,可能会出问题。我建议构造函数里只用静态分配的内存。
22.6 避坑指南
我曾经踩过几个坑,写出来让你少走弯路:
- 依赖顺序问题:构造函数 A 依赖构造函数 B 的结果,但优先级没设对。结果 A 先跑,B 还没初始化,直接崩了。解决方案:显式设置优先级,或者用状态标志做依赖检查。
- 静态库链接:如果构造函数在静态库里,链接器可能因为没被引用而把它优化掉。我一般用
--whole-archive链接选项强制包含。 - 多次调用:同一个函数不要同时加 constructor 和 destructor,否则行为未定义。
- 异常退出:如果程序被 kill -9 或者硬件看门狗复位,析构函数不会执行。所以不要把关键清理逻辑只放在 destructor 里。
22.7 知识体系图
下面这张图帮你理清 constructor/destructor 的整体脉络:
22.8 总结
__attribute__((constructor)) 和 __attribute__((destructor)) 是 GCC 提供的两个非常实用的扩展属性。它们让我们能在 main 函数前后自动执行代码,特别适合嵌入式系统中的硬件初始化、模块注册、资源清理等场景。
使用时要记住三点:一是合理设置优先级,避免依赖混乱;二是在构造函数中慎用动态内存分配;三是析构函数不能替代看门狗复位等异常场景的清理逻辑。
好了,这一章就到这里。如果你在实际项目中遇到过什么奇葩的构造函数问题,欢迎一起交流。