C++中的WINMAIN和main()(扩展版)

83

没错,我看了这篇帖子:C++中WinMain、main和DllMain的区别

现在我知道WINMAIN用于窗口应用程序,main()用于控制台。但是阅读这篇帖子并没有真正告诉我为什么不同的主函数要分开来启动程序。

我的意思是,将不同的主函数分离出来启动程序有什么意义呢?是因为性能问题吗?还是其他原因呢?


取决于您想传递什么给应用程序。根据标准,int main()始终允许作为入口点,但是int main(int, char**)也是一样。 - chris
7个回答

221

关于函数

C和C++标准要求任何程序(针对“托管”C或C++实现)都必须有一个名为main的函数,该函数作为程序的启动函数main函数在非本地静态变量进行零初始化之后被调用,可能但不一定会在此类变量的动态初始化之后调用(! ,C++11 §3.6.2 / 4)。它可以具有以下签名之一:

int main()
int main( int argc, char* argv[] )

除了结果类型必须是int外,还可能具有实现定义的签名(C++11 §3.6.1/2)。

作为C++中唯一的这样的函数,main具有默认结果值,即0。如果main返回,则在普通函数返回后调用exit,并将main结果值作为参数传递给它。标准定义了三个确保可以使用的值:0(表示成功),EXIT_SUCCESS(也表示成功,通常定义为0),以及EXIT_FAILURE(表示失败),其中两个命名常量由<stdlib.h>头文件定义,该头文件还声明了exit函数。

main参数旨在表示用于启动进程的命令行参数。 argc(参数计数)是argv(参数值)数组中的项目数。除了这些项目之外,argv[argc]保证为0。 如果argc > 0(不保证!),则argv [0]保证要么是指向空字符串的指针,要么是指向“用于调用程序的名称”的指针。此名称可能包括路径,也可能是可执行文件的名称。

在*nix中使用main参数获取命令行参数很好用,因为C和C++起源于*nix。 但是,Windows ANSI是对main参数进行编码的事实上的Windows标准,它不支持通用的Windows文件名(例如,对于挪威语Windows安装而言,带有希腊或西里尔字符的文件名)。因此,Microsoft选择使用一个名为wmain的Windows特定启动函数扩展了C和C++语言,该函数具有基于宽字符的参数,其编码为UTF-16,可以表示任何文件名。

wmain函数可以具有这些签名之一,对应于main的标准签名:

int wmain()
int wmain( int argc, wchar_t* argv[] )

还有一些不是特别有用的函数。

也就是说,wmainmain 的直接宽字符替换。

WinMain 是一个基于 char 类型的函数,在 20 世纪 80 年代早期引入了 Windows 系统:

int CALLBACK WinMain(
    HINSTANCE   hInstance,
    HINSTANCE   hPrevInstance,
    LPSTR       lpCmdLine,
    int         nCmdShow
    );

其中,CALLBACKHINSTANCELPSTR<windows.h>头文件定义(LPSTR仅仅是char*)。

参数:

  • hInstance参数值是可执行文件内存映像的基地址,它主要用于从可执行文件中加载资源,也可以通过GetModuleHandle API函数获得,

  • hPrevInstance参数总是为0,

  • lpCmdLine参数也可以通过GetCommandLine API函数获得,再加上一些奇怪的逻辑来跳过命令行中的程序名称部分,

  • nCmdShow参数值也可以通过GetStartupInfo API函数获得,但在现代Windows中,第一个顶层窗口的创建会自动完成这一步骤,因此没有任何实际用处。

因此,WinMain函数与标准的main函数具有相同的缺点(特别是冗长和非标准),却没有自己的优势,因此它真的很难解释,除非可能是供应商锁定的原因。但是,在Microsoft工具链中,它会将链接器默认设置为GUI子系统,这被一些人视为优点。但是,在例如GNU工具链中,它没有这种效果,因此不能依赖这种效果。

wWinMain基于wchar_t的函数是WinMain的宽字符变体,就像wmain是标准main的宽字符变体一样:

int WINAPI wWinMain(
    HINSTANCE   hInstance,
    HINSTANCE   hPrevInstance,
    PWSTR       lpCmdLine,
    int         nCmdShow
    );

WINAPICALLBACK是相同的,PWSTR仅仅是。

除了最不为人知和最少支持的函数wmain外,没有使用任何非标准函数的好理由,即使只是为了方便。这样可以避免使用GetCommandLineCommandLineToArgvW API函数来接收UTF-16编码的参数。

为了避免Microsoft链接器出现问题(GNU工具链的链接器不会出现这种问题),只需将LINK环境变量设置为/entry:mainCRTStartup,或直接指定该选项。这是Microsoft运行时库入口点函数,在一些初始化之后调用标准的main函数。其他启动函数都有相应的以相同系统方式命名的入口点函数。


使用标准main函数的示例。

常见源代码:

    foo.cpp

#undef UNICODE
#define UNICODE
#include <windows.h>

int main()
{
    MessageBox( 0, L"Press OK", L"Hi", MB_SETFOREGROUND );
}

以下示例(分别使用GNU工具链和Microsoft工具链)首先将该程序构建为控制台子系统程序,然后是GUI子系统程序。控制台子系统程序,简称控制台程序,是需要控制台窗口的程序。这是我使用的所有Windows链接器(尽管不是很多)的默认子系统,可能适用于所有Windows链接器。
对于控制台程序,如果需要,Windows会自动创建一个控制台窗口。任何Windows进程,无论子系统如何,都可以有一个关联的控制台窗口,最多只能有一个。此外,Windows命令解释器等待控制台程序完成,以便程序的文本显示完成。
相反,GUI子系统程序不需要控制台窗口。除了批处理文件之外,命令解释器不会等待GUI子系统程序。避免两种程序均需完成等待的一种方法是使用start命令。将控制台窗口文本从GUI子系统程序中呈现的一种方法是重定向其标准输出流。另一种方法是从程序的代码中显式创建控制台窗口。
程序的子系统编码在可执行文件的头部中。它不会被Windows Explorer显示(除了在Windows 9x中,“快速查看”可执行文件时,这几乎呈现了与Microsoft的dumpbin工具相同的信息)。没有相应的C++概念。
使用GNU工具链的main函数。
[D:\dev\test]
> g++ foo.cpp

[D:\dev\test]
> objdump -x a.exe | find /i "subsys"
主系统版本号   4
次系统版本号   0
子系统               00000003        (Windows CUI)
[544](sec -1)(fl 0x00)(ty   0)(scl   2) (nx 0) 0x00000004 __major_subsystem_version__
[612](sec -1)(fl 0x00)(ty   0)(scl   2) (nx 0) 0x00000003 __subsystem__
[636](sec -1)(fl 0x00)(ty   0)(scl   2) (nx 0) 0x00000000 __minor_subsystem_version__
[D:\dev\test] > g++ foo.cpp -mwindows [D:\dev\test] > objdump -x a.exe | find /i "subsys" 主系统版本号 4 次系统版本号 0 子系统 00000002 (Windows GUI) [544](sec -1)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x00000004 __major_subsystem_version__ [612](sec -1)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x00000002 __subsystem__ [636](sec -1)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x00000000 __minor_subsystem_version__
[D:\dev\test] > _

使用微软工具链的 main 函数:

[D:\dev\test]
> set LINK=/entry:mainCRTStartup

[D:\dev\test]
> cl foo.cpp user32.lib
foo.cpp

[D:\dev\test]
> dumpbin /headers foo.exe | find /i "subsys"
            6.00 子系统版本
               3 子系统 (Windows CUI)
[D:\dev\test] > cl foo.cpp /link user32.lib /subsystem:windows foo.cpp [D:\dev\test] > dumpbin /headers foo.exe | find /i "subsys" 6.00 子系统版本 2 子系统 (Windows GUI)
[D:\dev\test] > _

使用 Microsoft 的 wmain 函数的示例。

下面是使用 GNU 工具链和 Microsoft 工具链的常见主代码:

    bar.cpp

#undef UNICODE
#define UNICODE
#include <windows.h>

#include <string>       // std::wstring
#include <sstream>      // std::wostringstream
using namespace std;

int wmain( int argc, wchar_t* argv[] )
{
    wostringstream  text;

    text << argc - 1 << L" command line arguments:\n";
    for( int i = 1;  i < argc;  ++i )
    {
        text << "\n[" << argv[i] << "]";
    }

    MessageBox( 0, text.str().c_str(), argv[0], MB_SETFOREGROUND );
}

使用GNU工具链的wmain

GNU工具链不支持Microsoft的wmain函数:

[D:\dev\test]
> g++ bar.cpp
d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../libmingw32.a(main.o):main.c:(.text.startup+0xa3): undefined reference to `WinMain
@16'
collect2.exe: error: ld returned 1 exit status
[D:\dev\test] > _

这里的链接错误消息是关于WinMain的,因为GNU工具链支持那个函数(大概是因为太多的旧代码使用它),并在找不到标准的main后将其作为最后的选择。

然而,添加一个模块,其中包含调用wmain的标准main非常简单:

    wmain_support.cpp

extern int wmain( int, wchar_t** );

#undef UNICODE
#define UNICODE
#include <windows.h>    // GetCommandLine, CommandLineToArgvW, LocalFree

#include <stdlib.h>     // EXIT_FAILURE

int main()
{
    struct Args
    {
        int n;
        wchar_t** p;

        ~Args() {  if( p != 0 ) { ::LocalFree( p ); } }
        Args(): p(  ::CommandLineToArgvW( ::GetCommandLine(), &n ) ) {}
    };

    Args    args;

    if( args.p == 0 )
    {
        return EXIT_FAILURE;
    }
    return wmain( args.n, args.p );
}

现在,
[D:\dev\test]
> g++ bar.cpp wmain_support.cpp

[D:\dev\test]
> objdump -x a.exe | find /i "subsystem"
MajorSubsystemVersion   4
MinorSubsystemVersion   0
Subsystem               00000003        (Windows CUI)
[13134](sec -1)(fl 0x00)(ty   0)(scl   2) (nx 0) 0x00000004 __major_subsystem_version__
[13576](sec -1)(fl 0x00)(ty   0)(scl   2) (nx 0) 0x00000003 __subsystem__
[13689](sec -1)(fl 0x00)(ty   0)(scl   2) (nx 0) 0x00000000 __minor_subsystem_version__
[D:\dev\test] > g++ bar.cpp wmain_support.cpp -mwindows [D:\dev\test] > objdump -x a.exe | find /i "subsystem" MajorSubsystemVersion 4 MinorSubsystemVersion 0 Subsystem 00000002 (Windows GUI) [13134](sec -1)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x00000004 __major_subsystem_version__ [13576](sec -1)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x00000002 __subsystem__ [13689](sec -1)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x00000000 __minor_subsystem_version__
[D:\dev\test] > _

使用Microsoft的工具链编写wmain函数

在Microsoft的工具链中,如果未指定入口点且存在wmain函数(不清楚是否同时存在标准的main函数,最近几年没有检查过),链接器会自动推断wmainCRTStartup为入口点:

[D:\dev\test]
> set link=/entry:mainCRTStartup

[D:\dev\test]
> cl bar.cpp user32.lib
bar.cpp
LIBCMT.lib(crt0.obj) : error LNK2019: unresolved external symbol _main referenced in function ___tmainCRTStartup
bar.exe : fatal error LNK1120: 1 unresolved externals
[D:\dev\test] > set link= [D:\dev\test] > cl bar.cpp user32.lib bar.cpp [D:\dev\test] > _

然而,对于非标准的启动函数,如wmain,最好还是明确指定入口点,以便非常清楚地表示意图:

[D:\dev\test]
> 使用命令行编译bar.cpp并链接user32.lib库,入口点为wmainCRTStartup:
bar.cpp

[D:\dev\test]
> 执行dumpbin /headers bar.exe命令,并查找子系统信息:
            6.00 子系统版本
               3 子系统(Windows CUI)
[D:\dev\test] > 使用命令行编译bar.cpp并链接user32.lib库,入口点为wmainCRTStartup,子系统类型为windows: bar.cpp [D:\dev\test] > 执行dumpbin /headers bar.exe命令,并查找子系统信息: 6.00 子系统版本 2 子系统(Windows GUI)
[D:\dev\test] > _

有人可以更新有效的 winmain() 签名链接吗? - jvriesem
3
WinMainwWinMain只有一个有效的签名。然而,wmain就像标准的main一样,只是使用宽字符串参数。这意味着它支持像标准main一样的函数签名:int wmain(), int wmain( int, wchar_t** ), 以及作为扩展的 int wmain( int, wchar_t**, wchar** )。第三个参数通常被称为envp,它是一个指向环境变量字符串的指针,其中“根据当前 Microsoft 文档,该数组以 NULL 条目终止”。现在,翻译完了,SO不让我写更多文字。 - Cheers and hth. - Alf

15

根据@RaymondChen所说:

WinMain这个名称只是一种惯例

虽然WinMain函数在平台SDK中有文档记录,但它并不是平台的真正组成部分。相反,WinMain是Windows程序提供给用户的入口点的惯用名称。

真正的入口点在C运行时库中,它初始化运行时,运行全局构造函数,然后调用您的WinMain函数(如果您喜欢Unicode入口点,则调用wWinMain)。

DllMain和WinMain在其原型本身上是不同的。WinMain接受命令行参数,而另一个则涉及它如何连接到进程。

根据MSDN文档

默认情况下,起始地址是C运行时库中的函数名。链接器根据程序的属性选择它,如下表所示。

  • mainCRTStartup (或 wmainCRTStartup):使用/SUBSYSTEM:CONSOLE;的应用程序调用main(或wmain

  • WinMainCRTStartup(或wWinMainCRTStartup):使用/SUBSYSTEM:WINDOWS;的应用程序调用WinMain(或wWinMain),必须使用__stdcall定义

  • _DllMainCRTStartup:DLL; 如果存在,则调用使用__stdcall定义的DllMain


4

标准的C程序在启动时通过命令行传递两个参数:

int main( int argc, char** argv ) ;
  • char** argv 是一个字符串 (char*) 数组
  • int argc 是 argv 中 char* 的数量

程序员必须为 Windows 程序编写启动函数 WinMain,它略有不同。 WinMain 接受 4 个参数,在启动时由 Win O/S 传递给程序:

int WINAPI WinMain( HINSTANCE hInstance,    // HANDLE TO AN INSTANCE.  This is the "handle" to YOUR PROGRAM ITSELF.
                    HINSTANCE hPrevInstance,// USELESS on modern windows (totally ignore hPrevInstance)
                    LPSTR szCmdLine,        // Command line arguments.  similar to argv in standard C programs
                    int iCmdShow )          // Start window maximized, minimized, etc.

查看我的文章如何使用C语言创建基本窗口了解更多信息。


几个错误的说法: "必须编写",不是。 "通过Win操作系统传递... ",也不是。此外还有一些术语:"启动功能",也不是。 - Cheers and hth. - Alf
2
“必须编写”是错误的,因为您始终可以使用标准的main来创建应用程序,而不管Windows子系统如何。 “由Win O / S传递”是错误的,因为这些参数不是由操作系统传递的。它们是由入口点函数传递的,该函数通过调用“GetCommandLine”从操作系统获取信息。 “引导函数”是误导性的,因为没有涉及引导过程。 - Cheers and hth. - Alf
2
在我看来,这个答案似乎是无用的。QA问了有什么区别,你却说了函数签名的差异。这不是显而易见的吗? - Yury

4
Windows CRT 暴露了 5 个符号 mainCRTStartup, wmainCRTStartup, wWinMainCRTStartup, _DllMainCRTStartupWinMainCRTStartup

enter image description here

< p > w 前面表示 Unicode 版本,即传递给进程的命令行(命令和参数字符串)(并存储在 PPB PEB->ProcessParameters->CommandLine 中)是 UTF-16(即 WCHAR(宽字符)),而不是 ASCII(UTF-8 或 ANSI,例如 Windows-1252、Windows-850 等)。

如果未指定 /DLL/SUBSYSTEM 选项,则 MSVC 链接器将选择子系统和入口点,即根据符号表中是否存在 mainWinMainDllMain 中的一个来使入口地址成为这 5 个函数之一。入口点是 通过 libcmt.lib 静态链接的 MSVC 链接器, 它只包括由该入口函数实际使用的符号。

mainCRTStartup会调用GetStartupInfo() / 访问PPB以获取标准输入/输出句柄和命令行参数。还会调用_init term()_init_atexit(),然后调用main。在设置了atexit例程后,将main的返回值传递给ExitProcess()
需要使用编译器选项/Zi创建的.pdb中的调试符号才能在IDA Pro中加载,以揭示诸如mainCRTstartupmain之类的符号,否则它们将分别表示为startsub_??????
参考资料:

2
我模糊地记得在某个地方读到过Windows程序有一个main()函数。它只是隐藏在某个头文件或库中。我相信这个main()函数初始化了WinMain()所需的所有变量,然后调用它。
当然,我是WinAPI新手,如果我错了,希望更有经验的人能纠正我。

如果这是真的,而且我的想法是正确的,那么这意味着我们可以通过模拟“隐藏的main()”来隐藏控制台窗口,而无需使用“-mwindows”。 - rr-

0

关于Main和WinMain的区别:

根据我从许多链接中了解到的:

WinMain()是任何Windows应用程序的C入口函数。就像普通的DOS/console应用程序有main()函数作为C入口点一样,在Windows中我们有WinMain()WinMain()是系统在创建进程期间调用的函数。

第一个参数是当前进程的实例句柄。

接下来是先前的实例。

命令行参数作为下一个参数传递。

最后,shell传递主窗口的显示属性。

注意:WinMain返回零表示成功,非零表示错误。


0

我有一个使用_tWinMain和Configuration Properties.Linker.System.Subsystem: Windows (/SUBSYSTEM:WINDOWS)的exe文件。后来我想让它支持cmdline args并打印到控制台,所以我添加了:

// We need to printf to stdout and we don't have one, so get one
AllocConsole();
// Redirect pre-opened STDOUT to the console
freopen_s((FILE **)stdout, "CONOUT$", "w", stdout);

但是那只能在另一个控制台窗口中打印,而且很快就消失了,所以并不是很有用。下面是我改变它的方式,使其能够与控制台(/SUBSYSTEM:CONSOLE)一起工作,这样我就可以来回切换,如果需要的话。

int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
{
  UNREFERENCED_PARAMETER(argc);
  UNREFERENCED_PARAMETER(argv);
  UNREFERENCED_PARAMETER(envp);
  return (_tWinMain(NULL, NULL, ::GetCommandLineW(), 0));
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接