【问题标题】:Why we should not include source files in C为什么我们不应该在 C 中包含源文件
【发布时间】:2015-09-09 05:19:24
【问题描述】:

为什么将源文件包含到其他源文件中不是一个好习惯? 更好的方法是包含头文件。这种方法有什么好处,反之有什么缺点? 原谅我的英语不好。

【问题讨论】:

  • 这样做没有意义。编译器将项目中的每个源文件作为一个独立的编译单元。一旦完成对给定单元的编译,它就会“忘记”所有先前的定义(宏、函数原型等)。如果在源文件中包含源文件,前者会被编译两次,其中所有的全局符号(函数和全局变量)都会被实例化两次,导致链接错误。
  • #include源代码文件!虽然不是很合适,here 您可能会找到一个解释,为什么在标题中包含 definitions 是不好的做法(有一些例外)。
  • @barakmanos:混淆“源代码文件”和“输入文件”?请注意,该标准指的是“文件范围”,即整个编译单元,包括任何#included 文件。另请注意,实际上包含定义的好用例:例如,您要保留static的自动生成的表。或者 - 即使是普通的标题:inline 函数。
  • @Olaf:顺便说一句,对于那些自动生成的表格,您可以使用extern 而不是包含源文件。
  • @Olaf:如果您声明自动生成的表 static 并在多个位置包含源文件,那么您会创建该表的多个副本。这可能会非常浪费,尤其是在您刚刚提到的嵌入式系统中。

标签: c code-organization


【解决方案1】:

对于预处理器来说,文件的扩展名并不重要。您可以将代码放入具有“JPG”扩展名的文件中,并且只要代码合法,您仍然可以#include 它而不会出错。

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

您现在必须指定将哪些文件构建为单独的编译单元(目标文件),以单独编译并链接以形成生成的二进制文件。如果您的代码库习惯于使用预处理器来包含具有源文件扩展名的文件,那么您不知道只查看文件扩展名哪些文件将作为单独的编译单元构建,哪些文件实际上只是被包含在内由预处理器。因此,您可能会面临大量错误,只是试图像理智的人那样将所有源文件构建为单独的编译单元,并且可能不得不在检查所有代码并试图弄清楚时使用细齿梳调试您的构建过程找出哪个文件的用途。

在更高级别上,除了文件扩展名之外,如果您实际上在源文件中定义内容并将它们包含在预处理器中,那么您将面临相同符号的冗余链接器定义、棘手的链接时(可能还有编译时)错误.此外,这可能会在接口/声明(标头)和实现/定义(源)之间的分离方面表现出一般的思考。

有一些例外情况,例如统一构建,它作为构建时优化执行此操作,并且通过仔细的编码标准和对实践的实际、可衡量的好处可能在某种程度上是可以接受的,但总的来说,包括源文件可能真的很混乱,并且表明开发人员并不真正理解将声明与定义分开的意义,或者在尝试建立构建系统时可能导致的混乱。

【讨论】:

    【解决方案2】:

    为什么将源文件包含到其他源中不是一个好习惯 文件?

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

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

    例如:

    文件a.c

    int a = 42;
    ...
    

    文件b.c

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

    当你编译 a.cb.c 并链接它们时,你会得到 multiple definition 链接器错误。

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


    附言。当我说一般时,我的意思是有时包含源代码可能很有用。但在这种情况下,为了避免给读者造成混淆,我更愿意将文件后缀重命名为 .c 以外的其他名称,可能是 .inc 或类似名称。

    【讨论】:

    • 头文件也应该用#ifndef...#endif 保护,因此如果其中定义了符号,如果包含在多个文件中,它们将不会产生链接器错误。在头文件中声明符号有时很有用,但应该避免。文件扩展名只是给开发者的信息,不需要.h扩展名,众所周知的例子是像'iostream'这样没有扩展名的文件。
    • @riodoro1 在头文件中定义 symbols 不是一个好主意,并且包含保护不是为了那个,它是为了防止包含相同的文件两次,定义一个 @987654329 @ 例如,如果您将文件包含两次,则会出现问题。
    • @riodoro1 不,它们仍然会导致多重定义错误,因为编译单元在编译时彼此完全隔离。
    • @MohitJain 你真的改进了你的答案,+1。
    【解决方案3】:

    编译任何文件#included,就好像它的文本确实替换了处理器的相应#include 指令。虽然不完全合适,但您可以在此处找到有关此here 的更多信息。注意预处理器守卫。

    实际的问题是,你不应该在这样的标题中放入什么。这将是每个想法都会使具有外部链接的名称在多个编译单元/模块中定义。您也不应该将仅在一个此类模块中使用和/或应对其他模块隐藏的对象放在这里。

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

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

    【讨论】:

      【解决方案4】:

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

      当然,在非常特殊的情况下,包含 C 文件可能是两害相权取其轻。例如,如果您想为静态 c 函数编写单元测试,您可以将 C 文件包含在包含单元测试的文件中。

      另请参阅:How to test a static function

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

      【讨论】:

        【解决方案5】:

        c 源文件必须有定义,例如,如果你有一个函数int add(int, int) 将两个数字相加,那么它的定义应该是这样的

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

        一个头文件,包含一个prototype,它可以帮助编译器在你的代码中调用这个函数,它告诉编译器如何为函数创建堆栈帧,有多少个参数及其参数类型和返回类型。

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

        而是将原型添加到头文件中,如下所示

        int add(int x, int y);
        

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

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

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

        【讨论】:

        • 我总是想知道为什么人们会忘记inline 函数或自动生成的static 表。您不想每次都将它们添加到您的实现文件
        猜你喜欢
        • 2011-05-06
        • 1970-01-01
        • 2014-01-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-06-15
        • 1970-01-01
        相关资源
        最近更新 更多