我之所以提出这个问题,缘于一些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,这样,整个控制台生命才算进入僵死状态。 然后等待系统回收。不过这个过程不在代码中而已。
|