深度剖析C++全局构造函数和析构函数的调用机制

来源:互联网 发布:java web实时应用程序 编辑:程序博客网 时间:2024/06/06 18:15
C++全局构造函数和析构函数的调用机制
控制台EXE中C++的全局变量在main之前初始化,在main之后清除,VC编译器、链接器和VC运行库代码互相配合完成了这个魔术。请复制这段代码到你新建的控制台程序,创建并运行:
#include "stdafx.h"
#include <iostream>

#define SECNAME ".CRT$XCU"
#pragma section (SECNAME, long, read)

void Cleanup()
{
std::cout << "bye" << std::endl;
}

void Init()
{
//do anything you want.
std::cout << "hello" << std::endl;
atexit(Cleanup);//注册析构函数
}

typedef void (_cdecl *_PVFV) () ;
_declspec (allocate(SECNAME)) _PVFV callB4Main[] = { Init };

int main()
{
std::cout << "in main" << std::endl;
return 0;
}

输出结果:
hello
in main
bye

奇怪!你并没有调用Init和Cleanup,但很明显它们被调用了,这是怎么回事呢?
我们知道真正的入口函数是mainCRTStartup,对于我们的讨论,mainCRTStartup可简化如下:
int __tmainCRTStartup()//这个函数会被链接进我们的exe,并作为入口点函数
{
_initterm();
int result = main();
exit();
return result;
}

//  file:   crt\src\crt0dat.c
static void _cdecl _initterm  (_PVFV * pfbegin,_PVFV * pfend)
{
while   ( pfbegin < pfend )
{
if   (   *pfbegin  != NULL )
(* *pfbegin) ();
++pfbegin;
}
}
这就是调用构造函数的地方。对于我们定义的每一个全局变量,编译器把它的构造函数调用(不管你带多少参数)编译成一段代码(签名格式为_PVFV),这段代码同时用atexit注册了析构函数(对于编译器这是很容易的)。然后把这段代码的函数指针放入一个_PVFV数组。链接器把这些_PVFV数组连成一个大数组。
_initterm参数中的pfbegin就指向这个数组的第一个元素,pfend指向这个数组的最后一个元素(总为NULL),我们的构造函数就这样被调用了,这也是上面的魔术的工作原理。atexit注册的函数将在main之后exit(与_initterm代码相似)中执行,而且遵循LIFO的原则,所以析构函数会按照和构造函数相反的顺序调用。

对于非控制台EXE和DLL,情形是类似的。

任何用户态进程的真正的入口其实是kernel32.dll!_BaseProcessStart@4,但这与我们的讨论无关。

参考书籍:
《程序员的自我修养--装载、链接和库》
原创粉丝点击