1、 在介紹靜態對象、全局對象與程序的運行機制之間的關系之前,我們首先看一下atexit函數。
atexit函數的聲明為:int atexit( void ( __cdecl *func )( void ) );
參數為函數指針,返回值為整型,0表示成功,其他表示失敗。當程序運行結束時,他調用atexit函數注冊的所有函數。如果多次調用atexit函數,那么系統將以LIFO(last-in-first-out)的方式調用所有的注冊函數。
舉例如下(代碼摘自MSDN):
1
#include <stdlib.h>
2
#include <stdio.h>
3
4
void fn1( void ), fn2( void ), fn3( void ), fn4( void );
5
6
void main( void )
7

{
8
atexit( fn1 );
9
atexit( fn2 );
10
atexit( fn3 );
11
atexit( fn4 );
12
printf( "This is executed first.\n" );
13
}
14
15
void fn1()
16

{
17
printf( "next.\n" );
18
}
19
20
void fn2()
21

{
22
printf( "executed " );
23
}
24
25
void fn3()
26

{
27
printf( "is " );
28
}
29
30
void fn4()
31

{
32
printf( "This " );
33
}
34
編譯、運行程序后,程序的輸出為:
This is executed first.
This is executed next.
注冊函數的順序為:fn1、fn2、fn3、fn4,但是調用順序為fn4、fn3、fn2、fn1。
2、理解了atexit函數之后,我們就可以來看看局部靜態對象了。
1 class AAA{ … } ;
2 AAA* createAAA()
3 {
4 static AAA a ;
5 return &a ;
6 }
7
在調試狀態下,匯編代碼如下(請觀察藍色標記出來的代碼):
AAA* createAAA()
{
…
static AAA a ;
…
[1] 00401056 call AAA::AAA (4010A0h)
[2] 0040105B push offset `createAAA'::`2'::a::`dynamic atexit destructor' (442410h)
[3] 00401060 call atexit (409A50h)
00401065 add esp,4
00401068 mov dword ptr [ebp-4],0FFFFFFFFh
return &a ;
0040106F mov eax,offset a (452620h)
}
…
00401091 ret
注:[1]、[2]、[3]為方便說明加入的字符,實際代碼中并不存在。
[1]語句很明顯的調用AAA的構造函數。
[2]語句將442410h壓入棧中。
[3]語句調用atexit函數,根據我們的了解,atexit的參數應該是函數指針。那么我們來分析一下442410h處的代碼,從注釋來看,我們看到了destructor。代碼如下:
`createAAA'::`2'::a::`dynamic atexit destructor':
…
[1] 0044242E mov ecx,offset a (452620h)
[2] 00442433 call AAA::~AAA (403A90h)
…
0044244B ret
[1]語句將a的地址放在ecx寄存器中,這是this調用規范的風格。
[2]語句調用AAA的析構函數。
程序結束時,將調用atexit函數注冊的442410h處的函數,進而調用了AAA的析構函數。從而保證了析構函數的調用。
3、 了解了局部靜態對象之后,我們來看看全局對象。
我們知道全局對象必須在main函數前已經被構造。為了弄清楚全局對象何時被構造,我在全局對象的實例化處設置了斷點,調用堆棧如下:
static.exe!aaaa::`dynamic initializer'() Line 22 C++
static.exe!_initterm(void (void)* * pfbegin=0x00451038, void (void)* * pfend=0x00451064) Line 707 C
static.exe!_cinit(int initFloatingPrecision=1) Line 208 + 0xf bytes C
static.exe!mainCRTStartup() Line 266 + 0x7 bytes C
作為對比,我在AAA的析構函數出設置了斷點,調用堆棧如下:
static.exe!AAA::~AAA() Line 19 C++
static.exe!aaaa::`dynamic atexit destructor'() + 0x28 bytes C++
static.exe!doexit(int code=0, int quick=0, int retcaller=0) Line 451 C
static.exe!exit(int code=0) Line 311 + 0xd bytes C
static.exe!mainCRTStartup() Line 289 C
由此我們可以看出程序的實際入口點位mainCRTStartup而不是main函數(相對于ANSI的控制臺程序而言)。
我們來分析一下_cinit函數:
注釋中有一句[3. General C initializer routines],看來該函數的功能之一是完成C的初始化例程。
函數的核心代碼如下:
/*
* do initializations
*/
initret = _initterm_e( __xi_a, __xi_z );
/*
* do C++ initializations
*/
_initterm( __xc_a, __xc_z );
看來該函數主要進行C、C++的初始化。我們進一步分析函數_initterm_e和_initterm,兩個函數的功能進本相同,都是遍歷函數指針(由參數指定函數指針的開始位置[__xi_a、__xi_z]、結束位置[__xc_a、__xc_z]),如果函數指針不為null,那么調用該函數。
那么__xi_a、__xi_z和__xc_a、__xc_z到底代表了什么呢?在cinitexe.c文件中有如下代碼:
#pragma data_seg(".CRT$XIA")
_CRTALLOC(".CRT$XIA") _PVFV __xi_a[] = { NULL };
#pragma data_seg(".CRT$XIZ")
_CRTALLOC(".CRT$XIZ") _PVFV __xi_z[] = { NULL };/* C initializers */
#pragma data_seg(".CRT$XCA")
_CRTALLOC(".CRT$XCA") _PVFV __xc_a[] = { NULL };
#pragma data_seg(".CRT$XCZ")
_CRTALLOC(".CRT$XCZ") _PVFV __xc_z[] = { NULL };/* C++ initializers */
#pragma comment(linker, "/merge:.CRT=.data")
可以看出這四個變量分別在數據段.CRT$XIA、.CRT$XIZ、.CRT$XCA、.CRT$XCZ中。當連接器布局代碼時,它按根據的名稱,按照字母排序的規則,排列所有段。這樣在段.CRT$XIA中的變量出現在段.CRT$XIZ所有變量之前,從而形成鏈表。對于.CRT$XCA、.CRT$XCZ數據段同理。最后這四個數據段被合并到.data數據段中。
再看看這些變量的類型,typedef void (__cdecl *_PVFV)(void); 所以這些變量組成了2個初始化函數指針鏈表。
調試過程中,看到__xc_a、__xc_z鏈表中,指向的初始化函數很多是構造函數,如:
static std::_Init_locks initlocks;
static filebuf fout(_cpp_stdout);
extern _CRTDATA2 ostream cout(&fout);
cout對象也在此時被構造。
對于析構函數的調用也是采用相同的方式,只是此時每一種初始化,都有一種終止函數與之對應。
4、 總結
l 編譯、連接程序時,編譯器將所有全局對象的初始化函數放入.CRT$Xx中,連接器將所有的.CRT$XCx段合并成為.rdata數據段。在.CRT$XCA 到 .CRT$XCZ的所有段的數據組成初始化函數指針列表。
l 函數執行時,_initterm( __xc_a, __xc_z )函數調用所有的初始化函數。構造全局對象。構造對象完畢,調用atexit函數來保證析構函數的調用。Modern C++ Design就是通過控制調用atexit函數來決定對象的析構順序的。
l 對于靜態對象使用atexit來保證析構函數的調用。
l 程序結束時,調用exit來析構全局對象或靜態對象。
本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/coffeemay/archive/2006/11/19/1395250.aspx