编写第1个Windows应用程序
Windows支持两种类型的应用程序,分别基于控制台用户界面(Console User Interface-CUI)和图形用户界面(Graphical User Interface-GUI)。GUI应用程序拥有一个图形化的前端,它可以创建窗口、菜单,以对话框和用户交互,并使用所有的标准窗口组件。Windows附带的大多数软件,如记事本、计算器和写字板等,都是基于GUI的。基于CUI的应用通常不创建窗口或是处理消息,它们也不需要图形用户界面。虽然CUI程序运行时也会出现一个窗口,但其中只包含文本。Windows附带的cmd.exe便是典型的CUI程序。
这两种程序的界限非常模糊。你可以创建一个能够显示对话框的CUI程序,这样就可以使用对话框选择应用的命令行参数,而不是强迫用户记住命令。GUI程序也可以向控制台输出文本,我经常使用这种方法将GUI程序的调试信息写入控制台。当然我们推荐你在开发时将选择界面更为友好的GUI。
使用Visual Studio建立应用程序时,它会自动打开不同的链接器开关(linker switches),以将合适的子系统嵌入最后的可执行文件中。CUI应用的链接器开关是/SUBSYSTEM:CONSOLE,GUI则是/SUBSYSTEM:WINDOWS。当用户启动程序时,操作系统查看可执行文件的映像头,如果其中的子系统开关表明该应用是CUI程序,系统自动打开一个控制台供程序使用,如果开关指明应用基于GUI的,系统只是加载程序而不会创建控制台。一旦程序开始运行,系统将不再关心应用的用户界面(User Interface - UI)类型。
Windows应用程序必须有一个入口点函数在程序开始运行时调用,C/C++语言的入口点函数根据应用子系统类型的不同,有_tWinMain和_tmain两个,前者是GUI应用的入口点,后者是CUI程序的入口点:
/* entry-point function of GUI based application */ int WINAPI _tWinMain(HINSTANCE hInstanceExe, HINSTANCE, PTSTR pszCmdLine, int nCmdShow); /* entry-point functin of CUI based application */ int _tmain(int argc, TCHAR *argv[], TCHAR *envp[]);
程序是否使用Unicode字符串决定了_tWinMain和_tmain展开后的目标函数。操作系统并不会直接调用我们定义的入口点函数,它会调用一个C/C++运行时启动函数(run-time startup function),这个函数由C/C++运行时定义并在链接时由-entry:[command-line]链接选项接定。该函数负责初始化C/C++运行时库,这样我们才能调用malloc、free等函数,它也保证了你声明的C++全局和静态对象在代码执行之前构造。表4-1列出了Windows程序中可用的4个入口点函数和C/C++运行时启动函数:
链接器负责在链接目标代码时为程序选择合适的C/C++运行时启动函数。如果指定了/SUBSYSTEM:WINDOWS开关,链接器将在代码中寻找WinMain或wWinMain函数,找不到时链接器返回"unresolved external symbol"错误,找到时则调用WinMainCRTStartup或wWinMainCRTStartup函数。类似的,如果指定了/SUBSYSTEM:CONSOLE开关,链接器将在代码中寻找main或wmain函数,并根据找到的结果调用mainCRTStartup或wmainCRTStartup函数,如果没有找到时链接器返回"unresolved external symbol"错误。
鲜为人知的是,你可以在项目中移除/SUBSYSTEM开关。这时链接器将根据在代码中找到的入口点函数自动识别应用的子系统类型,并调用相应的C/C++运行时启动函数。比如入口点函数为wmain,链接器在链接时将自动识别应用的子系统类别为console,并调用wmainCRTStartup函数。
初次接触Windows/Visual C++的开发人员经常会在创建新项目时选错项目类型,比如创建了一个Win32应用程序却将入口点函数定义为_tmain,导致组建(build)程序时发生链接器错误。这是因为Visual Studio为Win32项目设置了/SUBSYSTEM:WINDOWS开关,链接器据此在代码中寻找wWinMain/WinMain函数却没找到。这时开发者有四种选择:
(1) 将_tmain改为_tWinMain,通常这不是最佳选择,因为开发者原本要创建的可能是控制台应用程序(CUI)。
(2) 新建一个类型正确的项目,并将已有的源码拷贝过去。这种选择过于冗烦。
(3) 选择“项目”——“属性”——“通用配置”——“链接器”——“系统”,将“子系统”更改为正确的类型。
(4) 选择“项目”——“属性”——“通用配置”——“链接器”——“系统”,将“子系统”开关置空。这样链接器将根据代码中的入口点函数自动识别子系统类别。
所有的C/C++启动函数功能基本相同,不同之处在于它们处理的字符串是ANSI还是UNICODE、在初始化C运行时库之后它们调用的入口点函数是哪个。Visaul C++将这些代码封装在CRT库中,你可以在crtexe.c文件中找到这4个启动函数的代码。下面是启动函数所做的初始化工作:
(1) 获得指向新进程命令行的指针;
(2) 获得指向新进程环境变量的指针;
(3) 初始化C/C++运行时全局变量,在代码中包含了stdlib.h后就可以使用这些变量。表4-2列出了这些变量,注意这些变量由于安全原因不推荐使用,因为使用它们在使用时有可能尚未被CRT初始化,你可以用相应的Windows API获得它们;
(4) 初始化CRT内存分配函数(malloc、free等)以及其它低层输入/输出函数用到的堆;
(5) 调用所有全局和静态C++对象的构造函数;
完成这些初始化之后,启动函数调用我们定义的程序入口点函数。下面是展示了入口点函数为_tWinMain且定义了_UNICODE宏时,启动函数调用入口点函数的代码:
GetStartupInfo(&StartupInfo); int nMainRetVal = wWinMain((HINSTANCE)&__ImageBase, NULL, pszCommandLineUnicode, (StartupInfo.dwFlags & STARTF_USESHOWWINDOW) ? StartupInfo.wShowWindow : SW_SHOWDEFAULT);
没有定义_UNICODE宏时的代码如下:
GetStartupInfo(&StartupInfo); int nMainRetVal = WinMain((HINSTANCE)&__ImageBase, NULL, pszCommandLineAnsi, (StartupInfo.dwFlags & STARTF_USESHOWWINDOW) ? StartupInfo.wShowWindow : SW_SHOWDEFAULT);
__ImageBase是链接器定义的伪变量,它存储了可执行映射到应用程序内存中的地址。在下一节“进程实例句柄”中我们将详细讨论这个话题。
如果程序入口点是_tmain,启动函数将做如下调用:
/* when _UNICODE is defined */ int nMainRetVal = wmain(argc, argv, envp); /* when _UNICODE is not defined */ int nMainRetVal = main(argc, argv, envp);
使用Visual Studio向导创建console程序时,main/wmain的第三个参数envp不会出现在自动生成的入口点函数中:
int _tmain(int argc, TCHAR* argv[]);
如果你需要在代码中访问环境变量,用以下语句替代上面的定义:
int _tmain(int argc, TCHAR* argv[], TCHAR* env[]);
env参数是一个表示环境变量的指针数组,其中每个元素都是形如"key=value"的键值对。在“进程的环境变量”一节中我们将详细讨论环境变量。
当入口点函数返回时,启动函数调用CRT的exit函数,将入口点函数的返回值传给它(nMainRetVal)。exit函数完成下面几项工作:
(1) 调用_onexit函数注册的所有函数;
(2) 调用所有全局和静态C++对象的析构函数;
(3) 在DEBUG模式下,如果指定了_CRTDBG_LEAK_CHECK_DF标志,调用_CrtDumpMemoryLeaks打印出程序中的内存汇露;
(4) 调用操作系统的ExitProcess函数,将nMainRetVal传递给它。操作系统据此停止进程并设置其退出码。