头文件中应该包含什么,不应该包含什么?[关闭]


71

头文件绝对不应包含哪些内容?

例如,如果我正在使用具有许多常量的已记录的行业标准格式,那么在头文件中定义它们(如果我正在为该格式编写解析器)是否是一种好习惯?

头文件应包含哪些功能?
什么功能不应该?


1
简短而轻松:多个模块中需要的定义和声明。
ott-- 2012年

21
将这个问题标记为“过于广泛”和结束是绝对适度的过分节制。这个问题恰好问我在寻找什么-这个问题的格式正确,并且提出了一个非常明确的问题:最佳实践是什么?如果对于软件工程来说“太宽泛了”,我们也可以关闭整个论坛。
Gewure '17

TL; 博士 对于C ++,在Bjarne Stroustrup(其创建者)撰写的第四版“ C ++编程语言”中,在15.2.2节中描述了标题应该包含和不应包含的内容。我知道您将问题标记为C,但其中一些建议也适用。我认为这是一个很好的问题……
horro,

Answers:


57

标题中要放什么:

  • #include当标头包含在某些源文件中时,使标头可编译所需的最少指令集。
  • 需要共享且只能通过预处理器完成的事物的预处理器符号定义。即使在C语言中,预处理器符号也最好保持最小。
  • 前向声明结构是可编译的,以便使头文件主体中的结构定义,函数原型和全局变量声明可编译。
  • 在多个源文件之间共享的数据结构和枚举的定义。
  • 函数和变量的声明,其定义对于链接器是可见的。
  • 内联函数定义,但请在此处注意。

标头中不包含的内容:

  • 免费#include指令。这些无用的内容会导致不需要重新编译的内容重新编译,并且有时可以重新编译,从而导致系统无法编译。不要#include一个文件中的一个标题,如果标题本身并不需要其他的头文件。
  • 其预处理器符号可以通过某种机制(除预处理器以外的任何机制)实现。
  • 很多结构定义。将其拆分为单独的标题。
  • 需要附加#include,可能更改或太大的函数的内联定义。这些内联函数应该几乎没有扇出,如果确实有扇出,则应将其本地化为标头中定义的内容。

最小的#include陈述集是什么?

事实证明这是一个不平凡的问题。TL; DR定义:头文件必须包括直接定义直接使用的每种类型或直接声明所涉及的头文件中使用的每个功能的头文件,但不得包含任何其他内容。指针或C ++引用类型不适合直接使用;前向引用是首选。

有一个免费#include指令的位置,这是在自动测试中。对于软件包中的每个头文件,我都会自动生成然后编译以下内容:

#include "path/to/random/header_under_test"
int main () { return 0; }

编译应该干净(即没有任何警告或错误)。关于不完整类型或未知类型的警告或错误意味着被测试的头文件缺少某些#include指令和/或缺少前向声明。请注意:仅仅通过测试并不意味着#include指令集就足够了,更不用说最少了。


因此,如果我有一个库定义了一个名为A的结构,而这个库B则使用了该结构,而库B由程序C使用,那么我应该在库B的主头文件中包含库A的头文件,还是应该我只是向前宣布吗?库A在编译过程中被编译并与库B链接。
MarcusJ '18年

@MarcusJ-我在标题不属于什么下列出的第一件事是免费的#include语句。如果头文件B不取决于头文件A中的定义,则不要在头文件B中#include头文件A。头文件不是指定第三方依赖项或生成指令的地方。这些文件放在其他地方,例如顶级自述文件。
大卫·哈门

1
@MarcusJ-我更新了我的答案,试图回答您的问题。请注意,您的问题没有一个答案。我将举例说明两个极端。情况1:库B直接使用库A的功能的唯一位置是库B源文件中。情况2:库B是库A中功能的瘦扩展,库B的头文件直接使用库A中定义的类型和/或函数。在情况1中,没有理由在库A中公开库A。在库2的标头中。在第2种情况下,这种暴露是强制性的。
David Hammen '18

是的,是第2种情况,对不起,我的评论忽略了它使用的是库B的标头中库A中声明的类型的事实,我想我可能可以转发声明它,但我认为这样行不通。感谢更新。
MarcusJ '18年

将常量添加到头文件中是大禁忌吗?
mding5692

15

除了已经说过的。

H文件应始终包含:

  • 源代码文档!!!至少,各种参数和函数的返回值的用途是什么。
  • 标头后卫,#ifndef MYHEADER_H#定义MYHEADER_H ... #endif

H文件绝对不能包含:

  • 任何形式的数据分配。
  • 函数定义。内联函数在某些情况下可能是一种罕见的例外。
  • 任何带有标签的内容static
  • 与应用程序其余部分无关的Typedef,#define或常量。

(我还要说,从来没有任何理由在任何地方使用非常量全局/外部变量,但这是另一篇文章的讨论。)


1
除了您强调的内容,我都同意。如果要制作图书馆,是的,您应该记录文件或图书馆的用户。对于内部项目,如果您使用良好的,易于解释的变量和函数名称,则无需在文档中弄乱标题。
martiert 2012年

5
@martiert我也是“让代码说明一切”的学校。但是,即使自己以外的人都不会使用它们,您至少也应该始终记录它们的功能。特别令人感兴趣的事情是:如果函数具有错误处理,它将返回什么错误代码以及在什么情况下会失败?如果函数失败,参数(缓冲区,指针等)会怎样?另一个非常相关的事情是:指针参数是否向调用者返回一些东西,即它们是否期望分配的内存?->

1
对于调用者应该显而易见的是,在函数内部完成了什么错误处理,没有完成什么。如果该函数期望分配的缓冲区,则很可能会将越界检查也留给调用方。如果该函数依赖于要执行的另一个函数,则必须对此进行记录(即在link_list_add()之前运行link_list_init())。最后,如果函数具有“副作用”,例如创建文件,线程,计时器等,则应在文档中说明。->

1
也许“源代码文档”在这里太宽泛了,这确实属于源代码。具有输入和输出,前置条件和后置条件以及副作用的“使用文档”一定应该放在那里,而不是史诗般的,而是简短的
确保

2
有点迟了,但是文档是+1。为什么存在此类?该代码不能说明一切。此功能有什么作用?RTFC(阅读精细的.cpp文件)是淫秽的四个字母的缩写。永远不要对RTFC有所了解。标头中的原型应在一些可提取的注释(例如doxygen)中概述参数是什么以及函数的作用。为什么该数据成员存在,包含哪些内容以及以米,英尺或弗朗斯为单位的值?存在(可提取的)评论的另一个主题。
大卫·哈门

4

我可能永远不会说“永不”,但是在解析时生成数据和代码的语句不应放在.h文件中。

宏,内联函数和模板可能看起来像数据或代码,但它们不会在解析时生成代码,而是在使用时生成代码。这些项目通常需要在多个.c或.cpp中使用,因此它们属于.h。

在我看来,头文件应该具有对应于.c或.cpp的最小实际接口。接口可以包括#defines,class,typedef,struct定义,函数原型,以及不太受欢迎的全局变量extern定义。但是,如果仅在一个源文件中使用声明,则可能应将其从.h中排除,而应包含在源文件中。

有些人可能不同意,但是我个人对.h文件的标准是它们#include所有其他需要编译的.h文件。在某些情况下,这可能是很多文件,因此我们有一些有效的方法来减少外部依赖性,例如对类的前向声明,这些类使我们可以使用指向类对象的指针,而不必包括包含文件的大树。


3

头文件应具有以下组织:

  • 类型和常量定义
  • 外部对象声明
  • 外部函数声明

头文件不应包含对象定义,只能包含类型定义和对象声明。


内联函数定义呢?
科斯2012年

如果内联函数是仅在一个C模块内部使用的“辅助”函数,则仅将其放在该.c文件中。如果内联函数必须对两个或多个模块可见,则将其放入头文件中。
theD 2012年

另外,如果必须在库边界上可见该函数,则不要使其内联,因为这会迫使每次使用该库的人在每次修改内容时都重新编译。
Donal Fellows 2012年

@DonalFellows:这是一个反手的解决方案。更好的规则:不要将标头放在经常修改的标头中。如果函数没有扇出并且具有明确的定义(仅在基础数据结构发生更改时才会更改),则在标头中内联简短的小函数没有错。如果函数定义由于基础结构定义发生变化而发生变化,是的,您必须重新编译所有内容,但是由于结构定义发生了变化,因此无论如何都必须这样做。
大卫·哈门

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.