1. 启动过程概述
当一个 C++ 程序启动时,操作系统负责加载可执行文件并将控制权移交给程序。具体的启动过程大致如下:
- 操作系统加载可执行文件:操作系统将可执行文件加载到内存,并设置程序的堆栈指针等。
- 调用启动例程(Startup Code):在调用
main
函数之前,程序会首先执行启动代码,这个启动代码由编译器或标准库提供,通常称为_start
函数。它负责为程序的执行做准备。 - 初始化全局/静态变量:C++ 中,全局变量和静态变量的初始化在
main
函数之前进行。编译器生成的启动代码会负责这些变量的初始化。对于构造函数,需要先调用构造函数初始化对象。 - 调用
main
函数:完成所有初始化工作后,启动代码会调用用户定义的main
函数。 - 程序执行
main
函数:程序从main
函数开始执行用户定义的逻辑。 - 程序退出:当
main
函数结束后,控制权返回给启动代码,启动代码负责执行清理工作,并向操作系统返回退出状态码。
2. 启动例程 (_start
)
_start
函数是由系统库(例如 libc
或 libstdc++
)提供的,用来为用户的 main
函数做初始化工作。它的作用包括:
- 设置程序的堆栈。
- 初始化全局变量和静态变量。
- 为动态库加载器提供支持。
- 调用 C++ 全局对象的构造函数(包括全局和静态对象)。
- 在完成所有初始化后,调用用户的
main
函数。
// 伪代码说明启动过程
void _start() {
// 初始化低级系统资源
system_initialization();
// 调用全局和静态对象的构造函数
initialize_global_objects();
// 调用用户定义的 main 函数
int result = main();
// 处理程序退出后的清理工作,并返回给操作系统
exit(result);
}
3. 全局/静态变量的初始化
C++ 中,全局和静态对象在进入 main
函数之前会被初始化,这也是 C++ 和 C 之间的一个重要区别。C++ 支持复杂的对象模型,因此在进入 main
函数之前,编译器会调用全局对象和静态对象的构造函数。这些对象的初始化顺序是:
- 已初始化的全局变量会被加载到内存并赋予其初始值。
- 未初始化的全局变量会被赋予默认值(通常是零值)。
- 全局和静态对象的构造函数会在
main
函数之前执行。
例如:
#include <iostream>
class MyClass {
public:
MyClass() {
std::cout << "MyClass Constructor" << std::endl;
}
};
MyClass globalObject; // 在 main 之前执行构造函数
int main() {
std::cout << "Inside main" << std::endl;
return 0;
}
在这段代码中,globalObject
的构造函数会在 main
之前执行。
4.main 函数的执行
当启动代码完成所有的准备工作之后,程序控制权交给 main
函数。main
函数的签名通常有两种形式:
int main() // 最常见的形式,表示不接收参数
{
// Your code here
return 0; // 返回给操作系统,表示程序成功结束
}
int main(int argc, char* argv[]) // 可接收命令行参数
{
// argc 表示参数个数
// argv 是参数数组
return 0;
}
5.程序退出后的清理
当 main
函数执行完毕后,控制权会返回给启动例程(如 _start
),然后会进行如下清理工作:
- 调用全局和静态对象的析构函数:如果程序中有全局或静态对象,其析构函数会被调用,以便进行资源释放。
- 执行
atexit
注册的函数:如果程序使用了atexit
注册退出时需要执行的函数,它们也会被依次执行。 - 返回退出状态:启动代码会将
main
函数的返回值作为程序的退出状态码返回给操作系统。
例如:
#include <iostream>
class MyClass {
public:
~MyClass() {
std::cout << "MyClass Destructor" << std::endl;
}
};
MyClass globalObject; // 在程序结束后调用析构函数
int main() {
std::cout << "Inside main" << std::endl;
return 0;
}
在这段代码中,程序退出时,globalObject
的析构函数会被自动调用。
6. 与编译器和链接器相关的细节
- 编译器在编译过程中会生成初始化和清理代码,用来处理全局/静态对象的构造和析构。
- 链接器将这些初始化和清理代码与用户定义的
main
函数链接在一起,确保程序在执行时能正确调用这些例程。