简而言之单独编译
首先,让我们来一些简单的例子:
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];当您包含特定类的标头时,通常会发生这种情况。 Functions 和 objects 必须在一个 TU 中定义一次;这通常发生在您在 **.cpp* 文件中实现它们时。但是,内联函数,包括类定义中的隐式内联函数,可以在多个 TU 中定义,但定义必须相同。
出于实用目的[2],templates(类模板和函数模板)只定义在头文件中,如果要使用单独的文件,则使用另一个标题[3].
[1] 由于最多一次的限制,标头使用包含保护来防止多次包含,从而防止出现多次定义错误。
[2]我不会在这里介绍其他可能性。
[3] 将其命名为 blahblah_detail.hpp、blahblah_private.hpp 或类似名称,如果你想证明它是非公开的。
指南
所以,虽然到目前为止我确信上面的所有内容都是一团糟,但它还不到一页就应该占用几章的内容,因此请将其用作简要参考。然而,理解上述概念很重要。使用这些,这里有一个简短的指南列表(但不是绝对规则):
-
总是在单个项目中一致地命名标题,例如 **.h* 用于 C 和 **.hpp* 用于 C++。
-
从不包含不是标题的文件。
-
总是一致地命名实现文件(将被直接编译),例如 **.c* 和 **.cpp*。
- 使用构建系统,它可以自动编译您的源文件。 make 是典型的例子,但也有很多选择。在简单的情况下保持简单。例如,make 可以使用其内置规则,甚至可以不使用 makefile。
- 使用可以生成头文件依赖项的构建系统。一些编译器可以使用命令行开关生成它,例如 -M,因此您可以轻松创建 surprisingly useful system。
构建过程
(这是回答您问题的一小部分,但您需要以上大部分内容才能到达这里。)
当你构建时,构建系统会经历几个步骤,其中重要的步骤是:
- 将每个实现文件编译为一个 TU,生成一个目标文件(**.o*、**.obj*)
- 每个都是独立编译的,这就是为什么每个 TU 都需要声明和定义
- 将这些文件以及指定的库链接到单个可执行文件中
我建议您学习 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.
}