C / C ++中自给自足的头文件

时间:2009-12-12 02:23:43

标签: c++ header-files

我最近发布了一个问题,询问 Zen of C++ 会构成哪些操作。我收到了很好的答案,但我无法理解一个建议:

  • 使头文件自给自足

如何确保您的标头文件自给自足

欢迎任何其他与C / C ++中头文件的设计和实现相关的建议或最佳实践。

编辑:我发现this question解决了我的“最佳实践”部分。

8 个答案:

答案 0 :(得分:31)

自给自足的头文件是一个不依赖于它包含在哪里以正常工作的上下文文件的文件。如果您在使用之前确保#include或定义/声明了所有内容,那么您就拥有了一个自给自足的标题 自足标头的示例可能是这样的:

----- MyClass.h -----

class MyClass
{
   MyClass(std::string s);
};

-

---- MyClass.cpp -----

#include <string>
#include "MyClass.h"

MyClass::MyClass(std::string s)
{}

在此示例中, MyClass.h 使用std::string而不首先使用#including。 为此,在 MyClass.cpp 中,您需要将#include <string>放在#include "MyClass.h"之前。
如果MyClass的用户未能执行此操作,则会收到 std :: string未包含的错误。

通常可以忽略维护标题自给自足。例如,你有一个巨大的MyClass标题,你添加了另一个使用std :: string的小方法。在这个类目前使用的所有地方,在MyClass.h之前已经是#included。那么有一天你将#include MyClass.h作为第一个标题,突然你在一个你甚至没有碰过的文件中都有这些新错误(MyClass.h)
小心地将标题保持为自给自足有助于避免此问题。

答案 1 :(得分:19)

NASA Goddard Space Flight Center(GSFC)已发布解决此问题的C和C ++编程标准。

假设您的模块包含源文件perverse.c及其标题perverse.h

确保标题是自包含的

有一种非常简单的方法可以确保标头是自包含的。 在源文件中,您包含的第一个标头是模块的标头。如果它像这样编译,标题是自包含的(自给自足)。如果没有,请修复标题,直到它(可靠 1 )自包含。

perverse.h

#ifndef PERVERSE_H_INCLUDED
#define PERVERSE_H_INCLUDED

#include <stddef.h>

extern size_t perverse(const unsigned char *bytes, size_t nbytes);

#endif /* PERVERSE_H_INCLUDED */

几乎所有标头都应该受到保护以防止多次包含。 (标准<assert.h>标头是规则的明确例外 - 因此是“几乎”限定符。)

perverse.c

#include "perverse.h"
#include <stdio.h>   // defines size_t too

size_t perverse(const unsigned char *bytes, size_t nbytes)
{
    ...etc...
}

请注意,即使在项目标题之前包含标准标题通常也是一个好主意,但在这种情况下,模块标题(perverse.h)在所有其他标题之前的可测试性至关重要。我允许的唯一例外是在模块头之前包含一个配置头;然而,即使这是可疑的。如果模块头需要使用(或者可能只是'可以使用')来自配置头的信息,它应该包括配置头本身,而不是依赖于使用它的源文件。


脚注1:Steve Jessop对Shoosh答案的评论是为什么我将带括号的'(可靠)'评论放入我的'修复'评论中。他说:

  

使这一点变得困难的另一个因素是C ++中的“系统头可以包含其他头”规则。如果<iostream>包含<string>,则很难发现您忘记将<string>包含在[{1}} [或{{1}的某个标题中}}]。单独编译头文件没有错误:它在这个版本的编译器上是自给自足的,但在另一个编译器上它可能不起作用。


附录:将这些规则与GCC预编译标题相匹配

预编译头的GCC规则允许每个翻译单元只有一个这样的头,它必须出现在任何C令牌之前。

GCC 4.4.1手册,§3.20使用预编译标题

  

只有在满足以下条件时才能使用预编译的头文件:

     
      
  • 在特定编译中只能使用一个预编译头。
  •   
  • 一旦看到第一个C令牌,就无法使用预编译的头。你可以有   预编译头之前的预处理器指令;你甚至可以包括一个预编译的   只要在#include之前没有C标记,就可以从另一个标题内部标题。
  •   
  • [...]
  •   
  • 必须定义在包含预编译头之前定义的任何宏   与生成预编译头文件时的方式相同,或者不得影响   预编译头,通常意味着它们不会出现在预编译中   总而言之。
  •   

首先,这些约束意味着预编译头必须是文件中的第一个。第二个近似值表明,如果'config.h'只包含#define语句,它可能出现在预编译头之前,但更可能的是(a)config.h中的定义会影响其余的代码,并且(b)预编译头部无论如何都需要包含config.h。

我所研究的项目并未设置为使用预编译的标题,并且由GCC定义的约束加上由不同的编码人员进行的20多年的强化维护和扩展所引起的无政府状态意味着它将非常困难添加它们。

鉴于GSFC指南和GCC预编译头之间的要求不同(并且假设预编译头正在使用中),我认为我将使用单独的机制确保头的自包含和幂等性。我已经为我正在处理的主要项目执行此操作 - 重新组织标题以符合GSFC指南并非易事 - 我使用的脚本是<iostream>,如下所示。您甚至可以将此作为头文件目录中的“构建”步骤 - 确保所有头文件都是自包含的“编译”规则。

chkhdr脚本

我使用此<string>脚本来检查标头是否为自包含。虽然shebang说“Korn shell”,但是对于Bash甚至是原始的(System V-ish)Bourne Shell来说,代码实际上是可以的。

chkhdr

碰巧我从来不需要将任何包含空格的选项传递给脚本,因此在处理空格选项时代码不健全。在Bourne / Korn shell中处理它们至少会使脚本更复杂,没有任何好处;使用Bash和数组可能会更好。

用法:

chkhdr

答案 2 :(得分:4)

确保在标题中包含您需要的所有内容,而不是假设您包含的内容包含您需要的其他内容。

答案 3 :(得分:3)

这个想法是头文件不依赖于先前的头文件来编译。因此头文件的顺序并不重要。这样做的一部分是在头文件中包含它将需要的所有其他头文件。另一部分是ifdef你的标题,以便它们不会被处理多次。

这个想法是,如果你需要在你的类中添加一个foo对象,你只需要#include foo.h并且你不需要在它前面使用bar.h来获得foo.h来编译(例如,在foo中有一个调用返回一个bar对象实例。你可能对这个调用不感兴趣,但你需要添加bar.h让编译器知道被引用的内容。)

我不确定我是否会同意这个建议。一个大项目将有数百个头文件,编译将最终通过它们中的常见文件读数百次,只是为了忽略#ifdefs。我在这种情况下看到的是头文件的头文件,它是项目的标准,包括三十个常见的头文件。它始终位于包含列表中的第一位。这可以加快编译时间,但是使通用标题的维护成为一项熟练的任务。

答案 4 :(得分:2)

老问题,新答案。 : - )

现在有一个名为include-what-you-use的工具,旨在分析您的代码以确定这类问题。在Debian和派生系统上,它可以作为iwyu包安装。

答案 5 :(得分:1)

您想要使用GNU C Preprocessor Manual中描述的方法:

  

2.4一次性标题

     

如果头文件恰好包含两次,编译器将处理其内容两次。这很可能导致错误,例如当编译器两次看到相同的结构定义时。即使没有,也肯定会浪费时间。

     

防止这种情况的标准方法是将文件的整个实际内容包含在条件中,如下所示:

/* File foo.  */
#ifndef FILE_FOO_SEEN
#define FILE_FOO_SEEN
     

整个文件

#endif /* !FILE_FOO_SEEN */
     

此构造通常称为包装器#ifndef 。当再次包含标头时,条件将为false,因为FILE_FOO_SEEN已定义。预处理器将跳过文件的整个内容,编译器将不会看到它两次。

     

CPP进一步优化。它会记住头文件何时有一个包装器“#ifndef”。如果后续的“#include”指定了该标头,并且仍然定义了“#ifndef”中的宏,则根本不会重新扫描该文件。

     

您可以将评论放在包装器之外。他们不会干扰这种优化。

     

FILE_FOO_SEEN称为控制宏保护宏。在用户头文件中,宏名称不应以“_”开头。在系统头文件中,它应以“__”开头,以避免与用户程序冲突。在任何类型的头文件中,宏名称应包含文件的名称和一些其他文本,以避免与其他头文件冲突。

答案 6 :(得分:1)

这是一个很好的问题。我想我会重新检查在使用Visual Studio时将stdafx.h作为第一个包含在每个.cpp文件中的做法。如果您使用预编译的头文件,它无论如何都不会满足,也可能有更友好的头文件。

感谢jalf纠正。来自Wikipedia

  

Visual C ++不会在#include之前编译任何内容   除非是源文件中的“stdafx.h”   编译选项/Yu'stdafx.h'是   未选中(默认情况下);它假设全部   源代码中包含的代码   那条线已经编译好了。

所以这意味着预编译的标题会破坏自给自足的标题规则,对吗?

答案 7 :(得分:0)

没有看到你的另一个问题,我首先想到的就是保护我的头文件免受多次调用(让我的标题自生自杀)。

#ifndef MY_PROTECTED_HEADER_H
#define MY_PROTECTED_HEADER_H
/*
 * Stuff here
 */
#endif /* MY_PROTECTED_HEADER_H */
编辑:请注意我的原始回复不正确,因此您可能会看到此回复的负面分数。我纠正了我原来的错误。