什么时候将小类分为头文件和cpp文件?

时间:2018-10-21 01:35:59

标签: c++ class

我知道之前已经回答过类似的问题,但是我已经搜索了stackoverflow(等等),但是对于在该程序中实例化并仅使用一次的小类,该如何处理却没有一个明确的想法。仍然将声明和实现放在单独的文件中真的很重要吗?

采用以下示例:

// timer.hpp

#pragma once
#include "presets.h" // includes #defines for u64 -> uint64_t, etc

class Timer {
    public:
        Timer() {}
        Timer(u64 elt) : elt_(elt) {}

        void startTiming() { if (NOT running_){ running_ = true;  sTime_ = GetTickCount64(); }};
        void stopTiming() { if (running_)     { running_ = false; eTime_ = GetTickCount64(); elt_ += (eTime_ - sTime_); sTime_ = eTime_; }}

        u64 getElapsed() { if (NOT running_) return elt_; eTime_ = GetTickCount64(); return elt_ + eTime_ - sTime_; }
    private:
        bool running_ = true;
        u64 elt_ = 0, eTime_ = 0, sTime_ = GetTickCount64();
};

我读过的所有东西都坚持声明和实现在单独的文件中,但是将这样一个简单的类分为.h文件和.cpp文件似乎是荒谬的。它很少更改,只能实例化一次。

我还有一些其他的类,稍大一些,并且仅在2个文件中使用一次。假设一个类仅在程序中实例化一次,我的问题是:

  1. 放置一个小的声明和实现是否合理 在一个文件中上课?如果没有,为什么不呢?
  2. 一个班级需要多大才能更好地分开 分成2个文件?

我知道这里已经存在非常类似的问题,但是我还没有读过任何对上述内容给出明确答案的问题。

3 个答案:

答案 0 :(得分:5)

  
      
  1. 将小类的声明和实现放在单个文件中是否合理?
  2.   

是的,将小类的声明和实现放在单个文件中是合理的。

  

如果没有,为什么不呢?

因为如果函数定义被修改,则依赖于类定义的所有翻译单元都需要重新编译-或更确切地说,包括该定义的所有翻译单元都需要重新编译。这包括所有确实依赖于类的对象,因为它们必须包含定义,但也包括那些无缘无故地包含定义的对象。

  
      
  1. 将一个类分成两个文件之前,一个类需要多大?
  2.   

没有硬性限制。它取决于许多变量,并且受个人偏好的影响很大。

有人将每个类的所有成员函数定义都放在一个翻译单元中,因为这是他们了解事情的方式,或者因为他们必须遵循编码标准。

其他人发誓,如果没有通过在标头中内联定义所有函数来允许优化的可能性,就无法生存,因此整个程序只有一个翻译单元。这样还可以减少从头开始的编译时间,还可以使整个项目在进行任何更改时都可以重建。

但是没有必要教条地遵循这两个路径,而且都不是最优的-两者都有缺点和优点。因此,在这些路径之间的某个地方可能是个不错的选择。但是正如我在上面说的那样,选择取决于许多变量,因此使用快速试探法而不是进行全面分析可能会更好。如果您找到合适的推理方法,可以参考以下几点:

  • 如果函数定义为空,则非常适合用于内联定义。
  • 如果函数或类是模板,则必须内联定义-您没有选择(除非可以限制可能的模板实例化数量)。
  • 如果函数的定义依赖于类的定义,否则不依赖于该类的定义。如果内联定义函数,则将导致依赖项传播。在这种情况下,最好不要内联定义函数。
  • 如果您习惯于很少修改函数的定义,并且许多翻译单元都使用了该定义,那么最好不要内联定义函数,否则您可能会发现自己一次又一次地重新编译大部分项目。如果项目很小,因此可以快速进行完整编译,则此启发式方法无关紧要。另外,使用班级的地点数量通常很难跟踪,因此假设最坏的情况通常更容易。
  • 如果对程序进行概要分析并发现每秒要调用一百万次的特定功能,则可以很好地利用内联扩展优化来进行内联定义。请注意,即使功能是在单独的翻译单元中定义的,使用链接时间优化也可以允许内联扩展。
  • 如果函数编译缓慢,则最好不要内联定义它。现在,要弄清哪些函数编译缓慢并不十分容易,因此您还需要一套启发式方法。如果找到合适的推理方法,可以参考以下几点:
    • 长函数定义通常比短函数定义慢。
    • 实例化模板的函数有时编译起来可能会很慢。
  

假设一个类仅在程序中实例化一次

实例化的数量与选择基本上无关-在这种情况下,除了内联构造函数对于性能而言可能并不重要。


P.S。

尽管它非常便携,但是#pragma once是非标准的。我并不是说您应该摆脱它;这只是要注意的事情。

GetTickCount64是特定于系统的,不可移植。 C ++在<chrono>标头中有一个标准时钟API。

答案 1 :(得分:2)

  1. 这取决于我。如果您在团队中工作,请遵循团队接受的标准。如果编码标准要求您将声明和实现拆分为单独的文件(.h和.cpp),并且这是唯一的编码方法,则应这样做。

否则,您也可以在.h文件中创建类的方式,我经常在单独的头文件中创建枚举。如果可以声明一个类并提供内联成员函数,以便同时定义该类,为什么不这样做呢?它减少了文件数量,减少了代码量,使其更简洁。

重要说明::我将提供一条注释,解释为什么我在头文件中声明和定义了一个类。其他开发人员将能够理解我的动机。

  1. 没有任何界限。一个最小的堆需要多少块石头?您能说出夜晚什么时候没有钟表吗?这只是直觉的问题。如果您有一个非常简单的类,则可以从头文件开始。如果您的班级越来越大,则可以对其进行重构,并添加源文件并将定义移到那里。

答案 2 :(得分:1)

这是另一个考虑因素:编译性能

请注意,您的多合一声明和定义文件使用了GetTickCount64。该文件需要包含Windows.h-一个相当大的野兽(尽管在您的情况下,这是间接地,通过presets.h毫无疑问),因为GetTickCount64是在Windows.h中声明的。 (如果您的计时器返回类型(u64)不依赖于Windows平台特定的类型,则这尤其重要。)

如果将声明与定义分开,则仅定义(.cpp文件)将需要包含Windows.h。并且声明(.h)不需要它。

这意味着任何包含Timer.h文件的.cpp文件都不会被强制包括Windows.h。这使所有Timer.h使用者的​​编译时间和内存需求降低了,并最终导致了更快的编译速度。 .h文件的使用者很多(项目中的许多其他.cpp文件都需要包含Timer.h文件),但是只需编译一个.cpp即可生成Timer类的定义(您的Timer .cpp),而这实际上是唯一需要包含该功能的Windows.h标头的文件。

随着应用程序大小的增长,您将希望创建越来越多的平台独立模块,这些模块依赖于简单的抽象(例如Timer类),而不是原始的Win32 / Win64 API调用(例如GetTickCount64)。您的应用程序的大部分只是您应用程序的业务逻辑,而不必依赖于包括大型API(例如Win32 API)的特定平台的标头。

第二章

此工作的下一部分将是将类的私有部分与接口的公共部分分开,以进一步隔离代码并使标头保持较小。在您的特定示例中,您的类声明中没有什么要隔离的特殊东西,但是在许多情况下,该类可能需要一个私有成员变量,而该私有变量的类型不是公共接口所关心的。无需给Timer.h头文件的使用者增加定义您可能需要在私有变量中放置的所有类型的内容的负担;仅属于您的公共API的类型。