靜態物件 全域性物件與程式的執行機制

2021-04-12 13:14:17 字數 4148 閱讀 8052

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 現用現定義...