GCC编译(4)构造和析构函数

GCC编译(4)构造和析构函数

Author: Once Day Date: 2026年2月19日

一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦...

漫漫长路,有人对你微笑过嘛...

全系列文章可参考专栏: 编译构建工具链_Once-Day的博客-CSDN博客

参考文章:


文章目录

  • GCC编译(4)构造和析构函数
        • [1. 构造和析构概述](#1. 构造和析构概述)
        • [2. GCC 构造函数属性](#2. GCC 构造函数属性)
        • [3. GCC 析构函数属性](#3. GCC 析构函数属性)
        • [4. GCC CleanUp函数](#4. GCC CleanUp函数)
        • [4. 实例](#4. 实例)
1. 构造和析构概述

从程序初始化视角看,广义"构造"与"析构"并不局限于类对象,而是指整个进程生命周期内的初始化与清理阶段,这一过程由编译器、链接器与运行时协同完成。

以 GCC + glibc 为例,程序入口并非 main,而是 _start,其由 crt1.o 提供。_start 负责建立栈环境后调用 __libc_start_main,随后触发 .init_array 中登记的构造函数,最后才进入 main。因此,全局/静态对象构造、本地静态变量初始化以及带有 __attribute__((constructor)) 标记的函数,都属于这一广义构造阶段。

链接层面上,GCC 会将具有静态存储期且需要动态初始化的对象注册进 .init_array 段;对应的析构函数则进入 .fini_array。运行时在正常退出路径(如 exit)中调用 .fini_array 中的函数,顺序与构造相反。可以用如下方式定义构造/析构钩子:

cpp 复制代码
__attribute__((constructor))
static void global_init() {
    // 进程初始化逻辑
}

__attribute__((destructor))
static void global_fini() {
    // 进程清理逻辑
}

在动态库场景中,构造与析构与 dlopen/dlclose 绑定,由动态加载器 ld-linux.so 负责触发。需要注意,异常退出(如 _exit 或信号终止)不会执行析构阶段,因此资源清理策略必须考虑异常路径。

此外,跨编译单元的静态初始化顺序不确定问题(Static Initialization Order Fiasco)本质上源于 .init_array 的链接顺序依赖,可通过函数内静态对象或显式初始化接口规避。

在工程实践中,理解这一宏观构造/析构流程,有助于设计插件系统、运行时注册机制以及高可靠后台服务的启动与退出模型。

2. GCC 构造函数属性

GCC 的 constructor 属性本质上是对 ELF 初始化机制的封装,它允许在 main 执行前自动调用指定函数,而无需显式注册。编译阶段,带有 __attribute__((constructor)) 的函数会被放入 .init_array 段,由运行时在 __libc_start_main 内部按顺序遍历执行。

这一机制既适用于可执行文件,也适用于共享库,在 dlopen 时同样会触发对应构造逻辑,因此常被用于插件注册、日志系统初始化或运行时框架搭建。

GCC 支持为构造函数指定优先级,其语法形式为:

c 复制代码
__attribute__((constructor(101)))
static void init_func(void) {
    // 初始化逻辑
}

优先级本质是一个整数,数值越小优先级越高,越早执行;未显式指定时默认值通常为 65535。GCC 会根据优先级生成不同子段,如 .init_array.0101,链接器按段名排序后组织执行顺序。因此,不同优先级之间具备稳定的先后关系,而同一优先级内的顺序仍依赖链接单元顺序,不应依赖其确定性。

析构阶段对应 __attribute__((destructor(priority))),其执行顺序与构造相反:优先级大的先析构,数值小的后析构。这种对称设计有助于构建依赖链。例如基础设施层使用较小数值(如 100),业务层使用较大数值(如 200),可确保基础设施先初始化、后销毁。

在工程实践中,应避免滥用优先级耦合模块关系,更推荐显式初始化接口或延迟初始化模式,以降低跨编译单元的隐式依赖风险。

3. GCC 析构函数属性

GCC 的 __attribute__((destructor)) 是对 ELF 终止机制的封装,用于在进程正常结束时自动调用指定函数,其语义与 constructor 对称。编译后,函数会被放入 .fini_array 段,由运行时在 exit() 流程中统一触发。具体路径通常是 main 返回或显式调用 exit 后,glibc 执行已注册的 atexit 回调以及 .fini_array 中的函数,因此它适用于全局资源清理、日志刷写或框架级卸载逻辑。

基本用法如下:

c 复制代码
__attribute__((destructor))
static void cleanup(void) {
    // 释放资源
}

与构造属性类似,析构函数也支持优先级:

c 复制代码
__attribute__((destructor(200)))
static void cleanup2(void) {}

优先级数值越小,在构造阶段越早执行;而在析构阶段顺序相反,数值小的后执行。GCC 通过生成类似 .fini_array.0200 的段名实现排序,链接器按字典序组织,因此不同优先级之间具有确定性,但同级之间仍依赖目标文件的链接顺序。

需要注意的是,析构属性只在"正常退出路径"生效;若调用 _exit、发生未捕获信号终止或进程崩溃,则不会执行。此外,在共享库中,析构函数会在 dlclose 时触发,由动态加载器负责调用。这意味着在插件体系下应确保析构逻辑幂等且不依赖已卸载模块。

工程实践中,析构函数适合做底层框架收尾,而业务级资源管理仍建议采用 RAII 或显式生命周期控制,以避免隐式执行顺序带来的耦合风险。

4. GCC CleanUp函数

GCC 的 cleanup 属性是一种作用于变量级别的扩展机制,用于在变量离开作用域时自动调用指定函数,其设计目标是为 C 语言提供类似 C++ RAII 的能力。它的本质是在编译阶段为变量的生命周期插入隐式清理代码,当控制流离开当前作用域(包括 returngoto、异常跳转等路径)时自动执行,从而避免遗漏资源释放逻辑。

这一机制常用于文件句柄、锁、内存等资源管理,在内核或系统级代码中尤为常见。

基本用法如下:

c 复制代码
static void free_wrapper(void *p) {
    void **ptr = (void **)p;
    free(*ptr);
}

void foo() {
    char *buf __attribute__((cleanup(free_wrapper))) = malloc(1024);
    if (!buf) return;
    // 使用 buf
} 

cleanup 函数接收的是变量地址,因此通常需要做一次间接访问。与 constructor/destructor 不同,cleanup 不参与 ELF 初始化段,而是在当前函数的栈展开路径中生成调用代码,因此属于编译期语义扩展。它只对具有自动存储期的变量有效,不能用于全局或静态变量。

在 GCC 实现层面,cleanup 通过在作用域末尾插入隐式调用实现,与异常处理表或栈展开机制协作。在开启优化(如 -O2)时,编译器仍会正确保留清理语义,但需要注意函数内联或变量生命周期缩短可能影响调用时机。

该机制虽增强了 C 的资源管理能力,但可读性与可移植性较弱(Clang 支持但 MSVC 不支持),在跨平台项目中应谨慎使用。

4. 实例

下面通过一个完整示例,从源码到二进制层面观察 GCC 构造与析构函数在 ELF 中的体现。

示例代码如下,包含显式的 constructordestructor 属性函数:

c 复制代码
// demo.c
#include <stdio.h>

__attribute__((constructor(200)))
static void my_init(void) {
    printf("my_init called\n");
}

__attribute__((destructor(200)))
static void my_fini(void) {
    printf("my_fini called\n");
}

int main(void) {
    printf("main called\n");
    return 0;
}

使用 GCC 编译时建议关闭优化以便观察符号:

bash 复制代码
gcc -O0 -g demo.c -o demo

程序执行顺序将是 my_init -> main -> my_fini,说明构造函数在 main 前执行,析构函数在正常退出路径中触发。接下来从 ELF 结构验证其实现机制。首先查看符号表:

bash 复制代码
nm -C demo | grep my_

典型输出如下:

bash 复制代码
0000000000001139 t my_init
0000000000001155 t my_fini

可以看到符号类型为小写 t,表示位于本地 .text 段的静态函数。关键在于它们如何被注册。使用 readelf 查看初始化数组:

bash 复制代码
readelf -S demo | grep -E 'init_array|fini_array'

输出类似:

bash 复制代码
[20] .init_array        INIT_ARRAY
[21] .fini_array        FINI_ARRAY

进一步查看内容:

bash 复制代码
readelf -x .init_array demo
readelf -x .fini_array demo

可以看到数组中存放的是函数地址(即 my_initmy_fini 的入口地址)。链接阶段,GCC 将带属性的函数指针写入对应段,运行时 __libc_start_main 遍历 .init_array,而 exit 流程遍历 .fini_array

若指定优先级 200,实际段名可能表现为 .init_array.0200,可通过:

bash 复制代码
readelf -S demo | grep init_array

观察子段命名与排序情况。由此可见,构造与析构属性并非语法层魔法,而是通过 ELF 段机制与运行时约定完成注册与调度,这也是理解其执行顺序与跨模块行为的关键基础。

Once Day

也信美人终作土,不堪幽梦太匆匆......
如果这篇文章为您带来了帮助或启发,不妨点个赞👍和关注!
(。◕‿◕。)感谢您的阅读与支持~~~

相关推荐
今儿敲了吗1 小时前
24| 字符串
数据结构·c++·笔记·学习·算法
StandbyTime1 小时前
C语言学习-菜鸟教程C经典100例-练习76
c语言
橘色的喵1 小时前
嵌入式 Telnet 调试 Shell 重构: 纯 POSIX 轻量化实现
c++
橘色的喵1 小时前
ztask: 一个C++14编写的、 类型安全、RAII 与模板化任务调度器
c++
StandbyTime1 小时前
C语言学习-菜鸟教程C经典100例-练习77
c语言
小龙报1 小时前
【51单片机】不止是调光!51 单片机 PWM 实战:呼吸灯 + 直流电机正反转 + 转速控制
数据结构·c++·stm32·单片机·嵌入式硬件·物联网·51单片机
彩妙不是菜喵2 小时前
C++:深入浅出讲解=>多态
开发语言·c++
Zevalin爱灰灰2 小时前
针对汽车工业软件安全性的C语言编码规范——MISRA C
c语言·开发语言·汽车·嵌入式
lightqjx3 小时前
【C++】C++11 - Lambda表达式+包装器
开发语言·c++·c++11·lambda·包装器