具有名为main而不是main函数的全局变量的程序如何工作?


97

考虑以下程序:

#include <iostream>
int main = ( std::cout << "C++ is excellent!\n", 195 ); 

使用Windows 7 OS上的g ++ 4.8.1(mingw64),程序可以编译并正常运行,并进行打印:

C ++很棒!

到控制台。main似乎是全局变量而不是函数;没有功能,该程序如何执行main()?此代码是否符合C ++标准?程序的行为是否定义明确?我也使用了该-pedantic-errors选项,但是程序仍然可以编译并运行。


11
@πάνταῥεῖ:为什么需要语言律师标签?
毁灭者

14
请注意,这195是该RET指令的操作码,并且在C调用约定中,调用者将清除堆栈。
布莱恩(Brian)

2
@PravasiMeet“然后该程序如何执行” –您不认为应该执行变量的初始化代码(即使没有该main()函数吗?实际上,它们是完全无关的。)
顺磁牛角包

4
我是发现程序segfaults仍然是这样的人(64位linux,g ++ 5.1 / clang 3.6)。我可以通过将其修改为int main = ( std::cout << "C++ is excellent!\n", exit(0),1 );(并包括<cstdlib>)来纠正此问题,尽管该程序仍然存在法律上的缺陷。
Mike Kinghan

11
@Brian在进行这样的声明时应该提到架构。全世界不是VAX。或x86。管他呢。
dmckee ---前主持人小猫,2015年

Answers:


84

在深入探讨正在发生的问题之前,必须指出,根据缺陷报告1886:main()的语言链接,程序是格式错误的:

[...]在全局范围内声明变量main或使用C语言链接(在任何命名空间中)声明main名称的程序格式错误。[...]

最新版本的clang和gcc导致此错误,并且程序将无法编译(请参见gcc live示例):

error: cannot declare '::main' to be a global variable
int main = ( std::cout << "C++ is excellent!\n", 195 ); 
    ^

那么,为什么在旧版本的gcc和clang中没有诊断程序?该缺陷报告甚至在2014年底之前都没有提出解决方案,因此该案例直到最近才被明确发现其病态严重,需要进行诊断。

在此之前,这似乎是未定义的行为,因为我们违反了[basic.start.main]部分中C ++标准草案的规定要求:3.6.1

程序应包含一个称为main的全局功能,这是程序的指定启动位置。[...]

未定义的行为是不可预测的,不需要诊断。我们看到的与重现行为的不一致是典型的未定义行为。

那么代码实际上在做什么?为什么在某些情况下会产生结果?让我们看看我们有什么:

declarator  
|        initializer----------------------------------
|        |                                           |
v        v                                           v
int main = ( std::cout << "C++ is excellent!\n", 195 ); 
    ^      ^                                   ^
    |      |                                   |
    |      |                                   comma operator
    |      primary expression
global variable of type int

我们拥有main一个在全局名称空间中声明的int并正在初始化,该变量具有静态存储持续时间。由实现定义,是否在尝试调用之前进行初始化,main但是gcc似乎在调用之前进行了初始化main

该代码使用逗号运算符,左操作数是一个舍弃的值表达式,在这里仅用于调用的副作用std::cout。逗号运算符的结果是右操作数,在这种情况下,右运算数195是分配给变量的prvalue main

我们可以看到sergej指出了cout在静态初始化期间调用的生成的程序集显示。尽管讨论现场直播更有趣的一点是:

main:
.zero   4

随后:

movl    $195, main(%rip)

可能的情况是该程序跳转到main期望有效代码的符号,并且在某些情况下将出现seg-fault。因此,在这种情况下,假设我们位于允许代码执行的段中,那么我们期望在变量中存储有效的机器代码main可能会导致程序可行。我们可以看到,这个1984年进入IOCCC只是

看来我们可以让gcc在C中使用(现场观看)进行此操作:

const int main = 195 ;

据赛格故障,如果变量main不是常量大概是因为它并不位于一个可执行文件的位置,帽尖这个评论这里这给了我这个想法。

另请参见FUZxxl对此问题的特定于C版本的答案


为什么实施也没有给出任何警告。(当我使用-Wall和-Wextra时,仍然没有给出警告)。为什么?您如何看待@Mark B对这个问题的回答?
破坏者

恕我直言,编译器不应发出警告,因为main它不是保留的标识符(3.6.1 / 3)。在这种情况下,我认为VS2013对这种情况的处理(请参见弗朗西斯·库格勒的答案)在处理方面比gcc和clang更正确。
cdmh

@PravasiMeet我更新了我的答案,以了解为什么早期版本的gcc无法给出诊断。
Shafik Yaghmour

2
…的确,当我在Linux / x86-64上使用g ++ 5.2(接受该程序-我猜您不是在开玩笑“最新版本”)测试OP的程序时,它完全崩溃了将。
zwol

1
@Walter我不认为这些是重复的,前者提出的问题要狭窄得多。显然,有一组SO用户对重复项具有更简化的看法,这对我来说并没有太大意义,因为我们可以将大多数SO问题归结为某些较早版本的问题,所以SO不会很有用。
Shafik Yaghmour

20

从3.6.1 / 1起:

程序应包含一个称为main的全局功能,这是程序的指定启动位置。由实现定义是否需要在独立环境中的程序来定义主要功能。

由此看来,g ++恰好允许没有主函数的程序(大概是“独立式”子句)。

然后从3.6.1 / 3开始:

在程序中不得使用main函数(3.2)。main的链接(3.5)已定义为实现。声明main为内联或静态的程序的格式不正确。名称main否则没有保留。

因此,在这里我们知道拥有一个名为的整数变量是完全可以的main

最后,如果您想知道为什么打印输出,则int main使用逗号运算符进行初始化以cout在静态init处执行,然后提供实际的整数值来进行初始化。


7
有趣的是,如果您重命名main为其他名称,链接将会失败:(.text+0x20): undefined reference to main'`
Fred Larson

1
您是否不必指定gcc您的程序是独立的?
Shafik Yaghmour

9

gcc 4.8.1生成以下x86程序集:

.LC0:
    .string "C++ is excellent!\n"
    subq    $8, %rsp    #,
    movl    std::__ioinit, %edi #,
    call    std::ios_base::Init::Init() #
    movl    $__dso_handle, %edx #,
    movl    std::__ioinit, %esi #,
    movl    std::ios_base::Init::~Init(), %edi  #,
    call    __cxa_atexit    #
    movl    $.LC0, %esi #,
    movl    std::cout, %edi #,
    call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)   #
    movl    $195, main(%rip)    #, main
    addq    $8, %rsp    #,
    ret
main:
    .zero   4

请注意,它cout是在初始化期间调用的,而不是在main函数中!

.zero 4声明从location开始的4个(0初始化)字节main,其中变量[!]main的名称。

main符号被解释为程序的开始。行为取决于平台。


1
正如Brian指出的那样, 请注意195ret某些体系结构上的操作码。因此说零条指令可能并不准确。
Shafik Yaghmour 2015年

@ShafikYaghmour感谢您的评论,您是对的。我搞砸了汇编程序指令。
sergej

8

那是一个格式错误的程序。它在我的测试环境cygwin64 / g ++ 4.9.3上崩溃。

从标准:

3.6.1主要功能 [basic.start.main]

1程序应包含一个称为main的全局功能,这是程序的指定启动位置。


我认为在我引用的缺陷报告之前,这只是普通的未定义行为。
Shafik Yaghmour

@ShafikYaghmour,就是在所有的地方标准用途适用的一般原则
R Sahu

我想说是,但我看不出有什么区别。从我的讨论中可以看出,格式错误的NDR和未定义的行为可能是同义词,因为它们都不需要诊断。这似乎暗示着格式不正确,并且UB不同但不确定。
Shafik Yaghmour

3
C99第4节(“符合性”)对此进行了明确规定:“如果违反了在约束之外出现的“应”或“不得”的要求,则行为是不确定的。” 我在C ++ 98或C ++ 11中找不到等效的措词,但我强烈怀疑委员会的意图是它在那里。(C和C ++委员会确实需要坐下来,消除两个标准之间的所有术语差异。)
zwol

7

我认为这行得通的原因是编译器不知道它在编译main()函数,所以它编译具有赋值副作用的全局整数。

对象格式,该转换单元被编译成不能够一个之间进行区分的函数符号和一个可变符号

因此,链接器愉快地链接到(变量)符号,并将其视为函数调用。但要等到运行时系统运行了全局变量初始化代码。

当我运行样本时,将其打印出来,但随后导致了段错误。我假设是在运行时系统试图执行一个int变量时,就好像它是一个函数一样


4

我已经在使用VS2013的Win7 64位操作系统上进行了尝试,并且可以正确编译,但是当我尝试构建应用程序时,会从输出窗口中收到此消息。

1>------ Build started: Project: tempTest, Configuration: Debug Win32 ------
1>LINK : fatal error LNK1561: entry point must be defined
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========

2
FWIW,这是链接器错误,而不是调试器的消息。编译成功,但是链接器找不到函数,main()因为它是类型的变量int
cdmh

感谢您的答复,我将改写我的最初回答以反映这一点。
弗朗西斯·库格勒

-1

您正在这里进行棘手的工作。作为main(不知何故)可以声明为整数。您使用列表运算符打印消息,然后为其分配195。就像下面的人所说的那样,它不适合C ++。但是由于编译器没有找到任何用户定义的名称,主要的,它没有抱怨。记住main不是系统定义的函数,它的用户定义函数和程序开始执行的是Main Module,而不是main()。再次由启动函数调用main(),该启动函数由加载程序有意执行。然后初始化所有变量,并在初始化时输出类似的内容。而已。没有main()的程序可以,但是不是标准的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.