Windows支持两种类型的应用程序:GUI和CUI程序。前者是Graphical User Interface的简称,后者是Console User Interface的简称。
用VS创建一个项目的时候,IDE会设置各种链接器开关,使链接器将子系统(注1)的正确类型嵌入最终生成的可执行文件中。对于CUI程序,这个链接器开关是/SUBSYSTEM:CONSOLE,对于GUI程序,则是/SUBSYSTEM:WINDOWS。用户运行应用程序时,操作系统的加载程序会检查可执行文件映像的文件头,并获取这个子系统值。如果此值表明是一个CUI程序,加载程序会确保有一个可用的文本控制台窗口。如果是GUI程序,加载器就不会创建控制台窗口;相反,它只是加载这个程序,一定应用程序开始运行,操作系统就不再关心程序的界面是什么了。
Windows应用程序必须有一个入口函数,应用程序开始运行时,这个函数会被调用。C/C++开发人员可以使用以下两种入口点函数。
int WINAPI _tWinMain(
HINSTANCE hInstanceExe,
HINSTANCE,
PTSTR pszCmdLine,
int nCmdShow);
int _tmain(
int argc,
TCHAR *argv[],
TCHAR *envp[]);
注意,具体的符号取决于我们是否要使用Unicode字符串。操作系统实际并不调用我们所写的入口点函数,相反,它会调用由C/C++运行库实现并在连接时使用-entry:命令行选项来设置一个C/C++运行时启动函数。该函数将初始化C/C++运行库,使我们能调用malloc和free之类的函数。它还确保了在我们的代码开始执行之前,我们声明的任何全局和静态C++对象都被正确地构造。下表总结了源代码要实现什么入口点函数,以及每个入口点函数应该在什么时候使用。
应用程序类型 |
入口点函数 |
嵌入可执行文件的启动函数 |
处理ANSI字符和字符串的GUI应用程序 |
_tWinMain(WinMain) |
WinMainCRTStartup |
处理unicode字符和字符串的GUI程序 |
_tWinMain(wWinMain) |
wWinMainCREStartup |
处理ANSI字符和字符串的CUI程序 |
_tmain(Main) |
mainCRTStartup |
处理unicode字符和字符串的CUI程序 |
_tmain(Wmain) |
wmainCRTStartup |
在链接可执行文件时,链接器将选择正确的C/C++运行库启动函数,如果指定了/SUBSYSTEM:WINDOWS链接器开关,链接器就会寻找WinMain或wWinMain函数,如果这两个函数都没有找到,链接就会返回一个“unresolved external sumbol”错误,否则它将根据具体情况分别选择WinMainCRTStartup或者wWinMainCRTStartup。
类似的,如果指定了/SUBSYSTEM:CONSOLE链接器也遵循同样的规则。
一个鲜为人知的事实是,我们完全可以从自己的项目中移除/SUBSYSTEM链接器开关,一旦这样做,链接器就会自动判断应该为应用程序设置为哪个子系统。
所有C/C++运行库启动函数所做的事情基本都是一样的,区别在于他们要处理的是ANSI还是Unicode字符串;以及在初始化C运行库之后,他们调用的是哪一个入口函数。Visual C++自带了C运行库源码。可以在crtexe.c文件中找到4个启动函数的源码,这些启动函数的用途简单总结如下:
l 获取指向新进程的完整命令行的一个指针。
l 获取执行新进程的环境变量的一个指针。
l 初始化C/C++运行库的全局变量。如果包含了stdlib.h,我们的代码可以访问这些变量。下表总结了这些变量。
l 初始化C运行库的内存分配函数(malloc和calloc)和其他底层I/O例程使用的堆。
l 调用所有全局和静态C++类对象的构造函数。
变量名称 |
类型 |
描述和推荐使用的windows函数 |
_osver |
unsigned int |
操作系统的构建版本号。例如:windows vista RTM为build 6000,所以_osver的值就是6000。请换用GetVersionEx |
_winmajor |
unsigned int |
以16进制表示的windows系统的主版本号,对于vista为6。请换用GetVersionEx |
_winminor |
unsigned int |
以16进制表示的windows系统的次版本号,对于vista为0。请换用GetVersionEx |
_winver |
unsigned int |
(_winmajor<<8)+_winminor。请换用GetVersionEx |
_argc |
unsigned int |
命令行上传递的参数个数。请换用GetCommandLine |
_argv _wargv |
char wchar_t |
长度为_argc的一个数组,每一项指向一个命令行参数。请换用GetCommandLine |
_environ _wenviron |
char wchar_t |
一个指针数组,数组中的每一项指向一个环境字符串。请换用GetEnvironmentStrings或GetEnvironmentVariable |
_pgmptr _wpgmptr |
char wchar_t |
正在运行的程序的名称及其ANSI/Unicode完整路径。请换用GetModuleFileName,将NULL作为第一个参数传进去。 |
完成这些初始化工作以后,C/C++才会调用我们的入口函数。
入口函数返回后,启动函数将调用C运行库的exit函数,向其传递入口函数的返回值。Exit函数将执行以下操作:
l 调用_onexit函数调用所注册的任何一个函数。
l 调用所有全局和静态C++类对象的析构函数。
l 在DEBUG生成中,如果设置了_CRTDBG_LEAK_CHECK_DF标志,就通过调用_CrtDumpMemoryLeaks函数生成内存泄漏报告。
l 调用操作系统的ExitProcess函数,向其传递入口函数的返回值。这会导致操作系统杀死我们的进程,并设置它的退出代码。
注意,为了安全起见,Microsoft不赞成使用所有这些变量,因为使用了这些变量的代码可能会在C运行库初始化这些变量之前运行。
注1: Windows最初曾有三个环境子系统:POSIX、OS/2、Windows,但是伴随着Windows2000的巨大成功,WIndows不再将posix随2000以后的系统一起发行了。(具体可参见潘爱民的深入理解windows)