为什么包含头文件这么邪恶?

时间:2011-05-04 16:16:04

标签: c++ header-files forward-declaration

我在when to use forward declarations over including header files上看到了很多解释,但很少有人解释为什么这样做很重要。我见过的一些原因包括:

  • 编译速度
  • 降低头文件管理的复杂性
  • 删除循环依赖

来自.net背景我发现标题管理令人沮丧。我有这种感觉我需要掌握前向声明,但到目前为止我一直在报废。

为什么编译器不能为我工作并使用一种机制(包括)找出我的依赖关系?

前向声明如何加快编译速度,因为在某些时候引用的对象需要编译?

我可以购买降低复杂性的论据,但这样做的实际例子是什么?

9 个答案:

答案 0 :(得分:19)

“掌握前瞻性声明”不是必要条件,它是可行的有用指南。

当包含标题,并且它引入更多标题,还有更多标题时,编译器必须完成大量处理单个翻译模块的工作。

例如,您可以查看gcc -E

的数量

单个#include <iostream>为我的g ++ 4.5.2提供了额外的18,560行代码。

#include <boost/asio.hpp>增加了另外74,906行。

#include <boost/spirit/include/qi.hpp>增加154,024行,超过5 MB的代码。

这加起来,特别是如果不小心包含在项目的每个文件中包含的某个文件中。

有时候翻过旧代码并修剪不必要的内容会因此而大大改进编译。替换包括转换模块中的前向声明,其中仅使用对某些类的引用或指针,进一步改进了这一点。

答案 1 :(得分:4)

  

为什么编译器不能为我工作并使用一种机制(包括)来计算我的依赖关系?

它不能因为,与其他语言不同,C ++有一个含糊不清的语法:

int f(X);

是函数声明还是变量定义?要回答这个问题,编译器必须知道X的含义,因此必须在该行之前声明X

答案 2 :(得分:3)

因为当你做这样的事情时:

bar.h:

class Bar {
  int foo(Foo &);
}

然后编译器不需要知道如何定义Foo结构/类;所以导入定义Foo的标头是没用的。此外,导入定义Foo的头文件可能还需要导入定义Foo使用的其他类的头文件;这可能意味着导入定义其他类等的标题....一直都是海龟。

最后,编译器正在处理的文件几乎就像复制粘贴所有头文件的结果一样;所以它会变得很大,没有充分的理由,当有人在你不需要(或导入,或类似的东西)的头文件中输入错误时,那么编译你的课程开始花费太多时间(或者失败)没有明显的原因)。

因此,根据需要向编译器提供尽可能少的信息是一件好事。

答案 3 :(得分:2)

  

前向声明如何加快编译速度,因为在某些时候引用的对象需要编译?

1)减少磁盘I / O(打开的文件越少,次数越少)

2)减少内存/ CPU使用量 大多数翻译只需要一个名字。如果你使用/分配对象,你需要它的声明。

这可能是它为您点击的地方:您编译的每个文件都会编译其翻译中可见的内容。

维护不善的系统最终将包含大量不需要的东西 - 然后这会针对它看到的每个文件进行编译。通过在可能的情况下使用前向,您可以绕过它,并显着减少必须编译公共接口(及其所有包含的依赖项)的次数。

也就是说:标题的内容不会被编译一次。它会一遍又一遍地编译。必须解析此翻译中的所有内容,检查它是否为有效程序,检查警告,优化等等。很多次。

包括懒惰只会增加显着的磁盘/ CPU /内存增加,这会为你带来无法忍受的构建时间,同时引入重要的依赖关系(在非平凡的项目中)。

  

我可以购买降低复杂性的论据,但实际的例子是什么呢?

不必要包括将依赖性作为副作用引入。当你编辑一个包含(必要或不包含)时,必须重新编译包含它的每个文件(当必须不必要地打开和编译数十万个文件时,这不是微不足道的。)

拉科斯写了一本好书,详细介绍了这一点:

http://www.amazon.com/Large-Scale-Software-Design-John-Lakos/dp/0201633620/ref=sr_1_1?ie=UTF8&s=books&qid=1304529571&sr=8-1

答案 4 :(得分:1)

本文中指定的

Header file inclusion rules将有助于减少管理头文件的工作量。

答案 5 :(得分:1)

我使用前向声明只是为了减少完成的源文件之间的导航量。例如如果模块X在模块Y中调用一些胶水或接口函数F,那么使用前向声明意味着编写函数并且可以通过仅访问2个位置来完成调用,Xc和Yc在一个好的IDE帮助时没有那么多问题你导航,但我更倾向于编写自下而上的编码工作代码,然后找出如何包装它而不是通过自上而下的界面规范..因为接口本身发展它不方便完全写出来。

在C(或c ++减去类)中,可以通过仅在使用它们的源文件中定义它们来真正保持结构细节的私有性,并且仅将前向声明暴露给外部世界 - 需要性能的黑色拳击级别 - 以c ++ / classes的方式破坏虚拟机。通过在源文件中列出“自下而上”(旧的静态关键字),也可以避免需要对事物进行原型设计(访问标题)。

管理标题的痛苦有时会暴露程序的模块化程度 - 如果它真正模块化,您必须访问的标题数量以及代码数量和数量。应尽量减少在其中声明的数据结构。

通过预编译标题处理一个包含“包含所有内容的所有内容”的大型项目,不会鼓励这种真正的模块化。

模块依赖性可以与与性能问题相关的数据流相关联,即i-cache和&amp; d-cache问题。如果一个程序涉及许多相互调用的模块,那么在许多随机位置修改数据,它可能具有较差的缓存一致性 - 优化这样一个程序的过程通常会涉及破坏通行证并添加中间数据......经常会破坏许多“类图”/“框架”(或至少需要创建许多中间体数据结构)。繁重的模板使用通常意味着复杂的指针追逐缓存破坏数据结构。在优化状态下,依赖性和指针追逐将减少。

答案 6 :(得分:0)

我相信前向声明会加快编译速度,因为头文件仅包含在实际使用的位置。这减少了打开和关闭文件一次的需要。你是正确的,在某些时候引用的对象将需要编译,但如果我只在我的另一个.h文件中使用指向该对象的指针,为什么实际包含它?如果我告诉编译器我正在使用指向类的指针,那就是它所需要的一切(只要我没有调用该类的任何方法。)

这不是它的结束。那些.h文件包含其他.h文件......因此,对于大型项目,打开,读取和关闭,重复包含的所有.h文件都会成为一个重要的开销。即使使用#IF检查,您仍然需要打开和关闭它们。

我们在就业来源实践这一点。我的老板用类似的方式对此进行了解释,但我确信他的解释更为明确。

答案 7 :(得分:0)

  

前向声明如何加快编译速度,因为在某些时候引用的对象需要编译?

因为include是预处理器的东西,这意味着它在解析文件时通过强力完成。您的对象将被编译一次(编译器),然后在适当的时候链接(链接器)。

在C / C ++中,当你编译时,你必须记住有一整套工具(预处理器,编译器,链接器以及make或Visual Studio等构建管理工具......)

答案 8 :(得分:0)

善与恶。战斗继续,但现在在头文件的战场上。头文件是语言的必需品和特征,但如果以非最佳方式使用,它们会产生许多不必要的开销,例如:不使用前瞻性声明等。

  

前瞻声明如何加速   从某种程度上来说,汇编   引用的对象需要   编译?

     

我可以购买减少的论点   复杂性,但实际上会是什么   这个例子是什么?

前瞻性声明很糟糕。我的经验是,很多c ++程序员都没有意识到你不必包含任何头文件,除非你真的想要使用某种类型,例如您需要定义类型,以便编译器了解您要执行的操作。尝试避免在其他头文件中包含头文件非常重要。

只需将指针从一个函数传递到另一个函数,只需要一个前向声明:

// someFile.h
class CSomeClass;
void SomeFunctionUsingSomeClass(CSomeClass* foo);

包含someFile.h并不要求你包含CSomeClass的头文件,因为你只是传递一个指向它的指针,而不是使用该类。这意味着编译器只需要解析一行行(类CSomeClass;)而不是整个头文件(可能链接到其他头文件等等)。

这减少了编译时间和链接时间,如果你有很多标题和很多类,我们在这里谈论大的优化。