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

时间:2015-06-23 12:02:32

标签: c code-organization

为什么将源文件包含在其他源文件中并不是一个好习惯? 更好的方法是包含头文件。这种方法有什么好处,反之亦然? 请原谅我糟糕的英语。

5 个答案:

答案 0 :(得分:3)

  

为什么将源文件包含到其他源中并不是一种好的做法   文件?

源文件包含定义。这些可能会导致多个定义错误,因此通常不应包含在其他源文件中。即使您通过仅编译包含其他源文件的文件来避免多个定义错误,代码也会变得无法管理。

在头文件中,您只需向编译器引入一些符号并通知其类型。这允许您将接口与实现分开。

例如:

文件 a.c

int a = 42;
...

档案 b.c

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

当您编译a.cb.c并链接它们时,您将收到multiple definition链接器错误。

如果计划将多个源文件包含在一个文件中并编译该文件,则会引入大量污染(宏,静态函数等),这对于读者和编译器来说是不易管理的。

P.S。当我说一般时,我的意思是有时候包括源代码可能会有用。但在这种情况下,为了避免与读者产生混淆,我宁愿将文件后缀重命名为.c以外的某些内容,可能是.inc或类似。

答案 1 :(得分:3)

如前所述,反对将C文件包含到C文件中的主要论点是多重定义错误的高风险。由于它是一种非常少用的技术,它会给代码维护者带来意想不到的副作用。

当然,在非常特殊的情况下,包括C-File可能是两个邪恶中的较小者。例如,如果要为静态c函数编写单元测试,可以使用单元测试将C文件包含到文件中。

另见:How to test a static function

另一个不寻常但有效的用法是将类或函数模板与其定义(C ++)分离: https://isocpp.org/wiki/faq/templates#separate-template-fn-defn-from-decl

答案 2 :(得分:3)

编译任何文件#include d,好像其文本确实替换了 pre 处理器的相应#include指令。虽然不是很合适,但在这里您可以找到有关此here的更多信息。注意预处理器保护。

实际的问题是,你不应该把这个标题放进去。这将是使得具有外部链接的名称在多个编译单元 /模块中定义的所有思考。您也不应该在此处放置对象,这些对象仅在一个此类模块中使用,和/或应对其他模块隐藏。

这将包括一般的功能:标题仅提供声明定义将在单个模块中。例外inline函数,实际上必须在标题中定义。

对于数据结构,大多数情况下,同样适用于函数。但是,static结构可能存在例外情况,这些结构必须由所有模块提供。另一个例外可能是自动生成的文件,例如表格仅由单个模块使用。这些也应该是#include d,但声明为static。通常,人们会对这些文件使用不同的扩展名,例如.inc代替.h

答案 3 :(得分:3)

对于预处理器,文件的扩展名并不重要。您可以将代码放入带有" JPG"的文件中。如果代码合法,你仍然可以在没有错误的情况下#include它。

传统上,对于具有源文件扩展名的#include文件,它被认为是不良做法的原因之一是从基本构建/制作角度来看。想象一下,您正在将一个大型项目移植到一个新的跨平台构建系统(例如,5000万行代码)。

现在,您必须指定要将哪些文件构建为单独编译单元(目标文件),以便单独编译并链接以形成生成的二进制文件。如果您的代码库习惯使用预处理器来包含具有源文件扩展名的文件,那么您根本不知道只查看文件扩展名哪些文件将作为单独的编译单元构建,哪些文件实际上只是包含在内由预处理器。那么你可能会面临一个错误的垃圾邮件,只是试图将所有源文件构建为单独的编译单元作为一个理智的人,并且可能必须使用细齿梳调试您的构建过程,同时检查所有代码并试图计算哪个文件适合什么。

在更高级别,超出文件扩展名,如果您实际在源文件中定义事物并将它们包含在预处理器中,那么您将面临相同符号的冗余链接器定义,即棘手的链接时间(以及可能的编译时)错误。此外,这可能会在界面/声明(标题)与实现/定义(来源)分离之间出现一般性的分解。

有一些例外情况,例如统一构建,它可以将其作为构建时优化,并且可以通过仔细的编码标准和实际的实际测量好处来接受,但一般来说,包括源文件可能会让人感到困惑和表示开发人员并不真正理解将声明与定义分开或者在尝试建立构建系统时可能导致的混淆。

答案 4 :(得分: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源文件中使用它而不提供定义,该函数将如何工作?

答案是只有当编译器将所有目标文件链接到最终二进制文件时才需要函数定义。