C++程序的起点是main()函数吗?

137

C++标准中的第3.6.1/1节内容如下:

程序必须包含一个名为main的全局函数,它是程序的起始点

现在来看一下这段代码:

int square(int i) { return i*i; }
int user_main()
{ 
    for ( int i = 0 ; i < 10 ; ++i )
           std::cout << square(i) << endl;
    return 0;
}
int main_ret= user_main();
int main() 
{
        return main_ret;
}

这个示例代码做到了我想做的事情,即打印从0到9的整数的平方,并在进入“main()”函数之前完成。 “main()”函数应该是程序的“开始”。

我还使用了GCC 4.5.0的-pedantic选项进行了编译。 它没有错误,甚至没有警告!

因此,我的问题是,

这段代码是否真正符合标准?

如果它符合标准,那么它不会使标准失效吗?main()不是此程序的开始!user_main()main()之前执行。

我知道use_main()先执行以初始化全局变量main_ret,但那是完全不同的事情;关键是,它确实使引用的标准$3.6.1/1无效,因为main()不是程序的开始;实际上,它是程序的结尾


编辑:

您如何定义“开始”?

这归结为对短语"start of the program"的定义。 那么您如何定义它?

12个回答

94

你没有正确理解这个句子。

一个程序应该包含一个名为main的全局函数,它是程序的指定起点。

标准在为后续内容定义“起点”的概念。它并没有说在调用main 之前不会执行任何代码。它只是说程序的起点被认为是在函数 main 处。

你的程序符合标准。只有在启动 main 之前,程序才正式“启动”。根据标准中“起点”的定义,这个函数在程序“启动”之前就被调用了,但这并不重要。在每个程序中,在调用 main 之前将执行大量代码,而不仅仅是这个例子。

出于讨论的目的,你的函数在程序的“起点”之前被执行,这完全符合标准。


3
抱歉,我不同意你对该条款的解释。 - Lightness Races in Orbit
我认为Adam Davis是正确的,“main”更像是某种编码限制。 - laike9m
1
@AdamDavis:我不记得我当时的担忧是什么了,现在也想不出来一个。 - Lightness Races in Orbit
@DonSlowik,原帖提供了一个带有构造函数的示例代码,我的回答是针对该构造函数的。 - Adam Davis
1
@AdamDavis int user_main() 是一个被调用来初始化 int main_ret 的函数,而不是一个构造函数,它会被调用来初始化一个(用户定义的)类。但这也没关系。不仅构造函数在 main 函数之前运行,各种初始化代码也可以在 main 函数之前运行,如 https://en.cppreference.com/w/cpp/language/initialization 中所述,在一个翻译单元内有序地进行非局部动态初始化 3)。 - Don Slowik
显示剩余4条评论

88

不,C++在调用main函数之前会执行很多"设置环境"的操作;但是,main函数是C++程序中“用户指定”部分的正式开始。

其中一些环境设置是无法控制的(例如用于设置std::cout的初始代码),但是有些环境是可控的,例如静态全局块(用于初始化静态全局变量)。请注意,由于在main函数之前您没有完全的控制权,因此您无法完全控制静态块的初始化顺序。

在main函数之后,您的代码在程序中概念上“完全掌控”,因为您既可以指定要执行的指令,也可以指定执行它们的顺序。多线程可以重新排列代码执行顺序,但是使用C ++仍然可以控制这种情况,因为您已经指定了要执行代码段(可能)以无序的方式执行。


9
+1 对于这个答案。“请注意,由于在 main 函数之前您没有完全的控制权,因此您无法完全控制静态块初始化的顺序。在 main 函数之后,您的代码就可以在程序中概念上“完全控制”了,也就是说,您既可以指定要执行的指令,也可以指定执行它们的顺序。” 这也让我将此答案标记为已接受的答案... 我认为这些都是非常重要的点,足以证明 main() 是“程序开始”的关键。 - Nawaz
15
请注意,除了无法完全控制初始化顺序外,您还无法控制初始化错误:您无法在全局范围内捕获异常。 - André Caron
1
@Nawaz:什么是静态全局块?你能用简单的例子来解释一下吗?谢谢。 - Destructor
@meet: 在命名空间级别声明的对象具有static存储期,因此,属于不同翻译单元的这些对象可以以未指定的顺序进行初始化(因为顺序未指定)。虽然我不确定这是否回答了你的问题,但在这个话题的上下文中,这就是我能说的。 - Nawaz

25

如果没有main函数,您的程序将无法链接,因此也无法运行。然而,main()函数并不会引起程序执行的开始,因为文件级别上的对象具有在其之前运行的构造函数,因此有可能编写一个在达到main()函数之前就完成其生命周期的整个程序,而让main()函数本身为空。

实际上,为了强制执行这一点,您必须有一个在main函数之前构建的对象,它的构造函数调用所有程序的流程。

看这个例子:

class Foo
{
public:
   Foo();

 // other stuff
};

Foo foo;

int main()
{
}

你的程序的流程实际上源自于Foo::Foo()


13
注意:如果您在不同的翻译单元中有多个全局对象,这会让您很快陷入麻烦,因为构造函数调用的顺序是未定义的。您可以使用单例和延迟初始化来避免问题,但在多线程环境下,事情会变得非常混乱。简而言之,在实际代码中不要这样做。 - Alexandre C.
3
虽然你在代码中应该给 main() 函数一个正确的实现并允许它来执行,但是很多 LD_PRELOAD 库的基础概念都是建立在启动对象之外的。 - CashCow
2
@Alex:标准规定为未定义,但实际上链接顺序(通常取决于编译器)控制初始化顺序。 - ThomasMcLeod
1
@Thomas:我肯定不会尝试依赖那个,也绝不会尝试手动控制构建系统。 - Alexandre C.
1
@Alex:虽然现在不那么重要了,但是早些时候我们会使用链接顺序来控制构建镜像,以减少物理内存分页。即使它不影响程序语义,你可能还有其他原因想要控制初始化顺序,比如启动性能比较测试。 - ThomasMcLeod
显示剩余4条评论

16
您还标记了"C"问题,因此,严格来说,就C而言,根据ISO C99标准的6.7.8“初始化”部分,您的初始化应该失败。
在这种情况下最相关的似乎是约束条件#4,它说:
所有具有静态存储期的对象的初始化程序中的所有表达式都必须是常量表达式或字符串字面量。
因此,答案是该代码不符合C标准。
如果您只对C ++标准感兴趣,您可能需要删除"C"标签。

4
@Remo.D,你能告诉我们那个部分有什么内容吗?并不是所有人都了解C标准 :)。 - UmmaGumma
2
既然你这么挑剔: ANSI C自1989年以来已经过时了。 ISO C90或C99是相关的标准。 - Lundin
3
感谢Remo提供的信息,指出这不是有效的C语言;我之前不知道。这就是人们学习的方式,有时是计划好的,有时是偶然发生的! - Nawaz
@Remo.D 的信息很有用,点赞。所以这是C99标准,对吗?请在您的答案中添加标准版本。此外,我认为这篇文章非常有用,C标签必须保留。 - UmmaGumma
你是否使用了-ansi-pedantic进行编译?或者甚至还加上了-W -Wall - Evan Teran
显示剩余3条评论

11

整个3.6节非常清楚地阐述了main和动态初始化的交互。 "程序的指定起点"在其他任何地方都没有使用,仅仅是对main()的一般意图进行描述。将这个短语解释成与标准中更详细和清晰的要求相矛盾是毫无意义的。


9
编译器通常需要在 main() 函数之前添加代码以符合标准。因为标准规定全局变量/静态变量的初始化必须在程序执行之前完成。而且,同样适用于放置在文件范围(全局)对象的构造函数。
因此,原问题对于 C 语言也是相关的,因为在 C 程序中,仍然需要在程序启动之前进行全局/静态初始化。
标准假设这些变量是通过“魔法”初始化的,因为它们不说明在程序初始化之前应该如何设置。我认为他们认为这是编程语言标准范围之外的事情。
编辑:例如 ISO 9899:1999 5.1.2:
所有具有静态存储期的对象在程序启动之前必须被初始化(设置为其初始值)。除此之外,这种初始化的方式和时机是未指定的。
这种“魔法”背后的理论回溯到 C 语言诞生之初,当时它是一种旨在仅用于基于 RAM 的计算机上的 UNIX 操作系统的编程语言。理论上,程序将能够将所有预初始化的数据从可执行文件中加载到 RAM 中,同时将程序本身上传到 RAM 中。
自那时以来,计算机和操作

4

4
你的“程序”只是从全局变量返回一个值。其他都是初始化代码。 因此,标准保持不变-您只有一个非常简单的程序和更复杂的初始化。

2

看起来是一种英语语义争议。原帖将他的代码块首先称为“代码”,然后称之为“程序”。用户编写代码,然后编译器编写程序。


2
Ubuntu 20.04 glibc 2.31 RTFS + GDB
glibc 在 main 函数之前进行一些设置,以使其某些功能正常工作。让我们尝试追踪此源代码。

hello.c

#include <stdio.h>

int main() {
    puts("hello");
    return 0;
}

编译和调试:
gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o hello.out hello.c
gdb hello.out

现在在 GDB 中:
b main
r
bt -past-main

给予:
#0  main () at hello.c:3
#1  0x00007ffff7dc60b3 in __libc_start_main (main=0x555555555149 <main()>, argc=1, argv=0x7fffffffbfb8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffbfa8) at ../csu/libc-start.c:308
#2  0x000055555555508e in _start ()

这已经包含了main调用者的代码行:https://github.com/cirosantilli/glibc/blob/glibc-2.31/csu/libc-start.c#L308。
由于glibc的遗留性和通用性水平,该函数有数十亿个ifdef语句,但是对我们产生影响的一些关键部分应该简化为:
# define LIBC_START_MAIN __libc_start_main

STATIC int
LIBC_START_MAIN (int (*main) (int, char **, char **),
         int argc, char **argv,
{

      /* Initialize some stuff. */

      result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
  exit (result);
}

在调用 __libc_start_main 之前,我们已经处于 _start 程序入口点。通过添加 gcc -Wl,--verbose 我们可以知道这是程序的入口点,因为链接脚本包含以下内容:
ENTRY(_start)

因此,它是动态加载器完成后实际执行的第一个指令。为了在GDB中确认这一点,我们可以使用-static进行编译来摆脱动态加载器。
gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o hello.out hello.c
gdb hello.out

然后使 GDB 在执行第一个 starti 指令时停止,并打印第一个指令:
starti
display/12i $pc

这个短语的翻译是:“这给出了:”。
=> 0x401c10 <_start>:   endbr64 
   0x401c14 <_start+4>: xor    %ebp,%ebp
   0x401c16 <_start+6>: mov    %rdx,%r9
   0x401c19 <_start+9>: pop    %rsi
   0x401c1a <_start+10>:        mov    %rsp,%rdx
   0x401c1d <_start+13>:        and    $0xfffffffffffffff0,%rsp
   0x401c21 <_start+17>:        push   %rax
   0x401c22 <_start+18>:        push   %rsp
   0x401c23 <_start+19>:        mov    $0x402dd0,%r8
   0x401c2a <_start+26>:        mov    $0x402d30,%rcx
   0x401c31 <_start+33>:        mov    $0x401d35,%rdi
   0x401c38 <_start+40>:        addr32 callq 0x4020d0 <__libc_start_main>

通过在源代码中查找 _start 并关注 x86_64 次数,我们可以看到这似乎对应于 sysdeps/x86_64/start.S:58

ENTRY (_start)
    /* Clearing frame pointer is insufficient, use CFI.  */
    cfi_undefined (rip)
    /* Clear the frame pointer.  The ABI suggests this be done, to mark
       the outermost frame obviously.  */
    xorl %ebp, %ebp

    /* Extract the arguments as encoded on the stack and set up
       the arguments for __libc_start_main (int (*main) (int, char **, char **),
           int argc, char *argv,
           void (*init) (void), void (*fini) (void),
           void (*rtld_fini) (void), void *stack_end).
       The arguments are passed via registers and on the stack:
    main:       %rdi
    argc:       %rsi
    argv:       %rdx
    init:       %rcx
    fini:       %r8
    rtld_fini:  %r9
    stack_end:  stack.  */

    mov %RDX_LP, %R9_LP /* Address of the shared library termination
                   function.  */
#ifdef __ILP32__
    mov (%rsp), %esi    /* Simulate popping 4-byte argument count.  */
    add $4, %esp
#else
    popq %rsi       /* Pop the argument count.  */
#endif
    /* argv starts just at the current stack top.  */
    mov %RSP_LP, %RDX_LP
    /* Align the stack to a 16 byte boundary to follow the ABI.  */
    and  $~15, %RSP_LP

    /* Push garbage because we push 8 more bytes.  */
    pushq %rax

    /* Provide the highest stack address to the user code (for stacks
       which grow downwards).  */
    pushq %rsp

#ifdef PIC
    /* Pass address of our own entry points to .fini and .init.  */
    mov __libc_csu_fini@GOTPCREL(%rip), %R8_LP
    mov __libc_csu_init@GOTPCREL(%rip), %RCX_LP

    mov main@GOTPCREL(%rip), %RDI_LP
#else
    /* Pass address of our own entry points to .fini and .init.  */
    mov $__libc_csu_fini, %R8_LP
    mov $__libc_csu_init, %RCX_LP

    mov $main, %RDI_LP
#endif

    /* Call the user's main function, and exit with its value.
       But let the libc call main.  Since __libc_start_main in
       libc.so is called very early, lazy binding isn't relevant
       here.  Use indirect branch via GOT to avoid extra branch
       to PLT slot.  In case of static executable, ld in binutils
       2.26 or above can convert indirect branch into direct
       branch.  */
    call *__libc_start_main@GOTPCREL(%rip)

这段话的翻译如下:

最终调用了 __libc_start_main,符合预期。

不幸的是,-static 使得来自 mainbt 显示的信息不够详细:

#0  main () at hello.c:3
#1  0x0000000000402560 in __libc_start_main ()
#2  0x0000000000401c3e in _start ()

如果我们去掉“-static”并从“starti”开始,则会得到以下结果:
=> 0x7ffff7fd0100 <_start>:     mov    %rsp,%rdi
   0x7ffff7fd0103 <_start+3>:   callq  0x7ffff7fd0df0 <_dl_start>
   0x7ffff7fd0108 <_dl_start_user>:     mov    %rax,%r12
   0x7ffff7fd010b <_dl_start_user+3>:   mov    0x2c4e7(%rip),%eax        # 0x7ffff7ffc5f8 <_dl_skip_args>
   0x7ffff7fd0111 <_dl_start_user+9>:   pop    %rdx

通过在源代码中使用 grep 命令搜索 _dl_start_user,可以看出这似乎来自于sysdeps/x86_64/dl-machine.h:L147
/* Initial entry point code for the dynamic linker.
   The C function `_dl_start' is the real entry point;
   its return value is the user program's entry point.  */
#define RTLD_START asm ("\n\
.text\n\
    .align 16\n\
.globl _start\n\
.globl _dl_start_user\n\
_start:\n\
    movq %rsp, %rdi\n\
    call _dl_start\n\
_dl_start_user:\n\
    # Save the user entry point address in %r12.\n\
    movq %rax, %r12\n\
    # See if we were run as a command with the executable file\n\
    # name as an extra leading argument.\n\
    movl _dl_skip_args(%rip), %eax\n\
    # Pop the original argument count.\n\
    popq %rdx\n\

这可能是动态加载器的入口点。
如果我们在 _start 处断点并继续执行,这似乎会与使用 -static 时结束于相同位置,然后调用 __libc_start_main
当我尝试一个 C++ 程序时:

hello.cpp

#include <iostream>

int main() {
    std::cout << "hello" << std::endl;
}

使用:

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o hello.out hello.cpp

结果基本相同,例如在main处的回溯完全相同。
我认为C++编译器只是调用钩子来实现任何C++特定的功能,并且在C/C++之间很好地分离了事物。
待办事项:

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