我之所以提出這個問題,緣於一些IT公司招聘開發人員的筆試題或者面試題:C++ 中能不能讓一些代碼在 main() 之前或者之後執行?
答案理所當然可以的。
這可以有很多實現方法。下面例舉:
1、一般來說,全局域的變量(包括靜態變量)賦值、初始化等工作都是在main之前執行的。此時初始化變量調用的普通賦值函數,初始化對象調用的類的構造函數,都是行之有效的方法。
比較典型的是靜態變量通過靜態函數賦值、靜態對象的初始化,這不僅對於C++,對於其他編程語言,如Java或C#也同樣適用。
如main()函數之前:
example 1.1 one of C++ samples
class Dummy {
public:
Dummy() { run_before_main(); }
};
Dummy dummy;
int main() {
...
}
example 1.2: one of Java samples
public class Test {
private static int dummy = foo();
private static int foo() {
System.out.println("This is executed first.");
return 0;
}
public static void main(String[] args) {
System.out.println("This is executed next.");
}
}
同理,利用全局域的變量(包括靜態變量)的析構可以在main() 之後執行代碼。
example 1.3 one of C++ samples
class A
{
public:
A() {
}
~A() {
printf("This is executed next./n");
}
};
A a;
int main(void) {
printf("This is executed first./n");
}
example 1.3 one integrated of C++ samples
int main(int argc) {
printf("This is executed with %d./n",argc);
return argc;
}
int c = main(521);
class A
{
public:
A() {
printf("This is A's constructor./n");
}
~A() {
//extern c;
//printf("%d This is executed next./n",c);
printf("This is executed next./n");
}
};
Output:
This is executed with 521.
This is A's constructor.
This is executed with 1.
This is executed next.
如果把int c = main(521);這一行放到最後,輸出則會變爲:
This is A's constructor.
This is executed with 521.
This is executed with 1.
This is executed next.
下面再給一段代碼,大家想一想執行以後會輸出什麼?
example 1.4 in C++
int main(int argc) {
printf("This is executed with %d./n",argc);
return argc;
}
class A
{
public:
A() {
extern c;
printf("This is A's constructor with %d./n", c);
}
~A() {
printf("This is executed next./n");
}
};
A a;
int c = main(521);
Output:
This is A's constructor with 0.
This is executed with 521.
This is executed with 1.
This is executed next.
可以看到extern關鍵字對變量的初始化時機沒有任何影響;同時,全局int如果不賦值其值爲0(double同樣)。
java也是可以在main()之前調用main()函數的:
example 1.5 in Java
public class Test {
private static int dummy = foo();
private static int foo() {
main((new String[1]));
System.out.println("This is executed first.");
return 0;
}
public static void main(String[] args) {
System.out.println("This is executed next. "+args.length);
}
}
這充分說明了同C++一樣,Java的main()也只不過是呈現給程序員的表面的符號而已。
2、調用C的庫函數。
main()之前執行:
example 2.1
在GCC中可以這樣
#include <stdio.h>
#include <string.h>
void first() __attribute__ ((constructor));
int main() {
printf("This function is %s ", __FUNCTION__);
return 0;
}
void first() {
printf("This %s is before main ", __FUNCTION__);
}
main() 之後執行:CRT會執行另一些代碼,進行處理工作。使用atexit()或_onexit()函數,註冊一個函數。
example 2.2
#include <stdlib.h>
int atexit(void(*function)(void));
#include <stdlib.h>
#include <stdio.h>
void fn1(void),fn2(void),fn3(void),fn4(void);
int main(void){
atexit(fn1);
atexit(fn2);
atexit(fn3);
atexit(fn4);
printf("This is executed first./n");
}
void fn1(){
printf("next./n");
}
void fn2(){
printf("executed ");
}
void fn3(){
printf("is ");
}
void fn4(){
printf("This ");
}
3、修改定義main入口的文件。main入口其實是由編譯器提供的一個庫文件定義的,並不是固化在編譯器內核的。因此如果需要的話,可以隨意更改。當然我們並不建議這樣。
在 windows 下看 VC的源代碼裏有 crt0.c 這個源文件,這個就是定義main入口的文件,如果你願意可以在裏面加任何語句,然後重新編譯。在VC裏大概是這個樣子:
void __cdecl __crt0 (
)
{
int mainret;
char szPgmName[32];
char *pArg;
char *argv[2];
#ifndef _M_MPPC
void *pv;
/* This is the magic stuff that MPW tools do to get info from MPW*/
pv = (void *)*(int *)0x316;
if (pv != NULL && !((int)pv & 1) && *(int *)pv == 'MPGM') {
pv = (void *)*++(int *)pv;
if (pv != NULL && *(short *)pv == 'SH') {
_pMPWBlock = (MPWBLOCK *)pv;
}
}
#endif /* _M_MPPC */
_environ = NULL;
if (_pMPWBlock == NULL) {
__argc = 1;
memcpy(szPgmName, (char *)0x910, sizeof(szPgmName));
pArg = _p2cstr_internal(szPgmName);
argv[0] = pArg;
argv[1] = NULL;
__argv = argv;
#ifndef _M_MPPC
_shellStack = 0; /* force ExitToShell */
#endif /* _M_MPPC */
}
#ifndef _M_MPPC
else {
_shellStack = _GetShellStack(); //return current a6, or first a6
_shellStack += 4; //a6 + 4 is the stack pointer we want
__argc = _pMPWBlock->argc;
__argv = _pMPWBlock->argv;
Inherit(); /* Inherit file handles - env is set up by _envinit if needed */
}
#endif /* _M_MPPC */
/*
* call run time initializer
*/
__cinit();
mainret = main(__argc, __argv, _environ);
exit(mainret);
}
注意:每個編輯器的實現是不一樣的。
4、利用多線程。這對於目前大多編程語言都適用。
example 稍候。
總結:
其實main 是在
mainCRTStartup中被調用的
在main之前會調用一系列初始化函數來初始化這個進程
而在main之後會調用exit(int)來進行進程的清理工作
#ifdef WPRFLAG
__winitenv = _wenviron;
mainret = wmain(__argc, __wargv, _wenviron);
#else /* WPRFLAG */
__initenv = _environ;
mainret = main(__argc, __argv, _environ);
#endif /* WPRFLAG */
#endif /* _WINMAIN_ */
exit(mainret);
樓上是說onexit是在main()之前, 來看看代碼便知
exit的代碼
void __cdecl exit (
int code
)
{
doexit(code, 0, 0); /* full term, kill process */
}
doexit的代碼
static void __cdecl doexit (
int code,
int quick,
int retcaller
)
{
#ifdef _DEBUG
static int fExit = 0;
#endif /* _DEBUG */
#ifdef _MT
_lockexit(); /* assure only 1 thread in exit path */
#endif /* _MT */
if (_C_Exit_Done == TRUE) /* if doexit() is being called recursively */
TerminateProcess(GetCurrentProcess(),code); /* terminate with extreme prejudice */
_C_Termination_Done = TRUE;
/* save callable exit flag (for use by terminators) */
_exitflag = (char) retcaller; /* 0 = term, !0 = callable exit */
if (!quick) {
/*
* do _onexit/atexit() terminators
* (if there are any)
*
* These terminators MUST be executed in reverse order (LIFO)!
*
* NOTE:
* This code assumes that __onexitbegin points
* to the first valid onexit() entry and that
* __onexitend points past the last valid entry.
* If __onexitbegin == __onexitend, the table
* is empty and there are no routines to call.
*/
if (__onexitbegin) {
_PVFV * pfend = __onexitend;
while ( --pfend >= __onexitbegin )
/*
* if current table entry is non-NULL,
* call thru it.
*/
if ( *pfend != NULL )
(**pfend)(); // 在這裏循環調用onexit
}
/*
* do pre-terminators
*/
_initterm(__xp_a, __xp_z);
}
/*
* do terminators
*/
_initterm(__xt_a, __xt_z);
#ifndef CRTDLL
#ifdef _DEBUG
/* Dump all memory leaks */
if (!fExit && _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) & _CRTDBG_LEAK_CHECK_DF)
{
fExit = 1;
_CrtDumpMemoryLeaks();
}
#endif /* _DEBUG */
#endif /* CRTDLL */
/* return to OS or to caller */
if (retcaller) {
#ifdef _MT
_unlockexit(); /* unlock the exit code path */
#endif /* _MT */
return;
}
_C_Exit_Done = TRUE;
ExitProcess(code);
}
區別一下,系統在main前和main後,是爲我們做了很多工作的,單單一個空的main,如果你用匯編級調試器去調試之後,發現起點不是main,而main只是其中一個空函數而已。
main的結束不等於整個程序的結束,也不等於C生命期的結束……
ITOM中有很清楚的闡述,關於全局數據區中創建的對象是如何銷燬,以及用怎樣的順序銷燬的。
如果願意,可以跟蹤一個全局對象的創建和銷燬過程,你從那個函數中返回出來的時候,都不是正常的main途徑了,做到這點很簡單,在你的構造函數和析構函數中加上如下代碼就可以了,其實也就是一個設置斷點異常的過程,
__asm int 3
然後用trap step,可以跟蹤出函數,發現其實進入了crt0.h中(MS的編譯器是如此)
然後跟着可以發現main函數。
調用main前和調用main分別有一個初始化全局和銷燬全局部分……
跟蹤的時候,可以跟到那個地方。
最後的結束,其實最終都需要執行系統API ExitProcess,這樣,整個控制檯生命纔算進入僵死狀態。
然後等待系統回收。不過這個過程不在代碼中而已。