多玩YY語音的面試題:C++中如何在main()函數之前執行操作?
第一反應main()函數是所有函數執行的開始。但是問題是main()函數執行之前如何執行呢?
聯想到MFC裏面的 C**App類的theApp對象,其執行順序就在main函數之前。道理相通,順理推下,能夠想到:如果在main函數之前聲明一個類的全局的對象。那麼其執行順序,根據全局對象的生存期和作用域,肯定先於main函數。
示例如下:
- <span style="font-size:14px;">class simpleClass
- {
- public:
- simpleClass( )
- {
- cout << "simpleClass constructor.." << endl; //step2
- }
- };
- simpleClass g_objectSimple; //step1全局對象
- int _tmain(int argc, _TCHAR* argv[]) //step3
- {
- return 0;
- }
- 可單步調試查看執行順序爲step1、step2、step3。</span>
考慮到全局對象,同理會進一步思考靜態對象的作用域。將上述示例進一步擴展如下:
- <span style="font-size:14px;">class simpleClass
- {
- public:
- simpleClass( )
- {
- cout << "simpleClass constructor.." << endl; //step2
- }
- };
- class simpleClassTwo
- {
- public:
- static simpleClass m_sSimpleClass;
- };
- simpleClass simpleClassTwo::m_sSimpleClass = simpleClass(); //step1 靜態對象
- int _tmain(int argc, _TCHAR* argv[]) //step3
- {
- return 0;
- }
- 可單步調試查看執行順序爲step1、step2、step3。</span>
至此,我們可以總結出:定義在main( )函數之前的全局對象、靜態對象的構造函數在main( )函數之前執行。
再進一步思考,既然可以在main( )函數之前執行全局、靜態對象的構造函數。那麼有沒有函數在main( )函數之後執行呢?
有的,onexit函數。原型如下:
_onexit_t _onexit(
_onexit_t function
);
_onexit_t_m _onexit_m(
_onexit_t_m function
);
解釋:The _onexit function is passed the address of a function (function) to be called when the program terminates normally. Successive calls to _onexit create a register of functions that are executed in LIFO (last-in-first-out) order. The functions passed to _onexit cannot take parameters.
核心點:
1) 執行期——程序執行終止的時候;
2) 傳遞參數——函數的地址,即函數指針;
3) 執行順序——後進先出。
_onexit is a Microsoft extension. For ANSI portability, use atexit. The _onexit_m version of the function is for mixed mode use.
onexit是微軟的擴展版本,標準C++裏面應用的是atexit。
【MSDN】示例:
- <span style="font-size:14px;">#include <stdlib.h>
- #include <stdio.h>
- /* Prototypes */
- int fn1(void), fn2(void), fn3(void), fn4 (void);
- int main( void )
- {
- _onexit( fn1 );
- _onexit( fn2 );
- _onexit( fn3 );
- _onexit( fn4 );
- printf( "This is executed first.\n" );
- }
- int fn1()
- {
- printf( "next.\n" );
- return 0;
- }
- int fn2()
- {
- printf( "executed " );
- return 0;
- }
- int fn3()
- {
- printf( "is " );
- return 0;
- }
- int fn4()
- {
- printf( "This " );
- return 0;
- }
- </span>
執行結果如下:
顯然,讀程序可以看出main( )函數執行完畢後又執行了onexit( )函數。
還有沒有其他特殊的情況呢?持續探討、更新中……
2013-4-23更新
補充:控制檯界面應用程序是如何啓動的?
以Windows平臺爲例,用MicrosoftVisual Studio 來創建一個應用程序項目時,集成開發環境會設置各種連接開關,使鏈接器將子系統的正確類型嵌入最終生成的可執行文件(executable)中。對於控制檯界面應用程序,這個鏈接器的開關是/SUBSYSTEM:CONSOLE。
用戶啓動應用程序時,操作系統的加載程序(loader)會檢查可執行文件映像的文件頭,並獲取這個子系統值。如果如下圖所示,子系統值爲/SUBSYSTEM:CONSOLE,加載程序會自動確保命令符啓動程序的時候有一個可用的文本控制檯窗口。另外,如有必要,如從資源管理器啓動CUI程序的時候,會創建一個新窗口。
在連接可執行文件時,鏈接器將選擇正確的C/C++運行庫啓動函數。如果指定了/SUBSYSTEM:CONSOLE,會執行如下步驟:
所有C/C++運行庫啓動函數所做的事情基本都是一樣的,區別1在於它們處理字符串的類型(ANSI字符串或者是Unicode字符串),區別2在於它們調用的是哪個入口點函數。
Visual C++自帶C++運行庫的源代碼啓動函數的用途簡單概括如下:
1) 獲取新進程完整命令行的一個指針;
2) 獲取指向新進程的環境變量的一個指針;
3) 初始化C/C++運行庫的全局變量。
4) 初始化C運行庫內存分配函數(malloc和calloc)和其他底層的I/O例程使用的堆。
5) 調用所有全局和靜態C++類對象的構造函數。
第5條也就解釋了爲什麼在main()函數前運行全局、靜態變量的構造函數了。
入口點函數返回後,啓動函數將調用C運行庫函數exit,向其傳遞返回值(nMainRetVal)。exit函數執行以下任務:
1) 調用_onexit函數所有調用所註冊的任何一個函數;
2) 調用所有全局和靜態C++類對象的析構函數;
3) 在DEBUG生成中,如果設置了_CRTDBG_LEAK_CHECK_DF標誌,就通過調用_CrtDumpMemoryLeaks函數生成內存泄露報告。
4) 調用操作系統的ExitProcess函數,向其傳入nMainRetVal。這會導致操作系統殺死我們的進程,並設置它的退出代碼。
exit( )函數的執行的先後順序爲:1)、2)、3)、4)。
如下上述程序的合體,驗證了exit函數的執行順序。
先全局對象構造函數,然後執行main函數打印語句,再執行_onexit註冊函數;最後執行全局對象析構函數。
- #include <stdlib.h>
- #include <stdio.h>
- class simpleClass
- {
- public:
- simpleClass()
- {
- cout<< "simpleClass constructor.." << endl;
- }
- ~simpleClass()
- {
- cout<< "~SimpleClass Destructor.." << endl;
- }
- };
- simpleClass g_objectSimple; //1全局對象
- /* Prototypes */
- int fn1(void), fn2(void), fn3(void), fn4(void);
- int main( void )
- {
- _onexit(fn1 );
- _onexit(fn2 );
- _onexit(fn3 );
- _onexit(fn4 );
- printf("This is executed first.\n" );
- }
- int fn1()
- {
- printf("next.\n" );
- return0;
- }
- int fn2()
- {
- printf("executed " );
- return0;
- }
- int fn3()
- {
- printf("is " );
- return0;
- }
- int fn4()
- {
- printf("This " );
- return0;
- }