为什么我们不应该在C中包含源文件

5
为什么将源文件包含在其他源文件中不是一种好的做法?更好的方法是包含头文件。采用这种方式有哪些好处,反之又有哪些缺点呢?请原谅我的英语不好。

3
这样做没有意义。编译器将项目中的每个源文件视为独立的编译单元。一旦完成编译给定的单元,它就会“忘记”所有先前的定义(宏、函数原型等)。如果您在一个源文件中包含另一个源文件,则前者将被编译两次,并且其中的所有全局符号(函数和全局变量)都将被实例化两次,从而导致链接错误。 - barak manos
1
混淆“源代码文件”和“输入文件”?请注意,标准指的是“文件作用域”,这意味着整个编译单元,包括任何#include的文件。同时也要注意:实际上确实存在一些好的用例需要包含定义:例如,自动生成的表格,你想要保持static。或者即使是普通的头文件也可以使用 inline函数。 - too honest for this site
@Olaf:我同意自动生成(通常很大)数组的问题,但由于这不是常见情况,我假设OP指的是“正常”的开发过程,在这个过程中,您需要“手动”创建并将代码添加到一个或多个源文件中。 - barak manos
1
@Olaf:顺便说一下,对于那些自动生成的表格,你可以使用“extern”而不是包含源文件。 - barak manos
1
@Olaf:如果您将自动生成的表声明为static并在多个位置包含源文件,则会创建该表的多个副本。这可能会非常浪费,特别是在您刚提到的嵌入式系统中。 - barak manos
显示剩余4条评论
5个回答

5
如前所述,将C文件包含到C文件中的主要争议在于高风险的重复定义错误。而且,由于这是一种很少使用的技术,它会给代码维护者带来意想不到的副作用。
当然,在非常特殊的情况下,包含C文件可能是两害相权取其轻的选择。例如,如果你想为静态c函数编写单元测试,你可以将C文件包含到具有单元测试的文件中。
参见:如何测试静态函数 另一个不寻常但有效的用法是将类或函数模板与其定义分离(C ++):https://isocpp.org/wiki/faq/templates#separate-template-fn-defn-from-decl

3
为什么把源文件包含到其他源文件中不是好的实践方法?
源文件包含定义。这可能导致多次定义错误,因此通常不应该包含在其他源文件中。即使您通过编译只包含其他源文件的文件来避免多次定义错误,代码也可能变得难以管理。
在头文件中,您只需向编译器引入一些符号并告知它们的类型。这使您可以将接口与实现分离。
例如:
文件 a.c
int a = 42;
...

file b.c

/* Example of bad code */
#include "a.c"
...

当您编译a.cb.c并将它们链接起来时,您将会得到multiple definition链接错误。
如果计划将多个源文件包含到一个文件中并编译该文件,则会引入大量污染(宏、静态函数等),这对读者和编译器都不是很可管理的事情。

附言:当我说一般情况时,我指的是有时包含源代码可能是有用的。但在这种情况下,为避免给读者带来困惑,我更喜欢将文件后缀重命名为.inc或类似的其他名称。


1
头文件也应该使用#ifndef...#endif进行保护,这样如果它们中定义了符号,则在包含在多个文件中时不会产生链接器错误。在头文件中声明符号有时很有用,但应该避免使用。 文件扩展名只是开发人员的信息,没有必要使用.h扩展名,非常著名的例子是像'iostream'这样没有扩展名的文件。 - riodoro1
2
@riodoro1 在头文件中定义符号不是一个好主意,而且 include guard 也不是为此而设计的,它的作用是防止重复包含同一文件。例如,如果您两次包含该文件,则定义结构将会出现问题。 - Iharob Al Asimi
@riodoro1 不,它们仍会导致多重定义错误,因为编译单元在编译时是完全隔离的。 - Mohit Jain
@MohitJain,你的回答真的提高了许多,加1分。 - Iharob Al Asimi

3
对于预处理器来说,文件的扩展名并不重要。你可以把代码放到一个带有“JPG”扩展名的文件中,只要代码合法,就可以无误地进行#include操作。
从基本的构建/生成角度考虑,传统上认为使用源文件扩展名#include文件是一种不好的做法之一。想象一下,你正在将一个大型项目移植到一个新的跨平台构建系统中(比如5000万行代码)。
现在,你必须指定哪些文件将作为单独的编译单元(目标文件)进行编译,然后链接以形成最终的二进制文件。如果你的代码库习惯于使用预处理器来包含具有源文件扩展名的文件,那么仅仅通过查看文件扩展名,你就无法知道哪些文件将作为单独的编译单元进行构建,哪些文件实际上只是由预处理器包含的文件。所以,你可能会面临大量错误,尝试将所有源文件作为单独的编译单元构建,而理智的人则会这样做,并且可能需要使用细齿梳调试构建过程,检查所有的代码并尝试弄清楚哪个文件用于什么目的。
在更高层次上,超越了文件扩展名,如果你在源文件中定义了东西并使用预处理器进行包含,那么你就会面临同一符号的冗余链接器定义、棘手的链接时间(可能还有编译时间)错误的风险。此外,这可能会导致在接口/声明(头文件)和实现/定义(源文件)之间思考分离的普遍崩溃。
当然也有例外,比如统一构建(unity builds),这是一种构建时优化的方法,通过谨慎的编码标准以及实际的、可衡量的好处,可能是可以接受的。但总的来说,包含源文件可能会非常令人困惑,并表明开发者并没有真正理解将声明与定义分离的重要性,或者在建立构建系统时可能会造成的混乱。

2

这些C语言源代码文件必须有定义。例如,如果您有一个函数int add(int, int)用于将两个数字相加,则其定义应如下:

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

一个头文件包含一个原型,它帮助编译器在代码中调用该函数时调用此函数,它告诉编译器如何为函数创建堆栈帧、参数数量和类型以及返回类型。

如果您包含一个包含上面代码示例的c源文件,则需要两个add()函数的定义,这是不可能的。

相反,您可以将原型添加到头文件中,如下所示:

int add(int x, int y);

然后包含头文件,这样add()函数就会有一个单一的定义。

你可能会问自己,如果我在另一个c源文件中使用它而没有提供定义,该函数将如何工作?

答案是,只有在编译器将所有对象文件链接成最终二进制文件时才需要函数定义。


我一直想知道为什么人们会忘记 inline 函数或自动生成的 static 表。你不需要每次都将它们添加到你的实现文件中。 - too honest for this site

2
任何被#include的文件都会被编译器视为其文本已替换相应的#include指令。尽管不完全相关,您可以在此处找到更多信息here,请注意预处理器保护。
实际问题是,您不应该将具有外部链接的名称定义放入此类头文件中,这样会使它们在多个编译单元/模块中被定义。您也不应该在此处放置仅在一个这样的模块中使用且/或应从其他模块中隐藏的对象。
这将包括一般函数:头文件仅提供声明定义将在单个模块中完成。例外是inline函数,它们实际上必须在头文件中定义。
对于数据结构,大多数情况下,与函数相同。但是,可能会有static结构的例外,所有模块都必须提供它们。另一个例外可能是自动生成的文件,例如仅由单个模块使用的表格。这些也应该被#include,但声明为static。通常,对于这些文件,人们会使用不同的扩展名,例如.inc而不是.h

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