C++中的int main()需要声明吗?

66

我曾被教导函数需要进行声明才能调用。为了说明这一点,以下例子会出现错误,因为函数sum没有进行声明:

#include <iostream>

int main() {
  std::cout << "The result is " << sum(1, 2);
  return 0;
}

int sum(int x, int y) {
  return x + y;
}

// main.cpp:4:36: error: use of undeclared identifier 'sum'
//  std::cout << "The result is " << sum(1, 2);
//                                   ^
// 1 error generated.

为了解决这个问题,我会添加以下声明:
#include <iostream>

int sum(int x, int y); // declaration

int main() {
  std::cout << "The result is " << sum(1, 2);
  return 0;
}

int sum(int x, int y) {
  return x + y;
}

为什么与其他函数不同,main 函数不需要声明就能使用?

18
手动调用主函数会导致未定义行为。 - George
23
在C语言中,允许调用main函数。但在C++中不行,因为它不仅仅是一个普通的函数,而是特殊的。历史上,编译器会在main函数中添加代码来初始化需要动态初始化的全局变量;如果在程序内部调用main函数,将会重新初始化这些变量,结果会是混乱的。 - Pete Becker
26
“你尝试过某事并发现它‘完全正常运作’并不意味着这个事情就不是未定义行为。” - Cody Gray
7
顺带一提,如果您将“sum”的定义放在文件的main函数之上,则不需要为“sum”进行声明。因此,在C和C++源代码中,通常会将main函数放在最后一个位置,这样您就不需要为该文件中定义的其他函数进行前向声明。与C#和Java不同,它们通常会将main函数放在第一位,尽管在这些情况下并不要求这样做。 - Cody
10
从技术上讲,你的示例代码已经声明了main,函数的定义也声明了该函数。这就是为什么可以将sum移到main之前,以避免需要单独声明sum的原因。 - Ross Ridge
显示剩余5条评论
7个回答

64
一个函数的定义也是对该函数的声明。
声明一个函数的目的是让编译器知道它的存在。如果声明了一个函数但不定义它,可以在使用该函数时方便地调用它。例如:
  • 如果在一个源文件(A)中使用了定义在另一个文件(B)中的函数,则需要在A中声明该函数(通常是通过头文件B.h来实现的)。
  • 如果两个或多个函数可能互相调用,则不能定义所有这些函数。其中一个必须先定义。因此,可以首先提供声明,然后再提供定义。
  • 许多人喜欢将“高级”例程放在源文件的前面,将子例程放在后面。由于这些“高级”例程调用各种子例程,所以必须先声明这些子例程。
在C++中,用户程序从不调用main()函数,因此不需要在定义之前进行声明。(请注意,如果您愿意,可以提供一个声明。在这方面,main()函数的声明并没有什么特别之处。) 在C语言中,程序可以调用main()函数。在这种情况下,需要在调用之前可见一个声明。请注意,调用main的代码确实需要知道它。这是在通常称为C++运行时启动代码中的特殊代码。链接器会自动为您包含该代码,当您使用适当的链接器选项链接C++程序时。无论该代码使用什么语言编写,它都具有调用main所需的任何声明,以便正确调用它。

我认为这是目前最完整和正确的答案。很遗憾由于文本过多,它不会变得更受欢迎。您能否在开头添加一些tl;dr?此外,我认为,C++编译器以这样的顺序解析代码可能并不明显。其他语言通过先扫描声明再扫描定义来解决此问题。C ++仅在类体中解决了这个问题。 - pkubik

42
我学过函数需要声明才能被调用。
确实如此。一个函数在被调用之前必须要有声明。
那为什么我们不为main函数添加一个声明呢?
嗯,你没有调用main函数。实际上,你根本不应该调用main1,所以永远不需要在任何地方声明main。
严格来说,所有的定义也都是声明,因此你对main的定义同时也声明了它。
注脚1:C++标准规定从程序内部调用main是未定义行为。
这使得C++的实现可以在main的顶部放置一些特殊的仅运行一次的启动代码,如果它们不能够从启动代码的钩子中更早地调用main的话。事实上,一些真实的实现确实这样做,例如调用一个快速数学函数来设置一些FPU标志,比如将非规范化数字设为零。
在假想的实现中,调用main可能会导致像重新运行所有静态变量的构造函数、重新初始化由new/delete使用的数据结构以跟踪分配或其他程序完全崩溃的有趣结果。或者它可能根本没有任何问题。未定义行为并不意味着它必须在每个实现中都失败。

35

如果您想调用函数,那么原型是必须的,但像您的情况下的sum一样,它还没有可用的原型。

不要自己调用main,因此没有必要拥有原型。甚至编写原型都是一个坏主意。


调用main函数不一定是一个“坏主意”。C语言允许这样做;C++则将其定义为未定义行为,这与它是否是一个坏主意无关。 - Kaz
9
@Kaz 这是一个不好的想法去做那些行为没有定义的事情。 - eerorika
@eeroika 这是一个循环论证。递归的main早已定义好了。答案说不仅你不能这样做,而且这甚至是个坏主意。这意味着它之所以是个坏主意,除了被禁止之外还有其他原因,或者可能是因为它是个坏主意而被禁止,但事实并非如此。这只是C语言的一个特性,C++方言未能实现。 - Kaz
1
C++编译器可以将翻译后的main映像作为extern "C"链接发出。或者完全替换其名称,例如__main等。然而,在编译main时,它也可以忽略这些考虑,并将其视为另一个函数,以便以普通方式声明main符号。对main的递归调用可能希望调用名为main的C++函数,具有普通的C++链接,支持重载等,但由于特殊处理,翻译中可能根本没有这样的符号。 - Kaz
1
@MatthieuBrucher 啊,好的;我误读了。原型在C++中可能没有任何有用的作用。 - Kaz
是的,这可能是因为我应该更清楚地说明了“坏主意”所指的内容。 - Matthieu Brucher

27

不需要对main()进行前向声明。

main()是C++中的一个特殊函数。

关于main()的一些重要事项:

  1. 链接器在创建可执行程序时要求存在且仅存在一个main()函数。
  2. 编译器期望main()函数采用以下两种形式之一:
int main () { /* body */ } 
int main (int argc, char *argv[]) { /* body */ } 

其中body为零个或多个语句。

另一个可接受的形式是实现特定的,它提供了在调用函数时环境变量列表:

int main (int argc, char* argv[], char *envp[]) { /* body */ }

程序员必须使用其中一种可接受的形式提供main的“定义”,但程序员不需要提供声明。编写的定义被编译器接受为main()的声明。如果没有提供返回语句,则编译器将在函数体中作为最后一条语句提供return 0;。顺便说一下,有时会对C++程序是否可以调用main()产生困惑。这是不推荐的。C++17草案规定,main()“不得在程序内使用”。换句话说,不能从程序内部调用它。请参见《C++编程语言工作草案标准》(日期为“2017-03-21”),第6.6.1.3段,第66页。我知道有些编译器支持这种行为(包括我的),但下一个版本的编译器可能会修改或删除该行为,因为标准使用了“不得”的术语。

还要注意的是,标准允许除了你在这里列出的两个之外的另一种实现定义的main签名。一个常见的选项是添加第三个参数(在argv之后),其中包含环境变量(使用与extern char ** environ相同的方法)。 - SJL
@SJL:当然!我只列出了编译器“必须”实现的内容。environ也非常有用。 - Gardener
8
编译器不需要对main()进行声明。每个定义都是一个声明,因此我认为措辞需要调整。"编译器将其声明为以下两个函数之一"。为什么是"编译器声明"?我们总是自己提供main的定义。 - HolyBlackCat
1
@HolyBlackCat:我理解你的观点。措辞很重要。即使我更改了它,没有引用整个标准,也不会有完整的答案。这个答案的目的是简单明了。看看你对这个更新的看法如何。 - Gardener
6
关于(前向)声明,主函数没有什么特殊之处,这只是一个干扰。相反,我们不需要提前声明它,因为在定义之前我们没有引用它。就是这样。 - Konrad Rudolph

10

在程序内部调用 main 是非法的。这意味着唯一可以调用它的是运行时,而编译器/链接器可以处理设置。这意味着您不需要 main 的原型。


7

函数的定义也隐含了其声明。如果你需要在定义之前引用一个函数,你需要在使用它之前对其进行声明。

因此,以下写法也是有效的:

int sum(int x, int y) {
  return x + y;
}

int main() {
  std::cout << "The result is " << sum(1, 2);
  return 0;
}

如果您在一个文件中使用声明使函数在定义之前为编译器所知,则在链接时必须知道其定义: main.cpp
int sum(int x, int y);

int main() {
  std::cout << "The result is " << sum(1, 2);
  return 0;
}

sum.cpp

int sum(int x, int y) {
  return x + y;
}

或者 sum 可能源于一个库,因此您甚至不需要自己编译它。

您的代码中没有使用/引用 main 函数,因此无需在任何地方添加 main 的声明。

在您的 main 函数之前和之后,c++ 库可能会执行一些初始化和清理步骤,并调用您的 main 函数。如果该库的部分被表示为 c++ 代码,则它将包含 int main() 的声明,以便可以编译该代码。该代码可能看起来像这样:

int main();

int __main() {
  __startup_runtime();

  main();

  __cleanup_runtime();
}

然而,你再次遇到了与__main__相同的问题,所以在某个时刻就不再有c++存在,某个函数 (main) 只是代表你的代码的入口点。


C++规定在程序内部调用main是未定义行为,因此C++编译器可以将启动/清理调用直接放入真正的main中。这个规则使得C++编译器可以在C环境上工作,例如,如果没有其他机制可用,可以从中调用静态构造函数。 (编译器还必须将main识别为特殊的函数名称,以给它一个隐式的return 0。) - Peter Cordes
从程序员的角度来看,根据标准调用main函数是未定义行为。但编译器或操作系统如何处理main取决于实现。因此理论上,main的编译结果可能看起来像是由运行时调用的常规函数,也可能不存在,正如你所说,编译器可以将这些启动/清理调用放在应用程序的入口点周围的代码中。 - t.niese
是的,在大多数实现中,它只是一个普通函数(但具有隐式的 extern "C" 以避免对其进行 C++ 名称修饰,因此 CRT 启动代码可以链接到它,而不管函数签名如何),真正的初始化工作是在 CRT 代码和/或动态链接器钩子中完成的。但就像我评论 Joshua 的答案一样,ICC(英特尔编译器)实际上确实在 main 内部添加了一些启动代码(https://godbolt.org/z/oWlmlc),包括设置 DAZ 和 FTZ 以禁用子规范数,以适应其默认的 -ffast-math。gcc/clang 为快速数学运算或非快速数学运算链接不同的 CRT 启动文件。 - Peter Cordes

5

不行。无论如何都不能调用它。

只有在调用函数之前需要前向声明,对于在其他文件中定义的函数需要外部声明(外部声明与前向声明看起来完全相同)。

但是在C++中你不能调用main,所以你不需要一个。这是因为C++编译器允许修改main以进行全局初始化。

[我查看了crt0.c,它确实有一个main声明,但这并不重要]。


你可以调用 main,但这通常是不好的做法。 - Cruz Jean
8
@CruzJean 不仅是一种不好的做法,就我所知,它还是未定义的行为。 - Guillaume Racicot
5
不错的练习。调用它会引发未定义行为。 - Algirdas Preidžius
@AlgirdasPreidžius 啊,我改正了。从来不知道这件事。 - Cruz Jean
@NathanOliver:请看我对@eerorika的回答所做的修改。请记住,C++编译器会发出汇编代码,而不是更多的C代码。它可以在main的顶部放置对初始化函数的调用,以便在main中的任何内容之前发生。事实上,现代ICC(英特尔的x86编译器)仍然这样做:在https://godbolt.org/z/oWlmlc中,请注意`call __intel_new_feature_proc_init`(无论那是什么)并将SIMD FPU设置为将非规格化数视为零(在MXCSR控制寄存器中设置DAZ和FTZ)。 - Peter Cordes
显示剩余3条评论

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