c++怎么实现一个函数先于main函数运行或之后运行
这是个经典题。方法有很多,但只有两种是标准 C++,其余都是平台扩展。
一、在 main 之前运行
方法 1:全局/静态对象的构造函数 —— 标准做法
;
static Init init_; // 命名空间作用域
int
before main
main
after main
唯一可移植的方案。 原理见 memory-layout.md:这些构造函数的指针被放进 .init_array,由 C 运行时在 __libc_start_main 里遍历调用。
方法 2:用全局变量的初始化式调用函数
不想定义类时的简写:
static int dummy_ = ; // 逗号表达式
static const bool ok_ = ;
更现代的写法是 IIFE + lambda:
static const int dummy_ = ;
C++17 起可以放进头文件(inline 变量保证全程序只有一份,只初始化一次):
// header.h
inline const int auto_init_ = ;
方法 3:GCC / Clang 的 constructor 属性
void
// 优先级:数字越小越早
void
void
| 优先级 | 说明 |
|---|---|
| 0–100 | 保留给实现,不要用 |
| 101–65535 | 用户可用,数值越小越先执行(析构则相反) |
| 不指定 | 视为最低优先级,按链接顺序 |
本质也是往 .init_array 里塞函数指针,和 C++ 全局对象走同一套机制。它的优势是可以用在纯 C 代码里。
方法 4:MSVC 的 .CRT$XCU 段
MSVC 不支持 __attribute__,等价物是往 CRT 初始化段里塞函数指针:
static void
extern "C" __declspec
void = before_main;
原理:链接器把 .CRT$XCA 到 .CRT$XCZ 的所有段按字母序合并成一个函数指针数组,CRT 启动时遍历调用。XCU 里的 U 表示 User。
顺带一提,_MSC_VER 下还有一个更简单的排序工具:
写在 .cpp 顶部,控制这个 TU 里所有全局对象的初始化档次。
方法 5:动态库的加载钩子
// Linux
void
void
// 老式写法:定义 _init / _fini(已不推荐)
// Windows
BOOL WINAPI
注意 DllMain 有严格限制(loader lock):不能调用 LoadLibrary、不能创建线程、不能等待同步对象,否则死锁。
跨平台封装
(不过在 C++ 里,直接用方法 1 或 2 更简单也更可移植,这套宏主要是给 C 代码用的。)
二、在 main 之后运行
方法 1:全局对象的析构函数 —— 标准做法
如上面的 Init::~Init()。按构造完成的反序执行。
方法 2:atexit() —— C 标准
void
void
int
// 输出:cleanup2 cleanup1 ← LIFO(后进先出)
限制:
- 只能注册无参无返回的函数(
void(*)())—— 想传数据只能靠全局变量 - 标准只保证至少能注册 32 个(glibc 实际是动态扩展的)
- 注册顺序的反序执行
方法 3:__attribute__((destructor))(GCC/Clang)
见上,塞进 .fini_array。
方法 4:__cxa_atexit —— 底层机制
这是 C++ 全局对象析构的实际实现。它比 atexit 多带一个对象指针参数:
extern "C" int ;
编译器为每个需要析构的静态对象自动调用它。关键点:atexit 注册的函数和全局对象的析构函数在同一条链上,按注册的反序统一执行:
;
void
A a1; // 构造时注册 ~a1
int
// 输出:~A(a2) f ~A(a1) ← 严格按注册的反序
方法 5:std::at_quick_exit(C++11)
配合 std::quick_exit() 使用,用于"快速退出但仍做少量清理"的场景:
;
; // 【不】调用全局析构和 atexit,只调 at_quick_exit
三、完整的执行顺序
程序启动
│
├─ 【静态初始化】零初始化 + 常量初始化(无代码执行)
│
├─ 【动态初始化】遍历 .init_array / .CRT$XC*
│ ├─ __attribute__((constructor)) 按优先级
│ └─ 全局对象构造函数(同 TU 内按定义顺序,跨 TU 【顺序不确定】)
│ 每个有析构的对象在此时 __cxa_atexit 注册自己的析构
│
├─ main()
│ └─ 块作用域 static 对象在【首次执行到声明处】构造并注册析构
│
├─ main 返回 → exit(返回值)
│
├─ 【退出处理】__run_exit_handlers:按【注册的反序】统一执行
│ ├─ atexit / __cxa_atexit 注册的函数
│ ├─ 全局与局部 static 对象的析构函数
│ └─ .fini_array(__attribute__((destructor))、库的卸载钩子)
│
├─ _IO_cleanup():flush 所有 stdio 流
│
└─ exit_group() 系统调用 → 进程终止
四、六个必须知道的陷阱
陷阱 1:跨 TU 的初始化顺序未定义
static initialization order fiasco —— 最经典的一个:
// a.cpp
Logger logger;
// b.cpp
Config config; // 构造函数里用了 logger —— 可能还没构造!
标准只保证同一 TU 内按定义顺序,跨 TU 完全不确定(取决于链接顺序)。
解法:Meyers' Singleton —— 用块作用域 static 的惰性初始化:
Logger&
陷阱 2:析构顺序 fiasco
反过来同样成立 —— A 的析构函数里用了 B,而 B 可能已经析构了。
解法:故意不销毁(leaky singleton)
Logger&
进程退出时操作系统会回收全部内存,所以这不是真正的泄漏。Google C++ Style Guide 明确推荐这种做法(用 absl::NoDestructor 或裸 new),理由就是避免不可控的析构顺序。
陷阱 3:这几种退出方式不会触发析构和 atexit
| 方式 | 全局析构 | atexit |
stdio flush |
|---|---|---|---|
return from main / std::exit() |
✓ | ✓ | ✓ |
std::quick_exit() |
✗ | ✗(只跑 at_quick_exit) |
✗ |
std::_Exit() / _exit() |
✗ | ✗ | ✗ |
std::abort() / 未捕获异常 / 信号 |
✗ | ✗ | ✗ |
所以 abort() 或崩溃时,没带 \n 的 printf 输出全部丢失(行缓冲没 flush)。调试打印要用 stderr 或手动 fflush。
陷阱 4:exit() 在多线程程序里很危险
std::exit 只终止调用它的线程的执行流,其他线程仍在运行。此时全局对象正在被析构 —— 其他线程访问它们就是 use-after-destruction。
C++11 之后推荐从 main 正常返回,并在此之前 join 所有线程。
陷阱 5:main 之前能用 std::cout 吗
能。 <iostream> 里藏着一个静态对象:
static std::ios_base::Init __ioinit; // 每个包含 <iostream> 的 TU 都有一份
它保证在任何包含了该头文件的 TU 的全局对象构造之前,cout/cin/cerr 已可用。这是标准明确规定的([ios::Init])。
但 printf 就没有这个保证 —— 虽然实践中 glibc 的 stdio 在更早的阶段就初始化好了。
陷阱 6:优先级只在模块内可靠
void ; // a.cpp
void ; // b.cpp
同一个可执行文件里,链接器会把 .init_array 按优先级排序,所以有效。但跨动态库就不保证了 —— 库的加载顺序由依赖关系决定,与优先级无关。
五、实战:自注册工厂
这是"main 之前运行"最常见的真实用途 —— 让新类型的注册代码和类型定义放在一起,不用改中央的注册表:
// factory.h
;
// circle.cpp —— 新增类型时只改这一个文件
这里有个大坑:静态库会把它优化掉
如果 circle.cpp 被编译进静态库 libshapes.a,链接器扫描时发现没有任何符号引用 circle.o,就根本不会把它链进来 —— 注册代码从未执行,create("Circle") 返回 nullptr。
(回忆 memory-layout.md 第四章:静态库链接是按需拉取 .o,不是全部拷贝。)
解法:
# GCC/Clang
# MSVC
# CMake
或者干脆改用动态库(.so/.dll 整个加载,不做按需裁剪),或者在某个必然被链接的地方强制引用一下那个符号。
这个坑在插件架构、测试框架(Google Test 的 TEST() 宏就是这个套路)、序列化库里反复出现。
六、速查表
| 需求 | 方法 | 可移植性 |
|---|---|---|
| main 前,最标准 | 全局对象构造函数 | ✅ 标准 |
| main 前,简写 | static const int _ = []{...}(); |
✅ 标准 |
| main 前,头文件里 | inline const int _ = []{...}(); |
✅ C++17 |
| main 前,纯 C | __attribute__((constructor)) |
GCC/Clang |
| main 前,MSVC | .CRT$XCU 或 #pragma init_seg |
MSVC |
| main 前,控制顺序 | 优先级参数(仅模块内可靠) | 平台扩展 |
| main 后,最标准 | 全局对象析构函数 | ✅ 标准 |
| main 后,注册回调 | atexit() |
✅ 标准(C) |
| main 后,带参数 | __cxa_atexit(编译器自动用) |
Itanium ABI |
| main 后,快速退出 | std::at_quick_exit + quick_exit |
✅ C++11 |
| 避免顺序问题 | Meyers' Singleton(构造)/ leaky singleton(析构) | ✅ 标准 |
一句话回答
标准做法只有一条:利用静态存储期对象的构造函数(
main前)和析构函数(main后)。 编译器把构造函数指针放进.init_array,由 C 运行时在__libc_start_main里遍历调用;析构则通过__cxa_atexit注册,退出时与atexit的回调在同一条链上按注册的反序执行。 GCC/Clang 的__attribute__((constructor/destructor))和 MSVC 的.CRT$XCU是同一套机制的非标准入口,主要给 C 代码用。 最大的坑是跨 TU 顺序不确定 —— 构造侧用 Meyers' Singleton 解决,析构侧用"故意不销毁"解决。另外abort/_exit/quick_exit都不会触发这些清理。
暂无评论,欢迎留下第一条评论。