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 的整体脉络:

__attribute__((constructor/destructor)) 知识体系 GCC 属性扩展 constructor destructor main 之前执行 优先级控制 硬件初始化 main 之后执行 优先级逆序 资源清理 本质:在启动/退出流程中插入自定义函数

22.8 总结

__attribute__((constructor))__attribute__((destructor)) 是 GCC 提供的两个非常实用的扩展属性。它们让我们能在 main 函数前后自动执行代码,特别适合嵌入式系统中的硬件初始化、模块注册、资源清理等场景。

使用时要记住三点:一是合理设置优先级,避免依赖混乱;二是在构造函数中慎用动态内存分配;三是析构函数不能替代看门狗复位等异常场景的清理逻辑。

好了,这一章就到这里。如果你在实际项目中遇到过什么奇葩的构造函数问题,欢迎一起交流。


公众号:蓝海资料掘金营,微信 deep3321