關於C/C++中全域性變數的初始化問題的深入思考
前言:
前日,在一次C++課程上,劉老師在舉例說明建構函式和解構函式的功能時,提到了全域性變數初始化時的建構函式的行為。建構函式在main函式之前初始化全域性變數。當然在C++下我是深信不疑的。但隨後老師聲稱C語言下的全域性變數也是如此,因為C沒有構造和解構函式,所以我們無法看到這一過程,在C++下可以在構造和解構函式中向螢幕列印資訊,進而可以觀察全域性變數的初始化和生存期。
這個觀點無疑使我心頭一震,作為C的痴迷者,長期以來在我頭腦中的印象是,全域性變數在編譯期就完成初始化了。難道我的觀念是錯誤的?!難道C真的也是在main函式之前,在程式執行初期才初始化?!
於是我翻看了《C語言參考手冊》這本書上沒有明確的答案,再翻看著名的K&R的《C程式設計語言》中只有括號裡面的一句話“在概念上.......”也是含糊其辭。(現在想想這個問題可能和編譯器有關,所以丹爺爺也沒說明太多)
在網上查詢了一下,關於這個問題,持什麼觀點的都有,沒有一個權威的答案。
只能靠自己了,動手實驗!
先給出我的結論:
C和C++中的一般全域性變數(不包括類class)是在編譯期確定的初始值,而不是在程式執行時,在main函式之前初始化的。
C++中的類的全域性變數是在程式執行時,在main函式之前初始化的。
預熱知識:
C或者C++語言,明面上的入口函式是main(argc,argv),或者tmain、wmain、WinMain等等。但實際上,是C Runtime的startup程式碼中的void mainCRTStartup(void)函式,呼叫了程式設計者寫的main函式。這個函式定義在VisualC++安裝目錄的crt\src\目錄下的某個.c檔案中(視VC++的版本不同,存放的檔案也不同)。它在執行一些初始化操作,如獲取命令列引數、獲取環境變數值、初始化全域性變數、初始化io的所需各項準備之後,呼叫main(argc,argv)。main函式返回後,mainCRTStartup還需要呼叫全域性變數的解構函式或者atexit()所登記的一些函式。往深裡說,是在連結生成可執行檔案時,告訴連結器這個可執行檔案的entry就是mainCRTStartup。當然,我們也可以對編譯器進行設定,使其不插入mainCRTStartup函式程式碼
以VC++6.0為例設定:Project->Settings->Link 在Category中選擇Output,在Entry-point symbol中填上main 即可。
-------------------------
實驗一:
1, C語言環境下:
實驗準備:
int a ;
int main(void)
{
return a+3 ;
}
在編譯器中設定入口函式為main(具體方法見上面)
這樣,我們讓編譯器生成的程式,直接從main函式中進入,而不是先執行mainCRTStartup函式做一些準備工作。
結果預測:
這樣,如果函式返回的是3,則說明此全域性變數是在編譯期就被初始化為0了,如果函式返回的是其它數字,則說明此全域性變數是在程式執行時,main函式執行前進行的初始化。
實驗結果:
進入控制檯(執行cmd命令),執行編譯後的程式(因為程式沒有向螢幕輸出結果,我們看不到任何現象),繼續輸入命令:echo %ERRORLEVEL% 則顯示3,此即為函式的返回值。
(echo是顯示其後的值,系統把前面執行的程式的返回值放在%ERRORLEVEL%中,故我們可以通過此方法獲得主函式的返回值)
同理:對於結構體全域性變數
struct A
{
int a ;
} sTest;
int main(void)
{
return sTest.a+3 ;
}
函式也返回3.
實驗結論:
在C語言中,全域性變數是在編譯期完成初始化的。
(在本實驗中我們沒有使用I/O函式把結果打印出來,因為I/O函式的呼叫之前必須要初始化記憶體中的某堆空間,而這個工作是由main函式之前的mainCRTStartup函式來做的。而我們設定讓編譯器跳過這個函式,故會在執行時出錯。)
實驗二:
C++語言環境下
實驗準備:
class A
{
public:
int a ;
A(){a=10;}
~A(){}
} ;
A cTest ;
int main(void)
{
return cTest.a ;
}
結果預測:
這樣,如果函式返回的是0,則說明此全域性變數是在編譯期就被初始化為0了,如果函式返回的是其它數字,則說明此全域性變數是在程式執行時,main函式執行前進行的初始化。
實驗結果:
在編譯器中設定入口函式為main,主函式返回一個其他值
在編譯器中設定入口函式為預設,主函式返回值為10
實驗結論:
在C++中,類(class)的全域性變數是在程式執行期,main函式開始之前,呼叫類的建構函式完成初始化的。
同理:
把C中的程式碼放到C++下實驗
int a ;
int main(void)
{
return a+3 ;
}
結果與C的結果相同。
說明:在C++中一般全域性變數的初始化(類除外),是在編譯期完成的,而不是在執行期完成。(與C語言規則相同)
mainCRTStartup函式不管一般全域性變數的初始化,它管理類(class)的全域性變數的初始化,呼叫類的解構函式。
編譯器會在編譯時,初始化一般全域性變數為0.
另:具有全域性生命期的區域性靜態變數的初始化,與區域性變數相同都是在執行時,執行到該初始化語句完成初始化的,只是區域性靜態變數只初始化一次。
後記:
1、程式不是從主函式開始執行的,而是先要執行一些啟動程式碼。(現在明白為什麼要在在嵌入式軟體程式設計時要在工程中新增類似於75x_init.s和75x_vect.s這兩個彙編檔案了吧)
2、你應該給主函式以返回值。實際上標準C只規定了兩種形式的main函式:
int main( void ) 和 int main(int argc, char *argv[])
main返回0,告訴系統程式正常終止,返回非零值告訴系統程式異常關閉.
其作用:我們可以利用程式的返回值,控制要不要執行下一個程式。
例:程式名&&DOS命令
前面的程式正常執行後才執行後面的DOS命令。當然我們也可以用其它的邏輯符把程式和命令組織起來,來實現複雜的功能。
(UNIX中的shell命令也有類似功能)