我应该在标题中使用#include吗?


71

是否有必要使用#include某些文件,如果在标头(* .h)中使用了此文件中定义的类型?

例如,如果我使用GLib并希望gchar在我的标头中定义的结构中使用基本类型,那么是否#include <glib.h>知道我的* .c文件中已经包含了它,是否有必要做一个?

如果是,我还必须将其放在#ifndef和之间#define或之后#define吗?


Answers:


107

NASA的戈达德太空飞行中心(GSFC)规定C头为标头,必须有可能在源文件中包含头作为唯一的头,然后将使用该头提供的功能来编译该代码。

这意味着标头必须是独立的,幂等的并且是最小的:

  • 自包含的-通过在必要时包含相关标头来定义所有必需的类型。
  • 幂等—即使多次被包含,编译也不会中断。
  • 最小化-它不会定义使用头文件访问头文件定义的功能的代码不需要的任何内容。

此规则的好处是,如果有人需要使用标头,则不必费力确定还必须包含哪些其他标头-他们知道标头提供了所有必要的内容。

可能的不利之处是某些标头可能会多次包含在内;这就是为什么多重包含标头保护至关重要的原因(以及为什么编译器尽量避免重新包含标头的原因)。

实作

此规则的手段,如果报头使用类型-如“ FILE *”或“ size_t” -那么它必须确保相应的其他报头(<stdio.h><stddef.h>例如)应包括在内。一个推论,往往忘记了,是此时的标题不应该包括被任何其他头为了使用程序包所需的包的用户。换句话说,标题应该最小。

此外,GSFC规则提供了一种简单的技术来确保发生这种情况:

  • 在定义功能的源文件中,标头必须是列出的第一个标头。

因此,假设我们有一个魔术排序。

魔术排序

魔术排序

请注意,标头必须包含一些定义的标准标头size_t;,做最小的标准头也是如此<stddef.h>,尽管其他几人也这样做(<stdio.h><stdlib.h><string.h>,可能是几个人)。

同样,如前所述,如果实现文件需要其他一些头,就这样吧,并且需要一些额外的头是完全正常的。但是实现文件('magicsort.c')应该包括它们本身,而不是依赖其头文件来包括它们。标头应仅包括软件用户需要的内容;不需要实施者。

配置头

如果您的代码使用配置标头(例如,GNU Autoconf和生成的“ config.h”),则可能需要在“ magicsort.c”中使用它:

这是我唯一一次知道模块的私有标头不是实现文件中的第一个标头。但是,“ config.h”的条件包含可能应该在“ magicsort.h”本身中。


更新2011-05-01

上面链接的URL不再起作用(404)。您可以在EverySpec.com上找到C ++标准(582-2003-004);实际上似乎缺少C标准(582-2000-005)。

C标准的准则是:

§2.1单位

(1)代码应以单元或独立的头文件的形式进行结构化。

(2)一个单元应由一个头文件(.h)和一个或多个主体(.c)文件组成。头文件和主体文件统称为源文件。

(3)单元头文件应包含客户单元所需的所有相关信息。单元的客户只需访问头文件即可使用该单元。

(4)单元头文件应包含#include语句,用于单元头所需的所有其他头。这样,客户端可以通过包含一个头文件来使用一个单元。

(5)在所有其他#include语句之前,单元主体文件应包含一个用于单元头的#include语句。这使编译器可以验证所有必需的#include语句都在头文件中。

(6)主体文件应仅包含与一个单元关联的功能。一个主体文件可能不提供在不同头文件中声明的函数的实现。

(7)所有使用给定单元U的任何部分的客户单元都应包括单元U的头文件;这样可以确保只有一个地方定义了单位U中的实体。客户单元只能调用单元头中定义的功能;它们可能不会调用主体中定义但未在标头中声明的函数。客户单元可能无法访问主体中声明的变量,但不能访问标头中的变量。

组件包含一个或多个单元。例如,数学库是一个包含多个单元的组件,例如向量,矩阵和四元数。

独立头文件没有关联的主体;例如,通用类型标头不声明函数,因此不需要正文。

一个单元具有多个正文文件的一些原因:

  • 主体代码的一部分与硬件或操作系统有关,而其余部分则很常见。
  • 文件太大。
  • 该单元是一个通用的实用程序包,某些项目将仅使用其中一些功能。将每个功能放在单独的文件中,使链接器可以从最终映像中排除未使用的功能。

§2.1.1标头包括基本原理

该标准要求单元的标头包含#include单元标头所需的所有其他标头的语句。#include首先在单元主体中放置单元标头,使编译器可以验证标头包含所有必需的#include语句。

本标准不允许的替代设计不允许#include在标题中使用任何语句;所有 #include的都在主体文件中完成。然后,单元头文件必须包含#ifdef用于检查所需头是否以正确顺序包含的语句。

替代设计的一个优点是,#include主体文件中的列表正是makefile中所需的依赖项列表,并且此列表由编译器检查。对于标准设计,必须使用工具来生成依赖关系列表。但是,所有分支推荐的开发环境都提供了这样的工具。

替代设计的主要缺点是,如果某个单元的所需标头列表发生更改,则必须编辑使用该单元的每个文件以更新#include语句列表。同样,在不同的目标上,编译器库单元所需的头列表可能会有所不同。

替代设计的另一个缺点是,必须修改编译器库头​​文件和其他第三方文件才能添加所需的#ifdef语句。

另一种不同的惯例是在主体文件中包括所有系统头文件,然后再包括任何项目头文件。该标准不遵循这种做法,因为某些项目头文件可能依赖于系统头文件,或者是因为它们使用了系统头中的定义,或者是因为它们想覆盖系统定义。这样的项目头文件应包含#include 系统头的语句;如果主体首先包含它们,则编译器不会对此进行检查。

GSFC标准可通过Internet存档获得2012-12-10

信息Eric S. Bullington提供

可以通过Internet档案访问和下载参考的NASA C编码标准:

http://web.archive.org/web/20090412090730/http://software.gsfc.nasa.gov/assetsbytype.cfm?TypeAsset=Standard

排序

问题还问:

如果是,我是否还必须将其(#include线)放在#ifndef和之间#define或之后#define

答案显示了正确的机制-嵌套的include等应位于的后面#define(并且#define应当是标头中的第二个非注释行),但是并不能解释为什么这是正确的。

试想,如果您将会怎么样#include之间#ifndef#define。假设其他标头本身包括各种标头,甚至可能是#include "magicsort.h"间接的。如果第二个包含magicsort.h发生在之前#define MAGICSORT_H_INCLUDED,则在定义其定义的类型之前,将第二次包含头。因此,在C89和C99中,任何typedef类型的名称都将被错误地重新定义(C2011允许将它们重新定义为相同的类型),并且您将获得多次处理文件的开销,这在第一个中就破坏了标题保护功能。地点。这也是为什么the#define是第二行而不是仅在#endif。之前编写的原因。给出的公式是可靠的:


谢谢,这回答了我的问题。只是一个细节:#define MAGICSORT_H_INCLUDED和#define MAGICSORT_H_INCLUDED <b> 1 </ b>之间的区别是什么
Victor

4
没有操作上的差异。如果#define X不指定任何值,则使用X表示空字符串,除非在条件上下文中(如X等于#if X0)。显然,#define X 1总是扩展为1。只要您始终使用#ifdef X(或#if defined(X)#elif defined(X))而不是使用进行测试#if X,那么您都可以使用。
乔纳森·莱夫勒

在最初的ANSI C标准中,它还声明一个合格的头文件必须独立(包括所需的一切)。在我的C文件中,我首先包含其自己的标头,这有助于检测这些非独立标头(但仅有帮助)。最好现在就抓住它们,因为以后的工作量较少。
ctrl-alt-delor 2012年

@richard:情况比那稍微细微些。每个标准C标头只能提供在其标准部分引用的类型,宏和函数声明(当然,还有保留给实现的任何名称)。但是,一些项目(例如NULL,size_t)在许多标准C标头中定义。因此,实现必须确保仅将它们定义一次,而不管转换标头中包含标准标头的顺序如何。实现的方式是它的问题。注意:C99允许<inttypes.h>包含<stdint.h>
乔纳森·勒夫勒

当您碰巧头周围有循环依赖项时,在头中包含头可能会导致相当神秘的问题。第一次,这个问题并不明显。唯一的症状是,尽管您包括相应的头文件,但编译器仍坚持未声明任何内容。如果您必须使用20年前的代码库(具有巨大的头文件依赖地狱和#ifdef混乱),要进行跟踪非常困难。
Calmarius

21

好的做法是仅在包含文件需要它们时才将其包含在包含文件中。如果给定包含文件中的定义仅在.c文件中使用,则仅在.c文件中包括它。

在您的情况下,我会将其包含在#ifdef /#endif之间的包含文件中。

这样可以最大程度地减少依赖关系,因此,如果包含文件发生更改,则不需要重新编译不需要给定包含的文件。


不好的建议。它将产生难看的C程序(带有#ifdef /#endif)。
psihodelia

19
我想你误解了我的答案。通常在.h文件中使用#ifdef /#endif以避免.h文件的多次包含。
理查德·彭宁顿

那就是我在gnu网站上阅读的内容。但是,在#define之前(和#ifndef之后)和#define之后放置的#include有什么区别?
维克多

与预处理程序没有区别,因为如果尚未包含该文件,该语句将始终执行。但是,您不应在#ifndef和#define之间放置任何内容,因为这是其他程序员经常会看到的常见模式。
danio

另一个约定(尽管较不常见)是:与另一个约定一样,#ifndef MY_HEADER // header contents #define MY_HEADER #endif这消除了像其他参考一样遵循的要求#endif// MY_HEADER
Jivan Pal

0

通常,库开发​​人员使用#ifndef /#define / #endif“ trick”保护其包含内容免受多个包含,因此您不必这样做。

当然,您应该检查...但是无论如何,编译器会在某个时候告诉您;-)无论如何,检查多个包含项是一种好习惯,因为它会减慢编译周期。


0

在编译期间,预处理器仅将#include指令替换为指定的文件内容。为了防止无限循环,应使用

如果某个标头包含在文件中包含的另一个标头中,则不必再次明确地包含它,因为它将以递归方式包含在文件中


0

是的,这是必要的,否则编译器在尝试编译不“意识到”的代码时会抱怨。认为#include是对编译器的提示/轻推/弯头,告诉编译器选择声明,结构等以使编译成功。jldupont指出的#ifdef /#endif标头技巧是为了加快代码编译速度。

在具有C ++编译器并编译纯C代码的情况下使用它,如下所示。 这是技巧的示例:

#ifndef __MY_HEADER_H__
#定义__MY_HEADER_H__

#ifdef __cplusplus
extern“ C” {
#万一


/ *此处的C代码,例如结构,声明等。* /

#ifdef __cplusplus
}
#万一

#endif / * __MY_HEADER_H__ * /

现在,如果多次包含该符号,则编译器将只包含一次,因为该符号__MY_HEADER_H__定义了一次,从而加快了编译时间。 请注意上面示例中的符号cplusplus,这是处理C ++编译的正常标准方式。

我已经包含了以上内容以证明这一点(尽管与发帖人的原始问题并不真正相关)。希望这对您有所帮助,汤姆,谢谢。

PS:抱歉让任何人对此表示不满,因为我认为这对C / C ++新手来说是有用的花絮。留下评论/批评等,因为它们是最受欢迎的。


ANSI C编译器不会抱怨在其他地方定义的类型的标头中的用法。
维克多

@Victor:是的,但是,如果您只有C ++编译器,这将使C ++编译器能够编译纯老式的C老式代码...希望这可以阐明我的观点。仍然感谢您的评论。:)
t0mm13b

在代码中使用双下划线不是一个好主意。使用C ++中实现保留双下划线开始用双下划线的所有名称和名称被保留为C.实施
乔纳森·莱弗勒

__cplusplus是由实现定义的宏(如果它是c ++编译器),尽管您对include Guard定义正确无误
Scott Wales

0

您需要从标头中包含标头,而无需在.c中包含标头。包含应该放在#define之后,这样就不必多次不必要地包含它们。例如:


因此,如果我在头文件中包含#glib.h,则不需要在.c文件中这样做?我也在.c中使用GLib类型和函数!
维克多

是的-因为.c包含.h,其中包括glib.h,因此有效地包括了.c中的glib.h
danio 2009年

然后,只要该标头包含在.c文件中,是的-所有include所做的就是在编译时插入文件的内容。因此将它们从glib.h复制到myheader.h,然后将其复制到mycode.c
Martin Beckett

0

我使用以下构造来确保在此包含之前包含了所需的包含文件。我仅将所有头文件包含在源文件中。


3
为什么不只输入#include“ INCLUDE.h”
ctrl-alt-

2
而不是这样做,请确保每个头文件都包含防护。然后,您可以包括它,而不必担心其他文件是否包含相同的标头。
伦丁

-1

我通常要做的是制作一个包含文件,该文件包含正确顺序的所有必要依赖项。所以我可能有:

全部在project.h中。现在,project.h可以包含在没有订单要求的任何地方,但是我仍然可以在不同的头文件中拥有依赖项,类型和API函数原型。


请注意,通常不应创建以下划线开头的函数,变量,标记或宏名称。部分C11§7.1.3保留标识符说: -以下划线和一个大写字母或另一个下划线开头的所有标识符,始终保留用于任何用途。所有以下划线开头的标识符始终保留为普通和标记名称空间中的文件范围标识符。另请参见C中的双下划线(__const)是什么意思?
乔纳森·莱夫勒

-2

只需将所有外部标头包含在项目的一个公共标头文件中,例如global.h,并将其包含在所有c文件中:

它看起来可能像这样:

该文件使用include Guard来避免多个包含,非法的多个定义等。


2
我宁愿将其命名为<projectname> .h之类的名称,但请务必注意,如果其中包含很多文件,则确实会减慢总编译时间。
Earlz

但是,我认为global.h可以。我已经在许多严肃的项目中看到了这样的约定。
psihodelia

3
如果您使用外部标头表示您在当前项目之外并且不会经常更改,则可以。如果使用它来包含当前模块/库外部的所有头文件,则当更改一个头文件(所有源文件都不需要)时,它将导致很多不必要的重新编译。
danio

很好,但是您告诉我将其包含在.c或.h <u>或</ u>文件中,但这并不能真正回答我的问题。我将其放入.c文件一次。似乎编译器也没有必要在.h中执行此操作。
维克多

4
这被广泛认为是直接有害的做法,因为它在整个项目中的每个不相关文件之间建立了紧密的联系。不要这样做,这是糟糕而危险的设计。
伦丁
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.