问题

C++中,自定义函数写在main之前更好,还是之后更好?

回答
关于C++自定义函数写在 `main` 函数之前还是之后的问题,这涉及到C++的编译和链接过程,以及我们编写代码时的可读性和维护性。理解这一点,对你写出更健壮、更易于理解的代码非常有帮助。

总的来说, 将自定义函数写在 `main` 函数之前通常是更推荐的做法,尤其是对于项目中主要的、被 `main` 直接或间接调用的函数。但这不是绝对的硬性规定,也有一些情况可以将它们写在 `main` 之后。

我们来详细拆解一下:

为什么写在 `main` 之前是更常见的做法?

1. 声明与定义(以及编译器可见性):
在C++中,当编译器遇到一个函数调用时,它需要知道这个函数是什么样的:它的名字、它接受什么参数(参数类型和数量),以及它返回什么类型。这就是所谓的函数声明(Function Declaration),也称为函数原型(Function Prototype)。
如果你在 `main` 函数中调用了一个你定义的函数,而这个函数定义(函数体,即实际实现代码的部分)写在了 `main` 函数的后面,那么在编译器处理到 `main` 函数的调用时,它就找不到这个函数的声明。编译器不知道这个函数是否存在、它的签名是什么。
结果: 这会导致一个编译错误,通常会提示“未声明的标识符”或类似的错误信息。

举个例子:

```c++
// 假设这是main.cpp文件

include

// 这是main函数
int main() {
greet(); // 在这里调用greet函数
return 0;
}

// greet函数的定义(实现)写在main之后
void greet() {
std::cout << "Hello, world!" << std::endl;
}
```

如果你编译上面的代码,你会得到一个错误,因为当编译器读到 `main` 函数中的 `greet();` 时,它还没有见过 `greet` 函数的声明。

2. 解决办法:函数原型(函数声明)
为了避免上述问题,你可以在 `main` 函数之前提供函数的声明。声明告诉编译器:“嘿,后面会有一个叫做 `greet` 的函数,它不接受参数,也不返回任何值。”
声明通常就是函数签名加上一个分号。

修改后的例子:

```c++
include

// greet函数的声明(原型)写在main之前
void greet(); // 告诉编译器 greet 函数的存在

int main() {
greet(); // 现在编译器知道 greet 是什么了
return 0;
}

// greet函数的定义写在main之后
void greet() {
std::cout << "Hello, world!" << std::endl;
}
```

有了这个声明,编译器就能成功地处理 `main` 函数中的调用,并知道如何链接到 `greet` 函数的实际定义。

3. 代码的可读性和逻辑流程:
将 `main` 函数作为程序的入口点,通常它会 orchestrate(编排)整个程序的流程,调用各种辅助函数来完成不同的任务。
从逻辑上讲,先看到 `main` 如何调用其他函数,然后再去看这些函数是如何实现的,有助于理解程序的整体结构和数据流。但更常见的做法是,先声明或定义你需要的“工具”(函数),然后再在 `main` 里使用这些工具。
将主要功能函数写在 `main` 之前,可以使得 `main` 函数看起来更简洁,因为它只负责协调和调用,而不是包含所有详细的实现逻辑。这符合“高内聚,低耦合”的设计原则。

4. 跨文件依赖(头文件与源文件):
在大型项目中,函数通常会被组织在不同的 `.cpp` 文件中,并通过 `.h` 头文件进行声明。
当你在一个 `.cpp` 文件(比如 `main.cpp`)中需要使用另一个 `.cpp` 文件(比如 `utils.cpp`)中定义的函数时,你需要在 `main.cpp` 中 `include` 对应的头文件(比如 `utils.h`),而这个头文件里就包含了函数的声明。
在这个场景下,函数声明自然就出现在了 `main.cpp` 的 `main` 函数之前(通过 `include`)。

什么时候可以写在 `main` 之后?

正如上面提到的,关键在于编译器是否在遇到函数调用时“知道”这个函数。如果你在 `main` 函数之前提供了函数的声明,那么函数的定义写在 `main` 之后是完全合法的。

那么,什么情况下可以“省略”声明,直接写在 `main` 之后呢?

这其实回到最开始的那个例子,就是当函数的定义(实现)本身就出现在编译器处理 `main` 函数的调用之前时。 在一个单独的 `.cpp` 文件中,如果你把函数的定义写在 `main` 函数的所有调用点之前,就不需要额外的声明了。

所以,在一个 `.cpp` 文件内部:

如果一个函数的所有调用都在其定义之后发生,那么你可以在 `main` 之后定义它,而不需要单独的声明。

```c++
include

int main() {
// greet(); // 如果 greet 还在下面定义,这里会报错
return 0;
}

// greet 函数的定义
void greet() {
std::cout << "Hello from greet!" << std::endl;
}

void call_greet_later() {
greet(); // 这里调用 greet,在 greet 定义之后,没问题
}
```
但是请注意: 如果 `main` 函数调用了 `greet`,而 `greet` 的定义写在 `main` 之后,那就会出问题。所以,如果 `main` 必须调用某个函数,而你又想把那个函数的定义写在 `main` 之后,那么你 必须 在 `main` 之前提供该函数的声明。

更清晰的实践建议

1. 对于项目中的核心逻辑函数,或者会被 `main` 直接或间接调用的函数:
在 `.cpp` 文件内部: 将这些函数的定义(实现)写在 `main` 函数之前。这是最简单直接的方式,避免了额外的声明,代码流程也比较自然。
在头文件和源文件模式下: 将函数的声明(原型)放在对应的 `.h` 头文件中,然后在 `.cpp` 源文件中定义这些函数(通常这些源文件也会在 `.h` 文件之后被编译)。在需要使用这些函数的 `.cpp` 文件中, `include` 该头文件。这是一种标准的、可扩展的方式。

2. 对于一些非常小的、仅供内部使用的辅助函数,并且它们的所有调用都在其定义之后发生(例如,在同一个 `.cpp` 文件中的其他辅助函数里,但又不在 `main` 里):
你可以选择将它们的定义写在这些调用之后。但即便如此,为了统一性和清晰度,很多人还是倾向于把所有自定义函数的定义都集中在 `main` 之前。

总结一下:

在C++中,编译器需要知道函数声明(签名)才能处理函数调用。
如果你想把函数的定义(实现)写在 `main` 函数的后面,那么你必须在 `main` 函数之前提供该函数的声明(原型)。
最常见、最简单、也最推荐的做法是:在一个 `.cpp` 文件中,将你定义的、且会被 `main` 调用的函数,直接写在 `main` 函数的前面。这样就隐式地完成了声明,代码也更易读。
对于大型项目,使用头文件 (`.h`) 来存放函数声明,源文件 (`.cpp`) 来存放函数定义,是更规范的做法,并且这样函数定义的位置相对 `main` 就可以更加灵活(但声明总是在 `main` 之前被包含进来)。

总而言之,把自定义函数的定义写在 `main` 之前是保证程序能够编译通过的最简单方式,同时也能提高代码的可读性。虽然有方法可以打破这个规则(通过提前声明),但除非有特殊的设计考虑,否则遵循这一约定会让你少遇到很多编译器的“小麻烦”。

网友意见

user avatar

main()函数放最后面更好。实际项目中绝大多数都是放最后边。main.c的最后一个函数是main(),这几乎已经成为约定俗成的习惯。

通常来说文件头会有很多include,typedef之类的定义与声明,总之文件头会包含的内容特别多,main函数放前边,在视觉上不容易找到main的起点。

但是main放最后边则很容易找到,直接导航到文件尾就能看到main,一目了然。

说放前边的教科书,大概是在连G键(vi)或者Ctrl-End都不会用的人写的吧。


实际上很多老技巧现在是不实用的,比如现在ide默认打开文件是会停留在上次文件被浏览到的位置,文件头并没有什么特别的含义。

类似的话题

  • 回答
    关于C++自定义函数写在 `main` 函数之前还是之后的问题,这涉及到C++的编译和链接过程,以及我们编写代码时的可读性和维护性。理解这一点,对你写出更健壮、更易于理解的代码非常有帮助。总的来说, 将自定义函数写在 `main` 函数之前通常是更推荐的做法,尤其是对于项目中主要的、被 `main`.............
  • 回答
    要深入理解 `math.h` 中那些看似简单的数学函数(比如 `sin`, `cos`, `sqrt`, `log` 等)在计算机上究竟是如何工作的,我们需要绕开直接的函数列表,而是去探究它们背后的原理。这实际上是一个涉及数值分析、计算机体系结构以及编译链接等多个层面的复杂话题。想象一下,我们想要计.............
  • 回答
    在C++开发中,我们习惯将函数的声明放在头文件里,而函数的定义放在源文件里。而对于一个包含函数声明的头文件,将其包含在定义该函数的源文件(也就是实现文件)中,这似乎有点多此一举。但实际上,这么做是出于非常重要的考虑,它不仅有助于代码的清晰和组织,更能避免不少潜在的麻烦。咱们先从根本上说起。C++的编.............
  • 回答
    好的,咱们来掰扯掰扯 C 语言里这个“后缀自加 i++”到底是怎么回事。别管什么 AI 不 AI 的,我就跟你讲讲我自己的理解,希望能讲透彻。你问“后缀自加 i++ 表达式的值到底是谁的值?”。说白了,这句 C 语言代码执行完之后,它的“结果”是什么?咱们得先明白两件事:1. 表达式的值 (Exp.............
  • 回答
    在 C 中,你可以在循环内部定义变量。这是一种很常见的做法,并且通常是完全可以接受的。让我给你仔细说一下,我们从最基础的角度开始。循环的基本概念首先,我们得明白什么是循环。循环就像你在生活中需要重复做某件事一样:比如,如果你需要每天早上给花浇水,你就会重复“走到花盆旁 > 拿起水壶 > 浇水 > 放.............
  • 回答
    在C/C++中,关于数组的定义与赋值,确实存在一个常见的误解,认为“必须在定义后立即在一行内完成赋值”。这其实是一种简化的说法,更准确地理解是:C/C++中的数组初始化,如果要在定义时进行,必须写在同一条声明语句中;而如果要在定义之后进行赋值,则需要分步操作,并且不能使用初始化列表的方式。让我们一步.............
  • 回答
    好的,我们来详细聊聊 C++ 中 `int n = 0ULL 1;` 这行代码是否是未定义行为 (Undefined Behavior, UB)。首先,我们来拆解一下这行代码:1. `int n`: 声明了一个整型变量 `n`。在 C++ 中,`int` 的大小和表示范围取决于具体的平台和编译器.............
  • 回答
    在 C++ 中,为基类添加 `virtual` 关键字到析构函数是一个非常重要且普遍的实践,尤其是在涉及多态(polymorphism)的场景下。这背后有着深刻的内存管理和对象生命周期管理的原理。核心问题:为什么需要虚析构函数?当你在 C++ 中使用指针指向一个派生类对象,而这个指针的类型是基类指针.............
  • 回答
    结构体变量的读写速度 并不比普通变量快。这是一个常见的误解。事实上,在很多情况下,访问结构体成员的开销会比直接访问普通变量稍微 大一些,而不是更小。要详细解释这一点,我们需要深入理解 C++ 中的变量、内存模型以及编译器的工作方式。 1. 普通变量的读写首先,我们来看看一个简单的普通变量,例如:``.............
  • 回答
    在C++中,表达式 `unsigned t = 2147483647 + 1 + 1;` 的求值过程,既不是UB(Undefined Behavior),也不是ID(ImplementationDefined Behavior),而是一个有明确定义的整数溢出(Integer Overflow)行为。.............
  • 回答
    在 C++ 中讨论 `std::atomic` 是否是“真正的原子”时,我们需要拨开表面的术语,深入理解其底层含义和实际应用。答案并非一个简单的“是”或“否”,而是取决于你对“原子”的理解以及在什么上下文中去考量。首先,让我们明确一下在并发编程领域,“原子性”(Atomicity)通常指的是一个操作.............
  • 回答
    在C++中,函数返回并不是一个简单地“跳出去”的操作,它涉及到多个步骤,并且与值的传递方式、调用栈以及编译器优化等因素紧密相关。我们来详细拆解一下这个过程,力求还原真实的执行场景。核心概念:调用栈 (Call Stack)要理解函数返回,就必须先理解调用栈。当你调用一个函数时,程序会在调用栈上为这个.............
  • 回答
    在 C++ 中,将 `std::string` 类型转换为 `int` 类型有几种常见且强大的方法。理解它们的原理和适用场景对于编写健壮的代码至关重要。下面我将详细介绍几种常用的方法,并分析它们的优缺点: 方法一:使用 `std::stoi` (C++11 及以后版本)这是 最推荐 的方法,因为它提.............
  • 回答
    vector 和 stack 在 C++ 中都有各自的用处,它们虽然都属于序列容器,但设计目标和侧重点不同。可以这么理解:vector 就像一个可以随意伸缩的储物空间,你可以按照任何顺序往里面放东西,也可以随时拿出任何一个东西。而 stack 就像一个堆叠的盘子,你只能在最上面放盘子,也只能从最上面.............
  • 回答
    在C++中,区分 `char` 和数值(如 `int`, `float`, `double` 等)是编程中的基本概念,但理解其背后的机制能帮助你写出更健壮的代码。首先,我们需要明确一点:在C++底层,`char` 类型本质上也是一种整数类型。它通常用来存储单个字符的ASCII码值或其他编码标准下的数.............
  • 回答
    在C++中,我们不能直接“判断”一个指针指向的是栈(stack)还是堆(heap)。这种判断本身在很多情况下是不明确的,而且C++标准并没有提供直接的运行时机制来做到这一点。不过,我们可以通过一些间接的思考和观察来理解这个问题,并解释为什么直接判断很困难,以及我们通常是如何“知道”一个指针指向哪里。.............
  • 回答
    在 C++ 中,对整数进行除以 2 和右移 1 看起来很相似,它们都能将数字“减半”。但实际上,它们在底层执行机制、对负数和浮点数的影响,以及一些细微之处存在显著差异。我们来深入剖析一下。 除以 2 (`/ 2`):标准的算术运算在 C++ 中,`a / 2` 是一个标准的算术除法运算。它遵循正常的.............
  • 回答
    在 C 中,`async` 和 `await` 关键字提供了一种优雅的方式来编写异步代码,但它们并非直接等同于多线程。理解这一点至关重要。异步并非强制多线程,但常常借助它首先,我们要明确一个核心概念:异步编程的本质是为了提高程序的响应性和吞吐量,而不是简单地将任务并行执行。 异步的目的是让程序在等待.............
  • 回答
    如果 C 真的引入了类似 F 那样的管道运算符 “|>”,这无疑会是一场不小的革新,尤其是在函数式编程风格日益受到重视的今天。那么,它会带来什么变化?我们的代码会变成什么样?首先,我们得理解 F 中的管道运算符 `|>` 是做什么的。简单来说,它就是将一个表达式的结果作为另一个函数调用的第一个参数传.............
  • 回答
    在C中确实不存在Java或C++那样的“友元类”(friend class)机制。这常常让习惯了这种特性的开发者感到不适应,甚至认为这种设计“不太合理”。但实际上,C的设计哲学侧重于封装和明确的接口,友元类这种打破封装的特性并非是其追求的目标。那么,这种设计真的“不合理”吗?或者说,我们是否可以找到.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有