一.头文件
1.所有的头文件都应该使用#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_
2.尽可能地避免使用前置声明。使用 #include 包含需要的头文件即可。
「前置声明」(forward declaration)是类、函数和模板的纯粹声明,没伴随着其定义。
- 类:前置声明只能作为指针或引用,尽量避免前置声明定义的类
- 函数:总是使用include
- 模板:优先使用include
3.内联函数
只有当函数只有10行甚至更少时才将其定义为内联函数。当函数被声明为内联函数之后,编译器会将其内联展开,而不是按通常的函数调用机制进行调用。
在类中定义的成员函数是内联函数(理论上,实际要看编译器实现)。
- 不要内联超过 10 行的函数. 谨慎对待析构函数, 析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用!
- 内联那些包含循环或switch语句的函数常常是得不偿失。
4.#include 的路径以及顺序
如dir/foo.cc或dir/foo_test.cc的主要作用是实现或测试dir2/foo2.h的功能,foo.cc 中包含头文件的次序如下:
- dir2/foo2.h
- C 系统文件
- C++ 系统文件
- 其他库的 .h 文件
- 本项目内 .h 文件
二、作用域
1.命名空间
使用具名的命名空间时,其名称可基于项目名或相对路径。禁止使用using指示(using-directive)。禁止使用内联命名空间(inline namespace)。
- 命名空间格式
// .h 文件
namespace mynamespace {
// 所有声明都置于命名空间中
// 注意不要使用缩进
class MyClass {
public:
...
void Foo();
};
} // namespace mynamespace
// .cc 文件
namespace mynamespace {
// 函数定义都置于命名空间中
void MyClass::Foo() {
...
}
} // namespace mynamespace
- 不要在命令空间std中声明
- 不应该使用using指示引入命令空间
// 禁止 —— 污染命名空间,并且暴露std下所有的函数
using namespace std;
using std::cin; //尽量指明具体使用函数库并限制它的可见性
- 不要在头文件中使用命令空间别名,除非限制它的可见性
// 在 .cc 中使用别名缩短常用的命名空间
namespace baz = ::foo::bar::baz;
// 在 .h 中使用别名缩短常用的命名空间
namespace librarian {
namespace impl { // 仅限内部使用
namespace sidetable = ::pipeline_diagnostics::sidetable;
} // namespace impl
inline void my_inline_function() {
// 限制在一个函数中的命名空间别名
namespace baz = ::foo::bar::baz;
...
}
} // namespace librarian
- 禁止使用内联命名空间
内联命名空间会自动把内部的标识符放到外层作用域,使得内部的成员不再受其声明所在命名空间的限制,比如:
namespace X {
inline namespace Y {
void foo();
} // namespace Y
} // namespace X
X::Y::foo() 与 X::foo() 彼此可代替。
2.匿名命名空间和静态变量
在.cc 文件中定义一个不需要被外部引用的变量时,可以将它们放在匿名命名空间或声明为static。但是不要在 .h文件中这么做。
3.非成员函数、静态成员函数和全局函数
- 如果函数需要在外部使用,能使用命名空间下的非成员函数,就不要使用静态成员函数。最忌讳是使用裸的全局函数。
有时, 把函数的定义同类的实例脱钩是有益的, 甚至是必要的. 这样的函数可以被定义成静态成员, 或是非成员函数. 非成员函数不应依赖于外部变量, 应尽量置于某个命名空间内. 相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类, 不如使用命名空间 。举例而言,对于头文件 myproject/foo_bar.h
应当使用:
namespace myproject {
namespace foo_bar {
void Function1();
void Function2();
} // namespace foo_bar
} // namespace myproject
而不是:
namespace myproject {
class FooBar {
public:
static void Function1();
static void Function2();
};
} // namespace myproject
- 如果你必须定义非成员函数,而又只是在.cc文件中使用。那么可以使用匿名命名空间或者static 来限制作用域。
4.局部变量
将函数变量尽可能置于最小作用域内,并在变量声明时进行初始化。
C++ 允许在函数的任何位置声明变量,但我们提倡在尽可能小的作用域中声明变量, 离第一次使用越近越好. 这使得代码浏览者更容易定位变量声明的位置, 了解变量的类型和初始值。
特别是应使用初始化的方式替代声明再赋值, 比如:
int i;
i = f(); // 坏——初始化和声明分离
int j = g(); // 好——初始化时声明
vector<int> v;
v.push_back(1); // 用花括号初始化更好
v.push_back(2);
vector<int> v = {1, 2}; // 好——v 一开始就初始化
属于 if, while 和 for 语句的变量应当在这些语句中正常地声明,这样子这些变量的作用域就被限制在这些语句中了,举例而言:
while (const char* p = strchr(str, '/')) str = p + 1;
有一个例外,如果变量是一个对象,每次进入作用域都要调用其构造函数,每次退出作用域都要调用其析构函数。这会导致效率降低。
// 低效的实现
for (int i = 0; i < 1000000; ++i) {
Foo f; // 构造函数和析构函数分别调用 1000000 次!
f.DoSomething(i);
}
在循环作用域外面声明这类变量要高效的多:
Foo f; // 构造函数和析构函数只调用 1 次
for (int i = 0; i < 1000000; ++i) {
f.DoSomething(i);
}
5.静态和全局变量
禁止定义静态储存周期非POD变量,禁止使用含有副作用的函数初始化POD全局变量,因为多编译单元中的静态变量执行时的构造和析构顺序是未明确的,这将导致代码的不可移植。但constexpr变量除外,毕竟它们又不涉及动态初始化或析构。
静态生存周期的对象,即包括了全局变量,静态变量,静态类成员变量和函数静态变量,都必须是原生数据类型 (POD : Plain Old Data): 即 int,char和float,以及POD类型的指针、数组和结构体。
只允许 POD 类型的静态变量,即完全禁用vector(使用C数组替代)和string(使用const char [])。
如果确实需要一个class类型的静态或全局变量,可以考虑在main() 函数或pthread_once() 内初始化一个指针且永不回收。注意只能用raw指针,别用智能指针,毕竟后者的析构函数涉及到上文指出的不定顺序问题。
三、类
1.构造函数
不要在构造函数中调用虚函数。经过构造函数完全初始化的对象可以为const类型,方便标准化使用。如果需要更有意义的初始化,可以使用init方法或者工厂模式。
2.隐式类型转换
不要使用隐式类型转换。对于转换运算符(例如operator bool())和单参数构造函数, 请使用 explicit 关键字。但是一个例外是拷贝构造函数和移动构造函数不应该被标记为explicit关键字。
3.可拷贝类型和可移动类型
如果你的类型需要, 就让它们支持拷贝/移动。 否则,就把隐式产生的拷贝和移动函数禁用。
如果定义了拷贝/移动操作,则要保证这些操作的默认实现是正确的。 记得时刻检查默认操作的正确性,并且在文档中说明类是可拷贝的且/或可移动的。
class Foo {
public:
Foo(Foo&& other) : field_(other.field) {}
// 差, 只定义了移动构造函数, 而没有定义对应的赋值运算符.
private:
Field field_;
};
由于存在对象切割的风险, 不要为任何有可能有派生类的对象提供赋值操作或者拷贝/移动构造函数 (当然也不要继承有这样的成员函数的类)。 如果你的基类需要可复制属性,请提供一个 public virtual Clone() 和一个 protected 的拷贝构造函数以供派生类实现。
如果你的类不需要拷贝/移动操作,请显式地通过在public域中使用 = delete 或其他手段禁用之。
// MyClass is neither copyable nor movable.
MyClass(const MyClass&) = delete;
MyClass& operator=(const MyClass&) = delete;
4.结构体和类
仅当只有数据成员时,使用struct,其他一概使用 class。为了和stl保持一致,对于仿函数等特性可以不用class而是使用 struct。
5.继承
优先使用组合。如果使用继承的话,定义为 public 继承。如果你想使用私有继承,你应该替换成把基类的实例作为成员对象的方式。
必要的话, 析构函数声明为virtual。 如果你的类有虚函数,则析构函数也应该为虚函数。
对于可能被子类访问的成员函数,不要过度使用protected关键字,但数据成员都必须是私有的。
对于重载的虚函数或虚析构函数, 使用override或 (较不常用的) final 关键字显式地进行标记. 较早 (早于 C++11) 的代码可能会使用 virtual 关键字作为不得已的选项。因此,在声明重载时, 请使用 override,final 或 virtual (不推荐)的其中之一进行标记. 标记为 override 或 final 的析构函数如果不是对基类虚函数的重载的话,编译会报错, 这有助于捕获常见的错误(这里跟前面一致,需要编译器支持)。
6.多继承
真正需要用到多重实现继承的情况少之又少。只在以下情况我们才允许多重继承: 最多只有一个基类是非抽象类; 其它基类都是以 Interface 为后缀的纯接口类。
7.接口
以 Interface 为后缀可以提醒其他人不要为该接口类增加函数实现或非静态数据成员. 这一点对于多重继承尤其重要.
当一个类满足以下要求时, 称之为纯接口:
- 只有纯虚函数 (“=0”) 和静态函数 (除了下文提到的析构函数).
- 没有非静态数据成员.
- 没有定义任何构造函数. 如果有, 也不能带有参数, 并且必须为 protected.
- 如果它是一个子类, 也只能从满足上述条件并以 Interface 为后缀的类继承.
接口类不能被直接实例化, 因为它声明了纯虚函数. 为确保接口类的所有实现可被正确销毁, 必须为之声明虚析构函数。
8.存取控制
将所有数据成员声明为private, 除非是static const类型成员。
9.声明顺序
类定义一般应以 public: 开始,后跟 protected:,最后是 private:.。
在各个部分中,建议将类似的声明放在一起。并且建议以如下的顺序: 类型 (包括 typedef,using 和嵌套的结构体与类),常量,工厂函数,构造函数,赋值运算符,析构函数,其它函数,数据成员。
四、函数
1.参数顺序
函数的参数顺序为:输入参数在先,后跟输出参数。
输入参数通常是值参或const引用,输出参数或输入/输出参数则一般为非const指针。在排列参数顺序时,将所有的输入参数置于输出参数之前。
2.编写简短函数
我们承认长函数有时是合理的, 因此并不硬性限制函数的长度。如果函数超过40行,可以思索一下能不能在不影响程序结构的前提下对其进行分割。
3.引用参数
所有按引用传递的参数必须加上 const。定义引用参数可以防止出现 (*pval)++ 这样丑陋的代码。引用参数对于拷贝构造函数这样的应用也是必需的,同时也更明确地不接受空指针。
函数参数列表中,所有引用参数都必须是 const:
void Foo(const string &in, string *out);
事实上这在 Google Code 是一个硬性约定:输入参数是值参或const引用, 输出参数为指针。 输入参数可以是 const指针,但决不能是非const的引用参数,除非特殊要求, 比如 swap()。
有时候, 在输入形参中用const T* 指针比 const T& 更好。 比如:
- 可能会传递空指针。
- 函数要把指针或对地址的引用赋值给输入形参。
总而言之, 大多时候输入形参往往是 const T&. 若用 const T* 则说明输入另有处理。所以若要使用 const T*,则应给出相应的理由, 否则会使得读者感到迷惑。
五、其他特性
1.异常
我们不使用 C++ 异常。
2.禁止使用RTTI
3.类型转换
使用 C++ 的类型转换, 如 static_cast<>(). 不要使用 int y = (int)x 或 int y = int(x) 等转换方式;
不要使用 C 风格类型转换,而应该使用 C++ 风格。
- 用 static_cast 替代 C 风格的值转换,或某个类指针需要明确的向上转换为父类指针时。
- 用 const_cast 去掉 const 限定符。
- 用 reinterpret_cast 指针类型和整型或其它指针之间进行不安全的相互转换。仅在你对所做一切了然于心时使用。
4.流
只在记录日志时使用流.
5.前置自增和自减
对于迭代器和其他模板对象使用前缀形式 (++i) 的自增,自减运算符。
6.const用法
const变量,数据成员,函数和参数为编译时类型检测增加了一层保障,便于尽早发现错误。因此,我们强烈建议在任何可能的情况下使用 const:
- 如果函数不会修改传你入的引用或指针类型参数,该参数应声明为const。
- 尽可能将函数声明为const。访问函数应该总是const;其他不会修改任何数据成员、未调用非const函数
不会返回数据成员非const指针或引用的函数也应该声明成const。 - 如果数据成员在对象构造之后不再发生变化,可将其定义为const。
然而,也不要发了疯似的使用const。像 const int * const * const x; 就有些过了,虽然它非常精确的描述了常量 x。 关注真正有帮助意义的信息: 前面的例子写成 const int** x 就够了。
const 的位置:
有人喜欢 int const foo 形式, 不喜欢 const int foo, 他们认为前者更一致因此可读性也更好: 遵循了 const总位于其描述的对象之后的原则。但是一致性原则不适用于此,“不要过度使用” 的声明可以取消大部分你原本想保持的一致性。将 const 放在前面才更易读,因为在自然语言中形容词 (const) 是在名词 (int) 之前.
提倡但不强制 const 在前,但要保持代码的一致性!
7.整型
C++ 没有指定整型的大小,C++ 中整型大小因编译器和体系结构的不同而不同。通常人们假定 short 是 16 位,int 是 32 位, long 是 32 位, long long 是 64 位。如果程序中需要不同大小的变量, 可以使用 <stdint.h> 中长度精确的整型, 如 int16_t。
8.预处理宏
使用宏define时要非常谨慎, 尽量以内联函数, 枚举和常量代替之.
如果你要宏, 尽可能遵守:
- 不要在 .h 文件中定义宏.
- 在马上要使用时才进行 #define, 使用后要立即 #undef.
- 不要只是对已经存在的宏使用#undef,选择一个不会冲突的名称;
- 不要试图使用展开后会导致 C++ 构造不稳定的宏, 不然也至少要附上文档说明其行为.
- 不要用 ## 处理函数,类和变量的名字。
9.0, nullptr 和 NULL
整数用 0,实数用 0.0, 指针用 nullptr 或 NULL, 字符 (串) 用 '\0'。
整数用 0, 实数用 0.0, 这一点是毫无争议的。
对于指针 (地址值),到底是用 0, NULL 还是 nullptr。 C++ 11项目用 nullptr; C++ 03 项目则用 NULL, 毕竟它看起来像指针。实际上,一些 C++ 编译器对 NULL 的定义比较特殊,可以输出有用的警告,特别是 sizeof(NULL) 就和 sizeof(0) 不一样。
字符 (串) 用 '\0', 不仅类型正确而且可读性好。
10.sizeof
尽可能用 sizeof(varname)代替sizeof(type)。使用 sizeof(varname) 是因为当代码中变量类型改变时会自动更新。
Struct data;
Struct data; memset(&data, 0, sizeof(data));
Warning
memset(&data, 0, sizeof(Struct));
11.auto
用 auto 绕过烦琐的类型名,只要可读性好就继续用,只能在局部变量中使用。
C++ 类型名有时又长又臭,特别是涉及模板或命名空间的时候。就像:
sparse_hash_map<string, int>::iterator iter = m.find(val);
返回类型好难读,代码目的也不够一目了然。重构其:
auto iter = m.find(val);
12.初始化列表
你可以用列表初始化。
早在 C++03 里,聚合类型(aggregate types)就已经可以被列表初始化了,比如数组和不自带构造函数的结构体:
struct Point { int x; int y; };
Point p = {1, 2};
C++11 中,该特性得到进一步的推广,任何对象类型都可以被列表初始化。示范如下:
(一般我们线上稳定的版本:centos7,gcc 4.8.5是不支持以下特性的)
// Vector 接收了一个初始化列表。
vector<string> v{"foo", "bar"};
// 不考虑细节上的微妙差别,大致上相同。
// 您可以任选其一。
vector<string> v = {"foo", "bar"};
// 可以配合 new 一起用。
auto p = new vector<string>{"foo", "bar"};
// map 接收了一些 pair, 列表初始化大显神威。
map<int, string> m = {{1, "one"}, {2, "2"}};
// 初始化列表也可以用在返回类型上的隐式转换。
vector<int> test_function() { return {1, 2, 3}; }
// 初始化列表可迭代。
for (int i : {-1, -2, -3}) {}
// 在函数调用里用列表初始化。
void TestFunction2(vector<int> v) {}
TestFunction2({1, 2, 3});
用户自定义类型也可以定义接收 std::initializer_list<T> 的构造函数和赋值运算符,以自动列表初始化:
class MyType {
public:
// std::initializer_list 专门接收 init 列表。
// 得以值传递。
MyType(std::initializer_list<int> init_list) {
for (int i : init_list) append(i);
}
MyType& operator=(std::initializer_list<int> init_list) {
clear();
for (int i : init_list) append(i);
}
};
MyType m{2, 3, 5, 7};
最后,列表初始化也适用于常规数据类型的构造,哪怕没有接收 std::initializer_list<T> 的构造函数。
double d{1.23};
// MyOtherType 没有 std::initializer_list 构造函数,
// 直接上接收常规类型的构造函数。
class MyOtherType {
public:
explicit MyOtherType(string);
MyOtherType(int, string);
};
MyOtherType m = {1, "b"};
// 不过如果构造函数是显式的(explict),您就不能用 `= {}` 了。
MyOtherType m{"b"};
千万别直接列表初始化 auto 变量,看下一句,估计没人看得懂:
Warning
auto d = {1.23}; // d 即是 std::initializer_list<double>
auto d = double{1.23}; // 善哉 -- d 即为 double, 并非 std::initializer_list.
13.boost库
只使用 Boost 中被认可的库.
为了向阅读和维护代码的人员提供更好的可读性, 我们只允许使用 Boost 一部分经认可的特性子集. 目前允许使用以下库:
- Call Traits : boost/call_traits.hpp
- Compressed Pair : boost/compressed_pair.hpp
- <The Boost Graph Library (BGL) : boost/graph, except serialization (adj_list_serialize.hpp) and parallel/distributed algorithms and data structures(boost/graph/parallel/* and boost/graph/distributed/*)
- Property Map : boost/property_map.hpp
- The part of Iterator that deals with defining iterators: boost/iterator/iterator_adaptor.hpp, boost/iterator/iterator_facade.hpp, and boost/function_output_iterator.hpp
- The part of Polygon that deals with Voronoi diagram construction and doesn’t depend on the rest of Polygon: boost/polygon/voronoi_builder.hpp, boost/polygon/voronoi_diagram.hpp, and boost/polygon/voronoi_geometry_type.hpp
- Bimap : boost/bimap
- Statistical Distributions and Functions : boost/math/distributions
- Multi-index : boost/multi_index
- Heap : boost/heap
- The flat containers from Container: boost/container/flat_map, and boost/container/flat_set
14.c++11
c++ 11 相对于前身,复杂极了:1300 页 vs 800 页!
很多开发者也不怎么熟悉它。于是从长远来看,前者特性对代码可读性以及维护代价难以预估。我们说不准什么时候采纳其特性,特别是在被迫依赖老旧工具的项目上。
和 6.23. Boost 库 一样,有些 C++ 11扩展提倡实则对可读性有害的编程实践——就像去除冗余检查(比如类型名)以帮助读者,或是鼓励模板元编程等等。有些扩展在功能上与原有机制冲突,容易招致困惑以及迁移代价。
缺点:
C++ 11特性除了个别情况下,可以用一用。除了本指南会有不少章节会加以讨若干C++11特性之外,以下特性最好不要用:
- 尾置返回类型,比如用 auto foo() -> int 代替 int foo(). 为了兼容于现有代码的声明风格。
- 编译时合数 <ratio>, 因为它涉及一个重模板的接口风格。
- <cfenv> 和 <fenv.h> 头文件,因为编译器尚不支持。
- 默认 lambda 捕获。
网友评论