【问题标题】:How can I avoid including class implementation files?如何避免包含类实现文件?
【发布时间】:2010-01-10 17:47:54
【问题描述】:

而不是做

#include "MyClass.cpp"

我想做

#include "MyClass.h"

我在网上看到这样做被认为是不好的做法。

【问题讨论】:

  • 你需要研究一下“链接”的概念。
  • 在我意识到它有多可怕之前我曾经这样做过。
  • 您需要将 .cpp 文件重命名为 .h 文件并添加包含保护。
  • ... 和一些 inline 关键字在这里和那里。
  • 唯一的问题(即使它是强制性的)是它违反了 DRY(不要重复自己)原则,因为你复制了所有方法的签名:(

标签: c++ class coding-style include


【解决方案1】:

简而言之单独编译

首先,让我们来一些简单的例子:

struct ClassDeclaration;   // 'class' / 'struct' mean almost the same thing here
struct ClassDefinition {}; // the only difference is default accessibility
                           // of bases and members

void function_declaration();
void function_definition() {}

extern int global_object_declaration;
int global_object_definition;

template<class T>           // cannot replace this 'class' with 'struct'
struct ClassTemplateDeclaration;
template<class T>
struct ClassTemplateDefinition {};

template<class T>
void function_template_declaration();
template<class T>
void function_template_definition() {}

翻译单位

翻译单元 (TU) 是一个单一的源文件(应该是一个 **.cpp* 文件)和它所包含的所有文件,以及它们所包含的等等。换句话说:预处理单个文件的结果。

标题

包含守卫是一种解决缺乏真正模块系统的技巧,使标头成为一种有限的模块;为此,多次包含同一个标头不得产生不利影响。

通过使后续的#includes no-ops 包含守卫工作,其中定义可从第一个包含中获得。由于它们的有限性质,控制标头选项的宏应该在整个项目中保持一致(像 这样的奇怪标头会导致问题),并且公共标头的所有#include 都应该在任何命名空间、类等之外,通常在任何文件的顶部。

查看我的包含守卫naming advice,包括一个短程序到generate include guards

声明

函数对象模板几乎可以在任何地方声明,可以声明任何数量次,并且必须在以任何方式引用它们之前声明。在一些奇怪的情况下,您可以在使用类时声明它们;此处不再赘述。

定义

每个TU最多可以定义一次[1];当您包含特定类的标头时,通常会发生这种情况。 Functionsobjects 必须在一个 TU 中定义一次;这通常发生在您在 **.cpp* 文件中实现它们时。但是,内联函数,包括类定义中的隐式内联函数,可以在多个 TU 中定义,但定义必须相同。

出于实用目的[2]templates(类模板和函数模板)只定义在头文件中,如果要使用单独的文件,则使用另一个标题[3].

[1] 由于最多一次的限制,标头使用包含保护来防止多次包含,从而防止出现多次定义错误。
[2]我不会在这里介绍其他可能性。
[3] 将其命名为 blahblah_detail.hppblahblah_private.hpp 或类似名称,如果你想证明它是非公开的。

指南

所以,虽然到目前为止我确信上面的所有内容都是一团糟,但它还不到一页就应该占用几章的内容,因此请将其用作简要参考。然而,理解上述概念很重要。使用这些,这里有一个简短的指南列表(但不是绝对规则):

  • 总是在单个项目中一致地命名标题,例如 **.h* 用于 C 和 **.hpp* 用于 C++。
  • 从不包含不是标题的文件。
  • 总是一致地命名实现文件(将被直接编译),例如 **.c* 和 **.cpp*。
  • 使用构建系统,它可以自动编译您的源文件。 make 是典型的例子,但也有很多选择。在简单的情况下保持简单。例如,make 可以使用其内置规则,甚至可以不使用 makefile。
  • 使用可以生成头文件依赖项的构建系统。一些编译器可以使用命令行开关生成它,例如 -M,因此您可以轻松创建 surprisingly useful system

构建过程

(这是回答您问题的一小部分,但您需要以上大部分内容才能到达这里。)

当你构建时,构建系统会经历几个步骤,其中重要的步骤是:

  1. 将每个实现文件编译为一个 TU,生成一个目标文件(**.o*、**.obj*)
    • 每个都是独立编译的,这就是为什么每个 TU 都需要声明和定义
  2. 将这些文件以及指定的库链接到单个可执行文件中

我建议您学习 make 的基本知识,因为它很受欢迎、易于理解且易于上手。但是,它是一个有几个问题的旧系统,您可能会想在某个时候切换到其他系统。

选择构建系统几乎是一种宗教体验,就像选择编辑器一样,但您必须与更多人(每个人都在同一个项目上工作)一起工作,并且可能会受到先例和惯例的更多限制。您可以使用为您处理相同细节的 IDE,但使用综合构建系统并没有真正的好处,您仍然应该知道它在后台做什么。

文件模板

example.hpp

#ifndef EXAMPLE_INCLUDE_GUARD_60497EBE580B4F5292059C8705848F75
#define EXAMPLE_INCLUDE_GUARD_60497EBE580B4F5292059C8705848F75
// all project-specific macros for this project are prefixed "EXAMPLE_"

#include <ostream> // required headers/"modules"/libraries from the
#include <string>  // stdlib, this project, and elsewhere
#include <vector>

namespace example { // main namespace for this project
template<class T>
struct TemplateExample { // for practical purposes, just put entire
  void f() {}            // definition of class and all methods in header
  T data;
};

struct FooBar {
  FooBar(); // declared
  int size() const { return v.size(); } // defined (& implicitly inline)
private:
  std::vector<TemplateExample<int> > v;
};

int main(std::vector<std::string> args); // declared
} // example::

#endif

example.cpp

#include "example.hpp" // include the headers "specific to" this implementation
// file first, helps make sure the header includes anything it needs (is
// independent)

#include <algorithm> // anything additional not included by the header
#include <iostream>

namespace example {
FooBar::FooBar() : v(42) {} // define ctor

int main(std::vector<std::string> args) { // define function
  using namespace std; // use inside function scope, if desired, is always okay
  // but using outside function scope can be problematic
  cout << "doing real work now...\n"; // no std:: needed here
  return 42;
}
} // example::

main.cpp

#include <iostream>
#include "example.hpp"

int main(int argc, char const** argv) try {
  // do any global initialization before real main
  return example::main(std::vector<std::string>(argv, argv + argc));
}
catch (std::exception& e) {
  std::cerr << "[uncaught exception: " << e.what() << "]\n";
  return 1; // or EXIT_FAILURE, etc.
}
catch (...) {
  std::cerr << "[unknown uncaught exception]\n";
  return 1; // or EXIT_FAILURE, etc.
}

【讨论】:

  • 一段时间后,我将不得不返回并编辑此内容以确保清晰。不幸的是,我不太明白如何使它更短并且仍然对您有用。我认为这是一个在不同的媒介中更好地涵盖的主题,比如一本书。
  • 是的,无法解释 C++ 编译模型比你做的要短得多。不幸的是,因为这是非常重要的信息。为我的勇敢尝试 +1 ;)
  • 如果我可能会问,外部函数作用域怎么可能导致问题?
  • @Dave:外部函数作用域是什么意思?
  • 对不起,我的意思是“范围”。我说的是如果在单个函数的范围之外使用“using”指令可能会导致的问题。
【解决方案2】:

这称为separate compilation model。您将类声明包含在需要它们的每个模块中,但定义它们only once

【讨论】:

  • 您在标题中定义类,因此它们在每个 TU中定义。 (而且 Wikipedia 对 ODR 的某些部分也存在误导和错误。)objects 和(非内联)functions 在标头中声明并仅定义一次.
  • “定义”和“声明”到底有什么区别?
  • 一个声明只引入了一个“签名”,但定义实际上是指函数或方法体。确实可以在一个语句中声明和定义一个类,但是您通常会将其放在头文件中。类也可以在标头中声明,将其方法体定义在 .cpp 文件中。
  • 示例:class Declaration; class Definition {}; void function_declaration(); void function_definition() {} extern int global_obj_declaration; int global_obj_definition;
【解决方案3】:

除了在 cpp 文件中隐藏实现细节(查看其他回复),您还可以通过类前向声明​​隐藏结构细节。

class FooPrivate;  

class Foo  
{  
  public:  
  // public stuff goes here  
  private:  
  FooPrivate *foo_private;  
};

表达式class FooPrivate 表示FooPrivate 完全定义在其他地方(最好在Foo 的实现所在的同一个文件中,在Foo 的东西出现之前。这样你可以确保实现细节Foo(Private) 不通过头文件公开。

【讨论】:

  • 它做了很多 - 我认为他在谈论 PIMPL 成语。
  • 我的错,我没有阅读完整的答案。我以为 Damg 是在谈论不透明类型,也就是 FILE
  • Dave:C++非常复杂且令人费解,但基本上保持了与 C、自身和各种工具的向后兼容性;这是一个权衡。
  • 说实话,我认为问题更多的是设计而不是想法本身,有很多事情可能会实施得更好。语法不一致且嘈杂,语义模棱两可,并且必须想出奇怪的技巧(例如“包含守卫”的东西)才能遵循软件质量标准。此外,标准库包含许多解决语言缺陷的工具(到底是什么 std::auto_ptr?),但缺乏网络和多线程等重要功能。
  • Dave,C++ 有它的缺点(主要是从 C 守卫那里继承而来),但我敦促您在更熟悉它之后再做出判断。有许多特性使该语言值得接受。
【解决方案4】:

您不需要包含 .c 或 .cpp 文件 - 编译器将编译它们,无论它们是否#included 到其他文件中。但是,如果其他文件不知道类/方法/函数/全局变量/其中包含的任何内容,则 .c/.cpp 文件中的代码将毫无用处。这就是标题发挥作用的地方。在标题中,您只放置声明,例如:

//myfile.hpp
class MyClass {
    public:
        MyClass (void);
        void myMethod (void);
        static int myStaticVar;
    private:
        int myPrivateVar;
};

现在,所有将 #include "myfile.hpp" 的 .c/.cpp 文件都将能够创建 MyClass 的实例,对 myStaticVar 进行操作并调用 MyClass::myMethod(),即使这里没有实际实现!看到了吗?

实现(实际代码)进入 myfile.cpp,在那里你告诉编译器你所有的东西是做什么的:

//myfile.cpp
int MyClass::myStaticVar = 0;

MyClass::MyClass (void) {
    myPrivateVar = 0;
}

void MyClass::myMethod (void) {
    myPrivateVar++;
}

你永远不会在任何地方包含这个文件,这绝对没有必要。

提示:创建一个 main.hpp(或 main.h,如果您愿意,没有区别)文件并将所有#includes 放在那里。然后每个 .c/.cpp 文件只需要有这一行:#include "main.hpp"。这足以访问您在整个项目中声明的所有类、方法等:)。

【讨论】:

  • 当然,带有所有#includes的main.hpp有一个缺点:单个头文件的更改将使编译器重新编译整个程序,而不仅仅是实际使用头文件的源文件: D.
  • 另一个缺点是你失去了对依赖关系的控制——通过添加#include "main.hpp",你实际上是在说一切都可以依赖于其他一切。如果你有一些有用的功能可以被另一个项目使用,那就太糟糕了。
  • 是的,这也是真的。但这仍然是一种以尽可能少的努力做事的好方法:)。
【解决方案5】:

您不应包含源文件(.c 或 .cpp)。相反,您应该包含包含声明的相应头文件(.h)。源文件需要单独编译并链接在一起才能得到最终的可执行文件。

【讨论】:

    【解决方案6】:

    Cpp 文件应在编译器脚本中定义为目标文件。

    你用的是什么ide? 我将假设您正在使用 gcc 进行编译,因此这里是将两个 .cpp 文件编译为一个可执行文件的命令

    gcc -o myclasses.out myclass.cpp myotherclass.cpp
    

    您应该只使用#include 来包含类定义,而不是实现

    【讨论】:

    • GCC 不是 IDE,它是一个编译器。 IDE 类似于 Eclipse。
    • 哎呀,纠正了误解
    【解决方案7】:

    在包含来自 .h/.hpp 的类声明时,您需要注意的一件事是确保它只包含一次。如果你不这样做,你会得到一些可能很神秘的编译器错误,这会让你陷入困境。

    为此,您需要使用#define 告诉编译器,仅当#define 不存在时才包含该文件。

    例如(MyClass.h):

    #ifndef MYCLASS_H
    #define MYCLASS_H
    class MyClass 
    {
    // Memebers and methods
    }
    #endif
    // End of file
    

    这将保证您的类声明只包含一次,即使您将它包含在许多不同的 .cpp 文件中。

    【讨论】:

    • 包含保护宏是保留的,不要使用。 stackoverflow.com/questions/1744144/…
    • “INCLUDE”并不是保留名称的原因。
    • @MadcapLaughter:尝试将包含保护宏名称更改为:MYCLASS_H,如果您愿意,也可以更改为 MYCLASS_INCLUDE
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-09-10
    • 1970-01-01
    • 1970-01-01
    • 2011-04-14
    • 2014-01-25
    • 2018-09-21
    • 2018-05-18
    相关资源
    最近更新 更多