C文件的组织
我习惯于在一个C文件中进行所有编码。但是,我正在做一个足够大的项目,以至于这样做是不切实际的。我一直将它们一起包含在其中,但是我遇到了多次包含一些文件的情况,等等。我听说过.h文件,但是我不确定它们的功能是什么(或者为什么如此)拥有2个文件比1个更好。
我应该使用什么策略来组织代码?是否可以将特定文件的"公用"功能与"专用"功能分开?
这个问题引起了我的询问。 tea.h文件未引用tea.c文件。编译器是否"知道"每个.h文件都有一个对应的.c文件?
解决方案
回答
尝试使每个.c专注于特定的功能领域。使用相应的.h文件来声明这些功能。
每个.h文件的内容周围应有一个"标题"保护符。例如:
#ifndef ACCOUNTS_H #define ACCOUNTS_H .... #endif
这样,我们可以根据需要多次包含" accounts.h",并且第一次在特定的编译单元中看到它是唯一真正吸收其内容的对象。
回答
要开始的几个简单规则:
- 将要设为"公共"的那些声明放入要创建的C实现文件的头文件中。
- 在C文件中,仅#include头文件是实现C文件所必需的。
- 仅在头文件中的声明需要时,才将头文件包括在头文件中。
- 使用安德鲁描述的include保护方法,或者如果编译器支持,则使用#pragma一次(这样做的作用相同-有时效率更高)
回答
编译器
我们可以在本主题上看到一个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函数时,链接器会修改该调用,因此实际上会调用正确的函数在茶中。链接器生成可执行文件。
有一个很棒的教程可以深入探讨该主题,包括范围和我们将遇到的其他问题。
祝你好运!
-亚当
回答
问题清楚地表明,我们尚未真正进行过认真的开发。通常的情况是代码通常太大而无法放入一个文件中。一个好的规则是,我们应该将功能分成逻辑单元(.c文件),并且每个文件中所包含的内容不得超过一次即可轻松保存的内容。
然后,给定的软件产品通常包括许多不同的.c文件的输出。通常这样做的方法是编译器生成许多目标文件(在unix系统中的" .o"文件中,VC生成.obj文件)。 "链接器"的目的是将这些目标文件组合到输出中(共享库或者可执行文件)。
通常,实现(.c)文件包含实际的可执行代码,而头文件(.h)在这些实现文件中具有公共功能的声明。我们可以轻松拥有比实现文件更多的头文件,并且有时头文件也可以包含内联代码。
实现文件相互包含通常是非常不寻常的。优良作法是确保每个实现文件将其关注点与其他文件分开。
我建议我们下载并查看linux内核的源代码。对于C程序而言,这是非常庞大的,但是可以很好地组织到单独的功能区域中。
回答
我们应该将.h文件视为.c文件的接口文件。每个.c文件代表一个具有一定功能的模块。如果.c文件中的函数被其他模块(即其他.c文件)使用,请将函数原型放入.h接口文件中。通过将接口文件包含在原始模块.c文件以及需要该功能的所有其他.c文件中,我们可以将此功能提供给其他模块。
如果只需要某个.c文件中的功能(而无需任何其他模块中的功能),则将其作用域声明为静态。这意味着只能在定义它的c文件中调用它。
跨多个模块使用的变量也是如此。它们应该放在头文件中,并且必须在其中标记关键字" extern"。注意:对于功能,关键字" extern"是可选的。函数始终被视为"外部"。
头文件中的包含防护有助于避免多次包含同一头文件。
例如:
Module1.c:
#include "Module1.h" static void MyLocalFunction(void); static unsigned int MyLocalVariable; unsigned int MyExternVariable; void MyExternFunction(void) { MyLocalVariable = 1u; /* Do something */ MyLocalFunction(); } static void MyLocalFunction(void) { /* Do something */ MyExternVariable = 2u; }
Module1.h:
#ifndef __MODULE1.H #define __MODULE1.H extern unsigned int MyExternVariable; void MyExternFunction(void); #endif
Module2.c
#include "Module.1.h" static void MyLocalFunction(void); static void MyLocalFunction(void) { MyExternVariable = 1u; MyExternFunction(); }
回答
要回答其他问题:
This question precipitated my inquiry. The tea.h file makes no reference to the tea.c file. Does the compiler "know" that every .h file has a corresponding .c file?
编译器主要不关心头文件。编译器的每次调用都会将源(.c)文件编译为目标(.o)文件。在幕后(即在make
文件或者项目文件中)正在生成与之等效的命令行:
compiler --options tea.c
源文件" #include"包含它所引用资源的所有头文件,这是编译器查找头文件的方式。
(我在这里掩盖了一些细节。有关构建C项目的知识很多。)
回答
.h文件应用于定义函数的原型。这是必要的,因此我们可以将所需的原型包含在C文件中,而无需在一个文件中声明所有需要的功能。
例如,当我们使用#include <stdio.h>时,这将提供printf和其他IO功能的原型。这些函数的符号通常是默认情况下由编译器加载的。如果我们对与这些文件有关的常规用法感兴趣,可以在/ usr / include下查看系统的.h文件。
如果我们只编写功能不多的琐碎应用程序,则实际上没有必要将所有内容模块化到过程的逻辑分组中。但是,如果需要开发大型系统,则需要考虑在何处定义每个功能。
回答
除了上面提供的答案外,将代码扩展为模块(单独的文件)的一个小好处是,如果我们必须具有任何全局变量,则可以使用关键字"静态的'。 (我们也可以将其应用于函数)。请注意,"静态"的这种用法与其在函数内部的用法不同。