您可以构建一个C++程序,以便(几乎)所有代码都驻留在Header文件中.它本质上看起来像一个C#或Java程序.但是,.cpp
在编译时,至少需要一个文件来提取所有头文件.现在我知道有些人绝对会厌恶这个想法.但我没有发现任何令人信服的缺点.我可以列举一些优点:
[1]编译时间更快.所有头文件只被解析一次,因为只有一个.cpp文件.此外,一个头文件不能包含多次,否则您将获得构建中断.在使用备用方法时,还有其他方法可以实现更快的编译,但这很简单.
[2]通过使它们绝对清楚,它避免了循环依赖.如果ClassA
在ClassA.h
对循环依赖ClassB
的ClassB.h
,我必须把前向参考与它伸出.(请注意,这与C#和Java不同,编译器会自动解析循环依赖关系.这会鼓励编写错误的编码实践IMO).同样,如果您的代码在.cpp
文件中,您可以避免循环依赖,但在实际项目中,.cpp
文件往往包含随机标题,直到您无法确定谁依赖于谁.
你的意见?
不在我的项目中:源文件(CPP)仅包含他们需要的标题(HPP).因此,当我因为微小的改变而需要重新编译一个CPP时,我有十倍于未重新编译的文件.
也许您应该在更多逻辑源/头中分解您的项目:A类实现中的修改不应该需要重新编译B,C,D,E等类的实现.
代码中的循环依赖?
对不起,但我还没有将这类问题作为一个真正的问题:让我们说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文件必须编译而无需包含任何其他内容
这将删除排序问题和循环依赖.
如果编译时间确实是一个问题,我会考虑:
使用预编译头文件(这对STL和BOOST非常有用)
通过PImpl惯用法减少耦合,如http://en.wikipedia.org/wiki/Opaque_pointer中所述
使用网络共享编译
你正在做的不是把所有东西放在标题中.
您基本上将所有文件都包含在一个且只有一个最终来源中.
也许你在全项目编译方面取胜.
但是当编译一个小的变化时,你总是会失败.
在编码时,我知道我经常编译很小的更改(如果只是让编译器验证我的代码),然后最后一次,做一个完整的项目更改.
如果我的项目按你的方式组织,我会失去很多时间.
我不同意第1点.
是的,只有一个.cpp,从头开始构建的时间更快.但是,你很少从头开始构建.您进行了小的更改,每次都需要重新编译整个项目.
我更喜欢这样做:
保留.h文件中的共享声明
保留仅在.cpp文件中的一个地方使用的类的定义
所以,我的一些.cpp文件开始看起来像Java或C#代码;)
但是,在设计系统时,"保持.h"方法很好,因为你做了第2点.我通常在构建类层次结构时执行此操作,稍后当代码体系结构变得稳定时,我将代码移动到.cpp文件.
你说你的解决方案有效是对的.它甚至可能对您当前的项目和开发环境没有影响.
但...
正如其他人所说,每次更改一行代码时,将所有代码放在头文件中都会强制进行完整编译.这可能不是一个问题,但您的项目可能会变得足够大,以至于编译时间将是一个问题.
另一个问题是共享代码时.虽然您可能还没有直接关注,但重要的是尽可能多地保留代码的潜在用户隐藏的代码.通过将代码放入头文件中,任何使用代码的程序员都必须查看整个代码,而对如何使用它只感兴趣.将代码放在cpp文件中只允许将二进制组件(静态或动态库)及其接口作为头文件提供,这在某些环境中可能更简单.
如果您希望能够将当前代码转换为动态库,则会出现此问题.由于您没有与实际代码分离的正确接口声明,因此您将无法将已编译的动态库及其使用接口作为可读头文件提供.
您可能还没有遇到这些问题,这就是为什么我告诉您的解决方案在您当前的环境中可能没问题.但是,随时准备应对任何变化总是更好,并且应该解决其中一些问题.
PS:关于C#或Java,您应该记住这些语言没有按照您的说法进行操作.它们实际上是独立编译文件(如cpp文件),并为每个文件全局存储接口.然后使用这些接口(以及任何其他链接接口)链接整个项目,这就是他们能够处理循环引用的原因.因为C++每个文件只进行一次编译传递,所以它无法全局存储接口.这就是为什么你需要在头文件中明确地写它们.
您误解了该语言的用途..cpp文件实际上(或者应该是内联和模板代码除外)是系统中可执行代码的唯一模块..cpp文件被编译为目标文件,然后链接在一起..h文件仅用于在.cpp文件中实现的代码的前向声明.
这导致更快的编译时间和更小的可执行文件.它看起来也相当清晰,因为你可以通过查看它的.h声明来快速了解你的课程.
对于内联和模板代码 - 因为这两者都用于由编译器生成代码而不是链接器 - 它们必须始终可用于编译器每个.cpp文件.因此,唯一的解决方案是将其包含在.h文件中.
但是,我开发了一个解决方案,我在.h文件中有我的类声明,在.inl文件中有所有模板和内联代码,在我的.cpp文件中有非模板/内联代码的所有实现..inl文件在我的.h文件底部是#included.这使事情保持清洁和一致.
对我来说明显的缺点是你总是需要一次构建所有代码.对于.cpp
文件,您可以进行单独编译,因此您只需重建真正已更改的位.