翻译自:https://docs.microsoft.com/en-us/cpp/cpp/header-files-cpp?view=vs-2019
参考:https://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/headers/
程序中各元素在使用前必须被声明,例如:变量,函数,类等。元素的声明告诉编译器该元素的类型,是int,double,函数,还是类。进一步地说,在每个.cpp文件中使用地元素必须被声明(直接或间接)。
当你编译一个程序时,每个.cpp文件被独立地编译未一个compilation unit编译单元。编译器并不知道其他编译单元里有哪些名称被声明过滤。这也就意味着,如果你定义一个类或函数,或全局变量,你必须在每个使用该对象的.cpp文件中提供该对象地声明。在所有文件中该对象的声明必须是一致的。当linker链接器尝试着将所有编译单元合成为一个单独的程序时,稍微的不一致就会导致错误,或预料外的行为。
为了使得错误的可能性最小化,C++采用了header file头文件的行驶来包含声明。你在头文件中声明变量,然后在需要该声明的每个.cpp文件中使用#include包含该头文件。在编译时,#include标识符将头文件的一个拷贝直接插入到.cpp文件中。
- Note:
在Visual Studio 2019,C++20的module特性用于提升并最终替代头文件。
- 例子:
下面的例子展示了声明一个类并在另外一个源文件中使用这个类的典型方式。我们首先创建一个header file,它包含类的声明,但是要注意该类的定义并不完整,因为do_something函数并没有定义:
// my_class.h namespace N { class my_class { public: void do_something(); }; }
下一步,创建一个实现文件(典型的是一个.cpp文件,或类似的文件扩展名)。我们将调用my_class.cpp并为成员函数提供定义。在.cpp文件中通过#include标识符包含my_class.h来获取my_class的声明。注意,头文件与.cpp在同一目录下的,使用#inlcude "",而使用标准库的头文件则使用<>。
在.cpp实现文件中,我们通过使用using来简化各个类型的书写,如果不使用using namespace std的话,string就必须写成:std::string。但是注意:不要在头文件中使用using!
// my_class.cpp #include "my_class.h" // header in local directory #include <iostream> // header in standard library using namespace N; using namespace std; void my_class::do_something() { cout << "Doing something!" << endl; }
现在,我们可以在另外一个.cpp文件中使用my_class了。通过#include包含头文件,这样编译器可以从头文件中获取各个声明。编译器需要知道的是my_class是一个类且有一个成员函数:do_something()。
// my_program.cpp #include "my_class.h" using namespace N; int main() { my_class mc; mc.do_something(); return 0; }
在编译器完成对每个.cpp文件的编译后,生成.obj文件,编译器将.obj文件传递给链接器linker。当链接器合并各个.obj文件时,它会找到my_class的一个声明,在my_class.cpp生成的.obj中,然后程序可编译成功。
- Include guards
典型情况下,头文件使用include guard(即下面的#ifndef---#define---#endif)或#pragma once来确保每个.cpp文件只引用了一次头文件。
// my_class.h #ifndef MY_CLASS_H // include guard #define MY_CLASS_H namespace N { class my_class { public: void do_something(); }; } #endif /* MY_CLASS_H */
- 头文件中要放哪些东西
一个头文件可能被多个文件引用,因而在头文件中不能针对同一个name有多个定义。下列行为不建议在头文件中声明:
- 命名空间或全局范围内的内置类型定义
- non-inline函数的定义(应该指的是函数的实现)
- non-const变量的定义
- aggregate definitions
- 未命名的命名空间
- 使用using
在头文件中使用using不会产生错误,但是可能引入问题。因为直接或间接引用该头文件的.cpp文件都可以访问该namespace,这就可能导致不同命名空间内变量名的冲突。
- sample header file
The following example shows the various kinds of declarations and definitions that are allowed in a header file:
// sample.h #pragma once #include <vector> // #include directive #include <string> namespace N // namespace declaration { inline namespace P { //... } enum class colors : short { red, blue, purple, azure }; const double PI = 3.14; // const and constexpr definitions constexpr int MeaningOfLife{ 42 }; constexpr int get_meaning() { static_assert(MeaningOfLife == 42, "unexpected!"); // static_assert return MeaningOfLife; } using vstr = std::vector<int>; // type alias extern double d; // extern variable #define LOG // macro definition #ifdef LOG // conditional compilation directive void print_to_log(); #endif class my_class // regular class definition, { // but no non-inline function definitions friend class other_class; public: void do_something(); // definition in my_class.cpp inline void put_value(int i) { vals.push_back(i); } // inline OK private: vstr vals; int i; }; struct RGB { short r{ 0 }; // member initialization short g{ 0 }; short b{ 0 }; }; template <typename T> // template definition class value_store { public: value_store<T>() = default; void write_value(T val) { //... function definition OK in template } private: std::vector<T> vals; }; template <typename T> // template declaration class value_widget; }
#define保护
所有头文件都应该使用 #define
来防止头文件被多重包含, 命名格式当是: <PROJECT>_<PATH>_<FILE>_H_
.
为保证唯一性, 头文件的命名应该基于所在项目源代码树的全路径. 例如, 项目 foo
中的头文件 foo/src/bar/baz.h
可按如下方式保护:
#ifndef FOO_BAR_BAZ_H_ #define FOO_BAR_BAZ_H_ ... #endif // FOO_BAR_BAZ_H_
内联函数
只有当函数只有10行甚至更少的时候才将其定义为内联函数。
定义:
当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用.
优点:
只要内联的函数体较小, 内联该函数可以令目标代码更加高效. 对于存取函数以及其它函数体比较短, 性能关键的函数, 鼓励使用内联.
缺点:
滥用内联将导致程序变得更慢. 内联可能使目标代码量或增或减, 这取决于内联函数的大小. 内联非常短小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增加代码大小. 现代处理器由于更好的利用了指令缓存, 小巧的代码往往执行更快。
结论:
一个较为合理的经验准则是, 不要内联超过 10 行的函数. 谨慎对待析构函数, 析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用!
另一个实用的经验准则: 内联那些包含循环或
switch
语句的函数常常是得不偿失 (除非在大多数情况下, 这些循环或switch
语句从不被执行).有些函数即使声明为内联的也不一定会被编译器内联, 这点很重要; 比如虚函数和递归函数就不会被正常内联. 通常, 递归函数不应该声明成内联函数.(YuleFox 注: 递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数). 虚函数内联的主要原因则是想把它的函数体放在类定义内, 为了图个方便, 抑或是当作文档描述其行为, 比如精短的存取函数.
#include的路径及顺序
使用标准的头文件包含顺序可增强可读性, 避免隐藏依赖: 相关头文件, C 库, C++ 库, 其他库的 .h, 本项目内的 .h.
例如在某个实现文件中,包含的头文件顺序如下:
dir2/foo2.h
(优先位置, 详情如下) -> 该实现文件对应的头文件- C 系统文件
- C++ 系统文件
- 其他库的
.h
文件 - 本项目内
.h
文件
例如:
#include "foo/public/fooserver.h" // 优先位置 #include <sys/types.h> #include <unistd.h> #include <hash_map> #include <vector> #include "base/basictypes.h" #include "base/commandlineflags.h" #include "foo/public/bar.h"
并且在不同类的头文件间添加空行,便于阅读。
有时,平台特定(system-specific)代码需要条件编译(conditional includes),这些代码可以放到其它 includes 之后。当然,您的平台特定代码也要够简练且独立,比如:
#include "foo/public/fooserver.h" #include "base/port.h" // For LANG_CXX11. #ifdef LANG_CXX11 #include <initializer_list> #endif // LANG_CXX11