全局变量初始化与销毁
先看代码:
#include <iostream> #include <memory> class Widget { public: Widget() { std::cout << "constructor" << ' '; } ~Widget() { std::cout << "destructor" << ' '; } }; std::unique_ptr<Widget> widget = std::make_unique<Widget>(); int main() { std::cout << "main" << ' '; return 0; }这段代码会输出什么?
先说答案,会输出:constructor main destructor
为什么呢?
一、初始化:分两个阶段,不是一次完成
C++ 标准[basic.start.static]/[basic.start.dynamic]把全局/静态对象的初始化分为:
| 阶段 | 时机 | 包含内容 |
|---|---|---|
静态初始化 | 程序启动最早期(main 之前很久) | 零初始化、常量初始化(如 |
动态初始化 | main 之前,但可能延迟到首次使用 | 需要运行代码的初始化(如调用构造函数、 |
举例:
int a = 0; int b = 42; int c = some_func(); Widget w; ```
二、构造顺序:同一文件内确定,跨文件未定义
```cpp
// file_a.cpp
A globalA;
// file_b.cpp
B globalB;
globalA和globalB谁先构造是未定义的——这就是著名的 Static Initialization Order Fiasco (SIOF)。如果B的构造函数依赖A已经构造好,就会踩坑。
解决方案是 Construct On First Use 惯用法(用函数内的static变量):
A& getGlobalA() {
static A instance;
return instance;
}
三、释放:不是简单的"main 之后"
更准确的说法是:
全局对象的析构函数在
main正常返回之后,或调用std::exit()时执行。
注意三种不会调用析构函数的退出方式:
| 退出方式 | 是否调析构 |
|---|---|
| ✅ 会 |
| ✅ 会 |
| ❌ 不会 |
| ❌ 不会 |
| ❌ 不会(只调 |
未捕获异常逃出 main | ⚠️ 实现定义(通常是 abort,不调析构) |
析构顺序与构造顺序严格相反(LIFO)。
四、和atexit的交互
#include <cstdlib>
#include <iostream>
struct S {
S() { std::cout << "S ctor\n"; }
~S() { std::cout << "S dtor\n"; }
};
S s;
int main() {
std::atexit([]{ std::cout << "atexit\n"; });
return 0;
}
输出:
S ctor
atexit
S dtor
规则:atexit注册的函数和全局对象的析构按"逆构造顺序"统一编排——谁后注册/构造,谁先执行。所以atexit在 main 末尾才注册,但它先于s的析构执行。
五、写个完整的验证程序
#include <iostream>
#include <cstdlib>
struct Tracer {
const char* name;
Tracer(const char* n) : name(n) { std::cout << "ctor " << name << '\n'; }
~Tracer() { std::cout << "dtor " << name << '\n'; }
};
Tracer g1("g1");
Tracer g2("g2");
Tracer* heap = new Tracer("heap");
int main() {
std::cout << "--- enter main ---\n";
static Tracer s1("s1");
std::atexit([]{ std::cout << "atexit handler\n"; });
std::cout << "--- leave main ---\n";
return 0;
}
预期输出:
ctor g1
ctor g2
ctor heap
--- enter main ---
ctor s1
--- leave main ---
atexit handler
dtor s1
dtor g2
dtor g1
可以观察到:
g1→g2→heap在 main 前构造(堆对象的指针赋值算动态初始化)- 函数内
static变量s1是首次执行到时才构造(C++11 起线程安全) - 析构顺序严格逆序:
s1→g2→g1 heap的析构没出现——印证上一个问题的结论atexit在所有析构之前执行(因为它最后注册)
对于如下代码:
#include <iostream> class Widget { public: Widget(){ std::cout << "constructor" << ' '; } ~Widget(){ std::cout << "destructor" << ' '; } }; Widget *widget = new Widget; int main() { std::cout << "main" << ' '; return 0; }代码输出:
constructor main
核心原因:C++ 标准规定new创建的对象必须显式delete才会调用析构函数。程序退出时操作系统只是回收进程的物理内存(mmap/brk 释放),并不会逐个调用 C++ 对象的析构函数——这是典型的内存泄漏(虽然程序马上要退出了所以不会造成实际危害)。
所以输出就是:constructor main,缺少destructor。
如果我们在main函数中加入:
delete widget;
#include <iostream> class Widget { public: Widget(){ std::cout << "constructor" << ' '; } ~Widget(){ std::cout << "destructor" << ' '; } }; Widget *widget = new Widget; int main() { std::cout << "main" << ' '; delete widget; return 0; }则会输出:
constructor main destructor
