1、在介紹靜態物件、全域性物件與程式的執行機制之間的關係之前,我們首先看一下atexit函式。
atexit函式的宣告為:int atexit( void ( __cdecl *func )( void ) );
引數為函式指標,返回值為整型,0表示成功,其他表示失敗。當程式執行結束時,他呼叫atexit函式註冊的所有函式。如果多次呼叫atexit函式,那麼系統將以lifo(last-in-first-out)的方式呼叫所有的註冊函式。
舉例如下(**摘自msdn):
#include
#include
void fn1( void ), fn2( void ), fn3( void ), fn4( void );
void main( void )
void fn1()
void fn2()
void fn3()
void fn4()
編譯、執行程式後,程式的輸出為:
this is executed first.
this is executed next.
註冊函式的順序為:fn1、fn2、fn3、fn4,但是呼叫順序為fn4、fn3、fn2、fn1。
2、
理解了atexit函式之後,我們就可以來看看區域性靜態物件了。
class aaa ;
aaa* createaaa()
在除錯狀態下,彙編**如下(請觀察藍色標記出來的**):
aaa* createaaa()
… 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 = ;
#pragma data_seg(".crt$xiz")
_crtalloc(".crt$xiz") _pvfv __xi_z = ;/* c initializers */
#pragma data_seg(".crt$xca")
_crtalloc(".crt$xca") _pvfv __xc_a = ;
#pragma data_seg(".crt$xcz")
_crtalloc(".crt$xcz") _pvfv __xc_z = ;/* 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
來析構全域性物件或靜態物件。
C 之全域性物件,區域性物件,靜態區域性物件
先說兩個概念 作用域 scope 和生命週期 lifetime 作用域 名字的作用域指的是知道該名字的程式文字區域 生命週期 物件的生命週期指在程式執行過程中物件存在的時間 全域性物件,顧名思義是全域性的物件,其作用域是整個程式文字,其物件的宣告週期是整個程式的執行過程 區域性物件 一般說的區域性變...
全域性物件和全域性靜態 區域性靜態析構順序
多久不用c 好多東西都模糊了,最近跟人討論全域性和靜態的析構順序,之前總覺得這個根據編譯器來決定的,其實還是有一定的說法的。記錄一下 class a destructor delete a class b destructor 區域性變數 class d destructor 區域性靜態變數 cla...
區域性物件與全域性物件的構造和析構
例程除錯,很多細節問題。可以單步除錯後,切入到反彙編進行觀察。include pch.h include include include include using namespace std namespace nmsp1 命名空間 a int m i void func a obja 現用現定義...