代码之家  ›  专栏  ›  技术社区  ›  user15071

C++中头文件中所有代码的优缺点?

  •  45
  • user15071  · 技术社区  · 17 年前

    您可以构造一个C++程序,以便(几乎)所有代码驻留在头文件中。它本质上看起来像一个C#或Java程序。然而,你至少需要一个 .cpp 文件,以在编译时拉入所有头文件。现在我知道有些人会非常讨厌这个想法。但我还没有发现这样做有任何令人信服的缺点。我可以列举一些优点:

    [1] 更快的编译时间。所有头文件只解析一次,因为只有一个头文件。cpp文件。此外,一个头文件不能包含多次,否则将导致构建中断。在使用替代方法时,还有其他方法可以实现更快的编译,但这非常简单。

    [2] 它避免了循环依赖,使它们变得绝对清晰。如果 ClassA 在里面 ClassA.h 循环依赖于 ClassB 在里面 ClassB.h ,我必须提出一个参考;它很突出。(请注意,这与C#和Java不同,在C#和Java中,编译器会自动解析循环依赖项。这会鼓励糟糕的编码实践)。同样,如果代码处于循环状态,则可以避免循环依赖关系 .cpp 文件,但在现实世界的项目中, .cpp 文件往往包含随机的标题,直到你无法确定谁依赖谁。

    你的想法?

    17 回复  |  直到 14 年前
        1
  •  36
  •   Community Mohan Dere    6 年前

    原因[1]编译时间更快

    不在我的项目中:源文件(CPP)只包含它们需要的头文件(HPP)。因此,当我因为一个微小的变化而只需要重新编译一个CPP时,我有十倍于相同数量的未重新编译的文件。

    也许你应该把你的项目分解成更具逻辑性的源/头:对类A实现的修改不需要重新编译类B、C、D、E等的实现。。

    原因[2]它避免了循环依赖

    代码中的循环依赖?

    抱歉,但我还没有遇到真正的问题:假设a依赖于B,B依赖于a:

    struct A
    {
       B * b ;
       void doSomethingWithB() ;
    } ;
    
    struct B
    {
       A * a ;
       void doSomethingWithA() ;
    } ;
    
    void A::doSomethingWithB() { /* etc. */ }
    void B::doSomethingWithA() { /* etc. */ }
    

    解决这个问题的一个好方法是,将这个源代码分解为每个类至少一个源代码/头(类似于Java的方式,但每个类有一个源代码和一个头):

    // A.hpp
    
    struct B ;
    
    struct A
    {
       B * b ;
       void doSomethingWithB() ;
    } ;
    

    .

    // B.hpp
    
    struct A ;
    
    struct B
    {
       A * a ;
       void doSomethingWithA() ;
    } ;
    

    .

    // A.cpp
    #include "A.hpp"
    #include "B.hpp"
    
    void A::doSomethingWithB() { /* etc. */ }
    

    .

    // B.cpp
    #include "B.hpp"
    #include "A.hpp"
    
    void B::doSomethingWithA() { /* etc. */ }
    

    因此,没有依赖性问题,编译速度仍然很快。

    我错过什么了吗?

    在从事“现实世界”项目时

    在现实世界的项目中,cpp文件往往包含随机标题,直到您无法确定谁依赖于谁

    当然但是,如果您有时间重新组织这些文件以构建“一个CPP”解决方案,那么您就有时间清理这些头文件。我的标题规则是:

    • 分解收割台,使其尽可能模块化
    • 永远不要包含你不需要的标题
    • 如果您需要一个符号,请向前声明
    • 仅当上述操作失败时,才包括标题

    无论如何,所有标题都必须是自给自足的,这意味着:

    • 页眉包括所有需要的页眉(并且只包括需要的页眉——见上文)
    • 必须编译包含一个头的空CPP文件,而不需要包含任何其他内容

    这将消除排序问题和循环依赖关系。

    编译时间是个问题吗?然后

    编译时间真的是一个问题,我会考虑:

    结论

    你所做的不是把所有的东西都放在标题里。

    基本上,您将所有文件都包含在一个且只有一个最终源中。

    也许你在完整的项目汇编方面获胜了。

    但是,当你为一个小小的改变而编译时,你总是会输。

    在编写代码时,我知道我经常编译一些小的更改(如果只是为了让编译器验证我的代码),然后最后一次,做一个完整的项目更改。

    如果我的项目按照你的方式组织,我会损失很多时间。

        2
  •  26
  •   Milan BabuÅ¡kov    17 年前

    我不同意第一点。

    是的,只有一个。cpp和从头开始的构建时间更快。但是,你很少从头开始构建。您需要做一些小的更改,每次都需要重新编译整个项目。

    我更喜欢用另一种方式:

    • 将共享声明保存在文档中。h文件
    • 保留仅在中的一个位置使用的类的定义。cpp文件

    所以,我的一些建议。cpp文件开始看起来像Java或C#代码;)

    但是 “把东西放在里面。h' 由于第2点,在设计系统时,这种方法是很好的。你成功了。我通常在构建类层次结构时这样做,之后当代码体系结构变得稳定时,我会将代码移动到。cpp文件。

        3
  •  16
  •   Vincent Robert    17 年前

    你说你的解决方案有效是对的。它甚至可能对您当前的项目和开发环境没有缺点。

    但是

    正如其他人所说,每次更改一行代码时,将所有代码放在头文件中会强制进行完整编译。这可能还不是一个问题,但您的项目可能会变得足够大,以至于编译时间将成为一个问题。

    另一个问题是在共享代码时。虽然您可能还没有直接担心,但对代码的潜在用户尽可能多地隐藏代码是很重要的。通过将代码放入头文件,任何使用代码的程序员都必须查看整个代码,而他们只是对如何使用它感兴趣。将代码放入cpp文件只允许以头文件的形式交付二进制组件(静态或动态库)及其接口,在某些环境中可能更简单。

    如果您想将当前代码转换为动态库,这是一个问题。由于没有与实际代码解耦的适当接口声明,因此无法将编译后的动态库及其使用接口作为可读的头文件交付。

    您可能还没有这些问题,这就是为什么我告诉您,您的解决方案在您当前的环境中可能还可以。但对任何变化做好准备总是更好的,其中一些问题应该得到解决。

    PS:关于C#或Java,你应该记住这些语言并没有按照你说的做。它们实际上是独立编译文件(如cpp文件),并全局存储每个文件的接口。这些接口(以及任何其他链接的接口)然后用于链接整个项目,这就是为什么它们能够处理循环引用。因为C++只对每个文件只编译一次,所以不能全局存储接口。这就是为什么需要在头文件中明确地编写它们。

        4
  •  12
  •   user19302 user19302    17 年前

    你误解了语言的用途。cpp文件实际上(或者应该是内联代码和模板代码除外)是系统中唯一的可执行代码模块。cpp文件被编译成对象文件,然后链接在一起。h文件的存在仅用于转发在中实现的代码声明。cpp文件。

    这会导致更快的编译时间和更小的可执行文件。它看起来也相当干净,因为你可以通过查看它的。h.宣言。

    至于内联代码和模板代码——因为这两种代码都是由编译器而不是链接器生成代码的——它们必须始终对编译器可用。cpp文件。因此,唯一的解决办法是将其包含在您的应用程序中。h文件。

    然而,我已经开发了一个解决方案,其中我在一个。h文件、所有模板和内联代码。inl文件和my中所有非模板/内联代码的实现。cpp文件。这个inl文件包含在我的。h文件。这可以保持事物的整洁和一致性。

        5
  •  11
  •   C. K. Young    17 年前

    对我来说,明显的缺点是,你总是必须一次编译所有的代码。具有 .cpp 文件,您可以进行单独的编译,所以您只需要重建真正发生变化的位。

        6
  •  4
  •   Don Neufeld    17 年前

    这种方法的一个缺点是无法进行并行编译。你可能认为你现在得到了更快的编译,但是如果你有多个。cpp文件您可以在自己机器上的多核上并行构建它们,也可以使用分布式构建系统(如distcc或IncredBuild)来构建它们。

        7
  •  3
  •   Chris de Vries    17 年前

    你可能想退房 Lazy C++ 。它允许您将所有内容放在一个文件中,然后在编译之前运行,并将代码拆分为多个文件。h和。cpp文件。这可能会让你两全其美。

    慢速编译时间通常是由于在C++编写的系统中的过度耦合。也许您需要将代码拆分为具有外部接口的子系统。这些模块可以在单独的项目中编译。这样可以最大限度地减少系统不同模块之间的依赖性。

        8
  •  3
  •   user21714    17 年前

    你要放弃的一件事是,如果没有匿名名称空间,我将很难生活。

    我发现它们对于定义特定于类的实用程序函数非常有价值,这些函数应该在类的实现文件之外不可见。它们还可以保存系统其他部分不可见的任何全局数据,比如单例实例。

        9
  •  3
  •   Paul Nathan    17 年前

    这超出了语言的设计范围。虽然你可能有一些好处,但它最终会咬你的屁股。

    C++是为具有声明的H文件设计的,并且CPP文件具有实现。编译器是围绕这种设计构建的。

    ,人们争论这是否是一个好的架构,但这是设计。最好是把你的时间花在解决问题上,而不是重新设计C++文件体系结构的新方法。

        10
  •  2
  •   mxg    17 年前

    我喜欢思考分离的问题。h和。cpp文件的接口和实现。这个h文件包含一个或多个类和的接口描述。cpp文件包含这些实现。有时会有一些实际问题或清晰性阻碍了完全干净的分离,但这就是我的出发点。例如,为了清晰起见,我通常在类声明中内联编写小型访问器函数。较大的函数在中进行编码。cpp文件

    在任何情况下,都不要让编译时间决定如何构造程序。最好是有一个可读性和可维护性强的程序,而不是用1.5分钟而不是2分钟编译的程序。

        11
  •  2
  •   terson    17 年前

    我认为,除非您使用的是MSVC的预编译头,并且您使用的是Makefile或其他基于依赖关系的构建系统,否则在迭代构建时,使用单独的源文件应该可以更快地编译。因为,我的开发几乎总是迭代的,我更关心的是它能以多快的速度重新编译我在文件x.cpp中所做的更改,而不是我没有更改的其他20个源文件。此外,我对源文件的更改要比对API的更改频繁得多,因此它们的更改频率较低。

    关于循环依赖。我会更进一步地接受佩尔塞巴尔的建议。他有两个类,每个类都有指向对方的指针。相反,我更频繁地遇到一个类需要另一个类的情况。发生这种情况时,我会在另一个类的头文件中包含依赖项的头文件。举个例子:

    // foo.hpp
    #ifndef __FOO_HPP__
    #define __FOO_HPP__
    
    struct foo
    {
       int data ;
    } ;
    
    #endif // __FOO_HPP__
    

    .

    // bar.hpp
    #ifndef __BAR_HPP__
    #define __BAR_HPP__
    
    #include "foo.hpp"
    
    struct bar
    {
       foo f ;
       void doSomethingWithFoo() ;
    } ;
    #endif // __BAR_HPP__
    

    .

    // bar.cpp
    #include "bar.hpp"
    
    void bar::doSomethingWithFoo()
    {
      // Initialize f
      f.data = 0;
      // etc.
    }
    

    我之所以包括这个,这与循环依赖关系有点无关,是因为我觉得除了随意包含头文件之外,还有其他选择。在本例中,struct bar源文件不包括struct foo头文件。这是在头文件中完成的。这有一个优点,即使用bar的开发人员不必知道开发人员使用该头文件时需要包含的任何其他文件。

        12
  •  2
  •   Matthew Watson    17 年前

    标题中的代码的一个问题是,它必须是内联的,否则在链接包含同一标题的多个翻译单元时,会出现多个定义问题。

    最初的问题指出,项目中只有一个cpp,但如果要创建一个用于可重用库的组件,则情况并非如此。

    因此,为了尽可能创建可重用和可维护的代码,只在头文件中放入内联和可内联代码。

        13
  •  1
  •   Stefan Rådström    17 年前

    正如许多人指出的那样,这个想法有很多缺点,但为了平衡一下,提供一个专业的版本,我想说,在标题中完全包含一些库代码是有意义的,因为它将使它独立于项目中使用的其他设置。

    例如,如果一个用户试图使用不同的开源库,那么可以将它们设置为使用不同的方法链接到您的程序——一些可能使用操作系统的动态加载库代码,另一些设置为静态链接;一些可以设置为使用多线程,而另一些则不是。对于程序员来说,试图解决这些不兼容的方法可能是一项艰巨的任务,尤其是在时间有限的情况下。

    然而,当使用完全包含在标题中的库时,所有这些都不是问题。对于一个合理的、写得很好的图书馆来说,“它只是有用的”。

        14
  •  0
  •   pngaz    17 年前

    静态或全局变量的乱码甚至更不透明,可能无法调试。

    例如,计算分析的总迭代次数。

    在我的杂乱无章的文件中,将这些项目放在cpp文件的顶部可以让它们很容易找到。

    所谓“可能无法调试”,我的意思是,我会定期将这样一个全局设置放入监视窗口。因为它总是在范围内,所以无论程序计数器现在在哪里,手表窗口总是可以到达它。通过将这些变量放在头文件顶部的{}之外,可以让所有下游代码“看到”它们。如果将程序放在{}中,如果程序计数器在{}之外,则调试器将不再将它们视为“作用域”。然而,在Cpp顶部的kludge global中,即使它可能是全局的,以至于出现在链接图pdb等中,如果没有extern语句,其他Cpp文件也无法访问它,从而避免意外耦合。

        15
  •  0
  •   Greg Rogers    17 年前

    有一件事没人提过,编译大文件需要 大量 关于记忆。一次编译整个项目需要巨大的内存空间,即使可以将所有代码放在头文件中,也不可行。

        16
  •  0
  •   Doug Doug    17 年前

    如果您使用的是模板类,那么无论如何都必须将整个实现放在头中。。。

    一次性编译整个项目(通过一个base.cpp文件)应该允许类似“整个程序优化”或“跨模块优化”的内容,这仅在少数高级编译器中可用。如果您要预编译所有的代码,那么使用标准编译器是不可能做到这一点的。将cpp文件转换为对象文件,然后链接。

        17
  •  0
  •   Dr. Debasish Jana    12 年前

    面向对象编程的重要理念在于,数据隐藏导致封装类,实现对用户隐藏。这主要是为了提供一个抽象层,在这个抽象层中,类的用户主要使用可公开访问的成员函数,例如特定于实例的类型和静态类型。然后,该类的开发人员可以自由修改实际的实现,前提是这些实现不向用户公开。即使实现是私有的并在头文件中声明,更改实现也需要重新编译所有依赖的代码库。然而,如果实现(成员函数的定义)在源代码(非头文件)中,则库会发生更改,依赖的代码库需要与库的修订版本重新链接。如果该库是动态链接的,就像一个共享库一样,那么保持函数签名(接口)不变且实现发生更改也不需要重新链接。有利条件当然

    推荐文章