28

我习惯于在一个 C 文件中完成所有编码。但是,我正在从事一个足够大的项目,以至于这样做变得不切实际。我一直将它们#include 在一起,但我遇到过我多次#include 某些文件的情况,等等。我听说过 .h 文件,但我不确定它们的功能是什么(或者为什么有 2 个文件比 1) 好。

我应该使用什么策略来组织我的代码?是否可以将特定文件的“公共”功能与“私人”功能分开?

这个问题引发了我的询问。tea.h 文件不引用tea.c 文件。编译器是否“知道”每个 .h 文件都有对应的 .c 文件?

4

8 回答 8

36

您应该将 .h 文件视为.c 文件的接口文件。每个 .c 文件都代表一个具有一定功能的模块。如果.c 文件中的函数被其他模块(即其他.c 文件)使用,则将函数原型放在.h 接口文件中。通过将接口文件包含在您的原始模块 .c 文件和您需要该函数的每个其他 .c 文件中,您可以使该函数可用于其他模块。

如果您只需要某个 .c 文件(而不是任何其他模块)中的函数,请将其范围声明为静态。这意味着它只能从定义它的 c 文件中调用。

跨多个模块使用的变量也是如此。它们应该放在头文件中,并且必须用关键字“extern”标记。注意:对于函数,关键字“extern”是可选的。函数始终被视为“外部”。

头文件中的包含保护有助于避免多次包含同一个头文件。

例如:

模块1.c:

    #include“模块1.h”

    静态无效 MyLocalFunction(void);
    静态无符号整数 MyLocalVariable;    
    无符号整数 MyExternVariable;

    无效我的外部函数(无效)
    {
        MyLocalVariable = 1u;       

        /* 做点什么 */

        MyLocalFunction();
    }

    静态无效 MyLocalFunction(无效)
    {
      /* 做点什么 */

      我的外部变量 = 2u;
    }

模块1.h:

    #ifndef __MODULE1.H
    #define __MODULE1.H

    extern unsigned int MyExternVariable;

    无效我的外部函数(无效);      

    #万一

模块2.c

    #include "模块.1.h"

    静态无效 MyLocalFunction(void);

    静态无效 MyLocalFunction(无效)
    {
      我的外部变量 = 1u;
      我的外部函数();
    }
于 2008-09-06T23:29:14.977 回答
10

尝试使每个 .c 都专注于特定的功能领域。使用相应的 .h 文件来声明这些函数。

每个 .h 文件都应该有一个围绕其内容的“标题”保护。例如:

#ifndef ACCOUNTS_H
#define ACCOUNTS_H
....
#endif

这样,您可以根据需要多次包含“accounts.h”,并且在特定编译单元中第一次看到它将是唯一实际提取其内容的编译单元。

于 2008-09-06T23:02:43.977 回答
8

编译器

您可以在本主题中看到 C“模块”的示例- 请注意,有两个文件 - 头文件 tea.h 和代码 tea.c。您在头文件中声明您希望其他程序访问的所有公共定义、变量和函数原型。在您的主项目中,您将 #include 并且该代码现在可以访问标题中提到的 tea 模块的函数和变量。

之后它变得有点复杂。如果您使用 Visual Studio 和许多其他为您管理构建的 IDE,请忽略这部分 - 它们负责编译和链接对象。

链接器

当您编译两个单独的 C 文件时,编译器会生成单独的目标文件 - 所以 main.c 变成 main.o,tea.c 变成 tea.o。链接器的工作是查看所有目标文件(您的 main.o 和 tea.o),并匹配引用 - 因此,当您在 main 中调用 tea 函数时,链接器会修改该调用,因此它确实调用了正确的茶中的作用。链接器生成可执行文件。

有一个很棒的教程可以更深入地讨论这个主题,包括范围和您将遇到的其他问题。

祝你好运!

-亚当

于 2008-09-06T23:21:15.230 回答
7

几个简单的规则开始:

  1. 将您想要“公开”的那些声明放入您正在创建的 C 实现文件的头文件中。
  2. 只有 C 文件中的 #include 头文件是实现 C 文件所需的。
  3. 仅当该头文件中的声明需要时,才在该头文件中包含头文件。

  4. 如果编译器支持它,请使用 Andrew 描述的包含保护方法或使用#pragma 一次(它做同样的事情 - 有时更有效)
于 2008-09-06T23:12:13.410 回答
3

要回答您的附加问题:

这个 问题引发了我的询问。tea.h 文件不引用tea.c 文件。编译器是否“知道”每个 .h 文件都有对应的 .c 文件?

编译器主要不关心头文件。编译器的每次调用都会将源 (.c) 文件编译成目标 (.o) 文件。在幕后(即在make文件或项目文件中)正在生成与此等效的命令行:

compiler --options tea.c

源文件#include是它引用的资源的所有头文件,这是编译器查找头文件的方式。

(我在这里忽略了一些细节。关于构建 C 项目有很多东西要学习。)

于 2008-09-06T23:38:10.743 回答
3

除了上面提供的答案之外,将代码拆分为模块(单独的文件)的一个小优点是,如果您必须拥有任何全局变量,您可以使用关键字 ' 将它们的范围限制为单个模块静止的'。(您也可以将其应用于函数)。请注意,“静态”的这种使用不同于它在函数中的使用。

于 2008-09-07T02:20:47.793 回答
2

你的问题清楚地表明你并没有真正做太多认真的开发。通常的情况是您的代码通常太大而无法放入一个文件中。一个好的规则是,您应该将功能拆分为逻辑单元(.c 文件),并且每个文件包含的内容不应超过您一次可以轻松掌握的内容。

然后,给定的软件产品通常包括来自许多不同 .c 文件的输出。通常这样做是编译器生成许多目标文件(在 unix 系统“.o”文件中,VC 生成 .obj 文件)。“链接器”的目的是将这些目标文件组合到输出中(共享库或可执行文件)。

通常,您的实现 (.c) 文件包含实际的可执行代码,而头文件 (.h) 在这些实现文件中包含公共函数的声明。你可以很容易地拥有比实现文件更多的头文件,有时头文件也可以包含内联代码。

实现文件相互包含通常是很不寻常的。一个好的做法是确保每个实现文件将其关注点与其他文件分开。

我建议您下载并查看 linux 内核的源代码。对于 C 程序来说,它是相当庞大的,但它被很好地组织成不同的功能区域。

于 2008-09-06T23:25:14.907 回答
0

.h 文件应该用于定义函数的原型。这是必要的,因此您可以在 C 文件中包含您需要的原型,而无需在一个文件中声明您需要的所有函数。

例如,当 you 时#include <stdio.h>,这提供了 printf 和其他 IO 函数的原型。这些函数的符号通常由编译器默认加载。如果您对这些文件所涉及的常规习语感兴趣,可以查看 /usr/include 下的系统 .h 文件。

如果您只是编写功能不多的琐碎应用程序,则实际上没有必要将所有内容模块化为过程的逻辑分组。但是,如果您需要开发一个大型系统,那么您需要考虑在哪里定义每个函数。

于 2008-09-07T00:01:17.160 回答