引言
Windows 创建不同的应用程序,所对应的函数入口也会有所不同,在此以main和wWinMain函数来分析一下为什么windows这么设计。其它类似main入口同理。
main函数
1. 首先创建一个windows控制台程序(如下所示),在源码处打个断点,点击调试
2. 查看调用堆栈
3. 右键外部代码,点击显示外部代码
4. 点击invoke_main
可以看到windows对main函数外面做了一层封装,一直到mianCRTStartup函数,这一层调用代码称为启动代码,是windows操作系统和程序的桥梁 。mainCRTStartup再之前就是内核代码了。
wWinMain函数
1. 创建windows桌面应用程序,在源码处打个断点,点击调试
2. 与上面同理,查看堆栈,可以看到堆栈与main函数貌似是一样的?继续往下看
3. 我们看下invoke_main源码
#if defined _SCRT_STARTUP_MAIN
using main_policy = __scrt_main_policy;
using file_policy = __scrt_file_policy;
using argv_policy = __scrt_narrow_argv_policy;
using environment_policy = __scrt_narrow_environment_policy;
static int __cdecl invoke_main()
{
return main(__argc, __argv, _get_initial_narrow_environment());
}
#elif defined _SCRT_STARTUP_WMAIN
using main_policy = __scrt_main_policy;
using file_policy = __scrt_file_policy;
using argv_policy = __scrt_wide_argv_policy;
using environment_policy = __scrt_wide_environment_policy;
static int __cdecl invoke_main()
{
return wmain(__argc, __wargv, _get_initial_wide_environment());
}
#elif defined _SCRT_STARTUP_WINMAIN
using main_policy = __scrt_winmain_policy;
using file_policy = __scrt_file_policy;
using argv_policy = __scrt_narrow_argv_policy;
using environment_policy = __scrt_narrow_environment_policy;
static int __cdecl invoke_main()
{
return WinMain(
reinterpret_cast<HINSTANCE>(&__ImageBase),
nullptr,
_get_narrow_winmain_command_line(),
__scrt_get_show_window_mode());
}
#elif defined _SCRT_STARTUP_WWINMAIN
using main_policy = __scrt_winmain_policy;
using file_policy = __scrt_file_policy;
using argv_policy = __scrt_wide_argv_policy;
using environment_policy = __scrt_wide_environment_policy;
static int __cdecl invoke_main()
{
return wWinMain(
reinterpret_cast<HINSTANCE>(&__ImageBase),
nullptr,
_get_wide_winmain_command_line(),
__scrt_get_show_window_mode());
}
#elif defined _SCRT_STARTUP_ENCLAVE || defined _SCRT_STARTUP_WENCLAVE
using main_policy = __scrt_enclavemain_policy;
using file_policy = __scrt_nofile_policy;
using argv_policy = __scrt_no_argv_policy;
using environment_policy = __scrt_no_environment_policy;
#if defined _SCRT_STARTUP_ENCLAVE
static int __cdecl invoke_main()
{
return main(0, nullptr, nullptr);
}
#else
static int __cdecl invoke_main()
{
return wmain(0, nullptr, nullptr);
}
#endif
#endif
这里可以看出不同的宏定义导致了执行不同的入口函数。这些宏在哪定义的?点击堆栈中的wWinMainCRTStartup函数(如下图),两个程序的入口cpp文件是不一样的。这里就决定了调用main函数还是wWinMain函数。
结论
以上我们可以看到main和wWinMain函数的整个调用过程,为什么要这么处理,看各个入口函数的参数就能看出,因为不同类型的应用程序有不同的要求,需要不同的环境来运行和管理。
举例来说,控制台应用程序主要通过命令行输入和输出与用户交互,因此它们需要使用 "main" 函数来读取命令行参数和输出结果到控制台。另一方面,GUI(图形用户界面)应用程序则需要与用户通过图形界面进行交互,因此它们需要使用 "WinMain" 函数来启动和管理窗口,处理消息和事件等。
其它
Q:windows程序的启动函数mainCRTStartup,如何被调用的
A: 在 Windows 操作系统中,C/C++ 程序的启动函数名为 mainCRTStartup。这个函数是由 C 运行时库(CRT)提供的,它负责初始化 CRT 并调用程序的 main 函数,从而启动程序。
在 Windows 操作系统中,mainCRTStartup 函数是通过链接器(Linker)在程序的可执行文件中生成的。当用户双击可执行文件或使用命令行启动程序时,操作系统会创建一个新的进程,并在进程的上下文中执行可执行文件中的代码。在进程启动时,操作系统会将控制权转移到 mainCRTStartup 函数,并开始执行它的代码。
mainCRTStartup 函数的主要作用是执行一系列初始化操作,包括:
-
初始化 CRT 运行时环境,包括内存分配器、文件 I/O 等。
-
解析命令行参数,并将它们传递给程序的 main 函数。
-
调用程序的 main 函数,并将 main 函数的返回值作为程序的退出代码。
当 mainCRTStartup 函数执行完毕后,程序的执行流程将转移到 CRT 库的退出函数(如 exit 函数),执行一些清理操作,最终退出程序并返回操作系统。
总之,mainCRTStartup 函数是 Windows 程序的启动函数,它由 CRT 运行时库提供,并由链接器生成在程序的可执行文件中。操作系统会在进程启动时调用 mainCRTStartup 函数,它执行一系列初始化操作,最终将控制权转移给程序的 main 函数,启动程序的执行。
Q: 为什么上面有.nil文件而不是.cpp和.h文件
A: 通常情况下,.nil文件是用于内联函数的头文件,而不是普通的头文件。内联函数是一种函数,当编译器遇到内联函数时,它会在编译时将函数的代码直接嵌入到调用该函数的代码中,而不是像普通函数那样需要调用函数时跳转到函数的代码段执行。这样就避免了函数调用时的开销,提高了程序的运行效率。因此,内联函数通常适用于函数体较短、频繁调用、对性能要求较高的场景。
Q: 内联函数和宏定义的区别
A: 宏定义是预处理阶段将函数的代码嵌入到调用处,内联函数是编译阶段将函数嵌入到调用处,以避免函数调用的开销。它们之间的区别:
-
类型安全:内联函数可以进行类型检查,从而保证类型安全;而宏定义则没有类型检查,容易产生潜在的类型错误。
-
可读性:内联函数的代码比较清晰易读,能够提高代码的可读性;而宏定义的代码则可能比较复杂,可读性较差。
-
调试:内联函数可以像普通函数一样进行调试,方便程序员进行调试和排错;而宏定义则不支持调试,出错时难以定位。
-
宏定义支持的功能更多,比如可以定义包含多个语句的代码块等。
需要注意的是,使用内联函数和宏定义时需要权衡其优缺点,并根据实际情况进行选择。在一些场合,内联函数会比宏定义更加安全、可读、易于调试;而在另一些场合,宏定义则可能更加方便、灵活、功能强大。