Table of Contents
- 1 书籍信息
- 2 Rule 0: 不要因为小事纠结
- 3 Rule 1: 使用最高警报级别编译
- 4 Rule 16: 尽量不使用宏
- 5 Rule 19: 总是初始化变量
- 6 Rule 25: 传参规则
- 7 Rule 29: 重载拷贝构造函数避免隐式类型转换
- 8 Rule 32: 明确类的类型
- 8.1 值类
- 8.2 基类
- 8.3 traits类
- 8.4 异常类
- 9 Rule 34: 使用组合代替继承
- 10 Rule 35: 避免从并非设计成基类的类中继承
- 11 Rule 37: 公用继承即可替代性。继承,不是为了重用,而是为了被重用
- 12 Rule 38: 实施安全的覆盖
- 13 Rule 39: NVI(Non-Virtual Interface)机制
- 14 Rule 40: 避免提供隐式类型转换
- 15 Rule 43: Pimpl惯用法
- 16 Rule 44: 优先编写非成员非友元函数
- 17 Rule 46: 如果提供专门的new,应该提供所有标准形式
- 17.1 new的3种类型
- 17.1.1 普通new
- 17.1.2 in-place new
- 17.1.3 nothrow new
- 17.1 new的3种类型
- 18 Rule 47: 以同样的顺序定义和初始化成员变量
- 19 Rule 48: 在构造函数中用初始化代替赋值
- 20 Rule 49: 避免在构造函数和析构函数中调用virtual函数
- 21 Rule 50: 总是为类编写析构函数,因为模式的是public且非virtual
- 22 Rule 51: 析构函数、释放和交换绝不能失败
- 23 Rule 52: 如果定义了拷贝构造函数、赋值操作符或者析构函数中的任一个,那么也应该定义另外两个
- 24 Rule 54: 为了避免切片,显示提供nvi风格的Clone函数,并且禁止拷贝
- 25 Rule 55: 使用赋值的标准形式
- 26 Rule 59: 不要在头文件中或者#include之前编写名字空间using
- 27 Rule 60: 不要把申请和释放内容放到不同模块
- 28 Rule 61: 不要在头文件中定义链接实体
- 29 Rule 62: 不要允许异常跨模块传播
- 30 Rule 68: 正确使用断言
- 31 Rule 73: 异常通过值抛出,通过引用捕获
- 32 Rule 75: 避免使用异常规范
- 33 Rule 76: 模式时使用vector。否则,选择其它合适容器
- 34 Rule 78: 使用vector(和string::c_str)与非C++ API交换数据
- 35 Rule 79: 容器中只存值和智能指针
- 36 Rule 82: 小技巧:释放容器多余空间、删除容器内容
- 37 Rule 95: 不要使用C风格的强制类型转换
- 38 Rule 96: 不要对类对象使用memcpy或者memcmp
1 书籍信息
- 书名:C++ Coding Standards, 101 Rules, Guidelines, and Best Practices
- 作者:Herb Sutter / Andrei Alexandrescu
2 Rule 0 : 不要因为小事纠结
我们建立代码规范的目的是提高代码质量,而不是为了把工程师约束成代码机器。所以,需要避免将一些旁枝末节的东西纳入到规范中来。下面是一些常见的例子:
- 不必规定缩进的风格 - 保证一个项目中使用统一的缩进风格即可
- 不必规定行的长度 - 只要代码可读性好就成
- 不必过度渴求命名 - 通俗易懂即可
- 不必规定注释风格 - 除了要使用doxygen这类工具生成文档的情况
3 Rule 1 : 使用最高警报级别编译
例如:-Wall -Werror
4 Rule 16 : 尽量不使用宏
在C++中,宏可以用如下语言特性替换:
- const或者enum定义常量
- inline避免函数调用的开销
- template指定函数系列和类型系列
- namespace避免名字冲突
5 Rule 19 : 总是初始化变量
6 Rule 25 : 传参规则
对于只输入参数:
- 使用const
- 对于复制开销低的类型,优先通过值传递
- 如果函数需要获得参数的副本,使用值传递
对于输入输出参数:
- 如果需要保存指针副本,使用(智能)指针
- 如果无需保存指针副本,使用引用
7 Rule 29 : 重载拷贝构造函数避免隐式类型转换
8 Rule 32 : 明确类的类型
8.1 值类
- 有一个public的析构函数、拷贝构造函数、赋值操作符
- 没有virtual函数(包括析构函数)
- 是用作具体类,不是基类(Rule 35)
- 总在栈中实例化,或者作为一个类的成员实例化
8.2 基类
- 有一个public且virtual,或者是protected且非virtual的析构函数(Rule 50)
- 有一个非public的拷贝构造函数和赋值操作符(Rule 53)
8.3 traits类
- 只包含typedef和静态函数,没有可修改的状态或者virtual函数
- 通常不实例化(构造函数被禁止)
8.4 异常类
- 有一个public的析构函数和不会失败的构造函数(特别是一个不会失败的拷贝构造函数。从异常的拷贝构造函数抛出将使程序中止)
- 有virtual函数,经常实现克隆(Rule 54)和访问(visitation)
- 从std::exception虚拟派生更好
9 Rule 34 : 使用组合代替继承
继承是仅次于友元的第二紧密的的耦合关系。如仅仅为了代码复用,使用组合即可。和继承相比,组合有如下好处:
- 不影响调用代码的情况下具有更大的灵活性 。(Rule 37)
- 更好的编译时隔离,更短的编译时间 。使用组合能减少对头文件的依赖,因为在定义中使用某个类的指针,不需要完整的类定义。但是继承需要引入完整的头文件。
- 奇异现象减少 。(Rule 58)。
- 更广的适用性 。有些类一开始并不是想设计成基类(Rule 35)。但是,大多数类都能充当一个成员的角色。
- 复杂性和脆弱性降低 。继承会带来更多额外的复杂情况,比如名字隐藏。
在下面的情况中,使用继承会更适合:
- 使用公用继承模拟可替代性 。(Rule 37)
- 如果需要改写virtual函数
- 如果需要访问protected成员
- 如果需要在基类之前构造已使用过的对象,或者在基类之后销毁此对象
- 如果需要控制多态
10 Rule 35 : 避免从并非设计成基类的类中继承
一个被设计成基类的类需要做一些额外的工作的(Rule 32、Rule 50、Rule 54)。 以std::string为例,若想扩充它的功能(做一个super_string),定义自由函数(非成员)要比继承它好得多,因为:
- 自由函数对于已经使用了string的遗留代码工作良好,super_string则需要重构整块代码,将类型和函数签名改为super_string
- 以string为参数的接口需要做改动(3选1):a)避开super_string提供的功能;b)复制参数为super_string;c)将string的引用强制转换成super_string。都很搓
- super_string的新增成员函数并不会比自由函数有更大的访问权限。因为string也许并没有被设计成可继承,那么成员可能是private
- super_string若隐藏(重新定义了一个非virtual成员函数)了一个string的成员,会导致功能异常
使用自由函数来扩展,需要注意:
- 要将这些函数放在被扩展类同一个名字空间中(Rule 57)
11 Rule 37 : 公用继承即可替代性。继承,不是为了重用,而是为了被重用
12 Rule 38 : 实施安全的覆盖
- 覆盖应该保持被覆盖函数的约定,如:是否一定成功、保持参数的默认值等
- 谨防不小心隐藏了重载函数
class Base { public: virtual void Foo(int a) {printf("%d\n", a);} virtual void Foo(int a, int b) {printf("%d%d\n", a, b);} virtual void Foo(int a, int b, int c) {printf("%d%d%d\n", a, b,c);} }; class Derived : public Base { public: virtual void Foo(int a) {printf("%d\n", a);} }; int main(int argc, char *argv[]) { Derived d; d.Foo(1); // 正确 d.Foo(1, 2); // 错误 d.Foo(1, 2, 3); // 错误 return 0; }
上面的代码里面,由于派生类隐藏了基类的2个Foo函数,导致这2个函数的调用是非法的,不能编译。可以使用using来暴露函数:
class Derived : public Base { public: virtual void Foo(int a) {printf("%d\n", a);} // 覆盖Base::Foo(int) using Base::Foo; // 将其它Base::Foo重载函数引入作用域 };
13 Rule 39 : NVI(Non-Virtual Interface)机制
公用的virtual函数有2个相互矛盾的职责:
- 指定接口
- 提供实现细节
NVI的目的是将接口控制权收回给基类。下面是一个NVI的例子:
class Base {
public:
Base() {};
int Work(void) {
printf("in base\n");
return DoWork();
}
private:
virtual int DoWork(void) = 0;
};
class Derived : public Base {
public:
Derived() {};
private:
virtual int DoWork(void) {
return 2;
}
};
NVI将控制权收回到了基类,这样能在编写基类的时候,编写一些前置或后置的代码,而不用担心会被子类给覆盖掉。
14 Rule 40 : 避免提供隐式类型转换
对于只有一个参数的构造函数,加上 explicit 关键字。
15 Rule 43 : Pimpl惯用法
代码举例如下:
class Map { private: struct Impl; shared_ptr<Impl> pimpl_; };
使用pimpl存储所有的私有成员,包括数据成员和成员函数。这样就能 随意改变类的私有实现细节,而不用重新编译调用代码
16 Rule 44 : 优先编写非成员非友元函数
将不依赖于类私有成员的函数从类里面独立出来。
17 Rule 46 : 如果提供专门的new,应该提供所有标准形式
17.1 new的3种类型
17.1.1 普通new
void *operator new(std::size_t);
17.1.2 in-place new
void *operator new(std::size_t, void*);
17.1.3 nothrow new
void *operator new(std::size_t, std::nothrow_t) throw();
18 Rule 47 : 以同样的顺序定义和初始化成员变量
C++是按照定义的顺序初始化成员变量的,和构造函数初始化列表的顺序无关。如下面代码:
class A { private: string a_, b_, c_; public: A(const char *a, const char *b, const char *c) : b_(b), c_(c), a_(b_ + c_) {} };
这段代码能通过编译,但是运行时候会报错。
19 Rule 48 : 在构造函数中用初始化代替赋值
class A { string s1_, s2_; public: A() {s1_ = "hello"; s2_ = "world";} };
这代码实际上是:
class A { string s1_, s2_; public: A() : s1_(), s2_() {s1_ = "hello"; s2_ = "world";}
那么最优的写法:
class A { string s1_, s2_; public: A() : s1_("hello"), s2_("world") {} };
20 Rule 49 : 避免在构造函数和析构函数中调用virtual函数
下面代码,能编译通过,但是运行时会报错:
class Base { public: Base() {DoWork();}; virtual void DoWork() = 0; }; class Derived : public Base { public: Derived() {} virtual void DoWork() {printf("DoWork\n");} };
但是有的情况就需要后构造,可以采取如下方法:
- 一个bool的成员变量表示类的初始化状态,显示提供一个init的函数,执行后构造。在文档里面规定,调用者必须在类构造之后,调用init函数。(比较搓)
- 工厂方法,示例代码如下:
class Base { protected: Base() {} virtual Init() {} // 构造之后立即调用 public: template<class T> static shared_ptr<T> Create() { shared_ptr<T> p(new T); p->Init(); return p; } }; class Derived : public Base { /*****/ } int main(int argc, char *argv[]) { shared_ptr<D> p = D::Create<D>(); }
21 Rule 50 : 总是为类编写析构函数,因为模式的是public且非virtual
22 Rule 51 : 析构函数、释放和交换绝不能失败
这些函数应该是总能捕获异常的,不让异常传播到函数以外。
23 Rule 52 : 如果定义了拷贝构造函数、赋值操作符或者析构函数中的任一个,那么也应该定义另外两个
24 Rule 54 : 为了避免切片,显示提供nvi风格的Clone函数,并且禁止拷贝
class Base { public: Base* Clone() const { B *p = DoClone(); assert(typeid(*p) == typeid(*this) && "DoClone incorrectly overridden"); return p; protected: B(const B&); private: virtual B *DoClone() const = 0; } };
25 Rule 55 : 使用赋值的标准形式
T& operator=(const T&); T& operator=(T);
- 不要把返回值定义为const T&,否则将不能和标准库一起使用
- 赋值函数需要考虑参数就是对象本身的情况
- 注意区分 成员赋值函数 和 独立赋值函数
26 Rule 59 : 不要在头文件中或者#include之前编写名字空间using
27 Rule 60 : 不要把申请和释放内容放到不同模块
28 Rule 61 : 不要在头文件中定义链接实体
链接实体,放到头文件中会链接错误,诸如以下:
int age; void Foo() { /*****/ }
但以下例外:
- 内联函数
- 函数模板
- 类static数据成员
此外, Schwarz计数器 或 灵巧计数器 等全局数据初始化技术要求将静态(或匿名名字空间中的)数据。cin、cout、cerr、clog中使用了此类技术。
29 Rule 62 : 不要允许异常跨模块传播
30 Rule 68 : 正确使用断言
- 通过定义NDEBUG屏蔽assert
- 要避免使用
assert(false);
使用:
assert(!"Info Message");
31 Rule 73 : 异常通过值抛出,通过引用捕获
32 Rule 75 : 避免使用异常规范
33 Rule 76 : 模式时使用vector。否则,选择其它合适容器
vector具有一下特性:
- 保证具有所有容器中最低的空间开销
- 保证具有所有容器中对所存元素进行存取的速度最快
- 保证具有与身俱来的引用局部性。容器中相邻对象保证在内存中也相邻
- 保证具有与C语言兼容的内存布局。vector和string::c_str都可以传递给C语言的API。Rule 78
- 保证具有所有容器中最灵活的迭代器(随机访问迭代器)
- 几乎肯定具有最快的迭代器
34 Rule 78 : 使用vector(和string::c_str)与非C++ API交换数据
35 Rule 79 : 容器中只存值和智能指针
36 Rule 82 : 小技巧:释放容器多余空间、删除容器内容
container<T>(c).swap(c); // 去除多余容量的shrink-to-fit(压缩到合适)惯用法 container<T>().swap(c); // 去除全部内容和容量的惯用法
remove并不真正从容器中删除元素。需要remove之后再erase才能删除。
37 Rule 95 : 不要使用C风格的强制类型转换
反面例子:
extern void Fun(Derived*); void Gun(Base *pb) { Derived *pd = (Derived*)pb; Fun(pd); }
而应该:
extern void Fun(Derived*); void Gun(Base *pb) { Derived *pd = static_cast<Derived*>(pb); // 或者 = dynamic_cast<Derived*>(pb); Fun(pd); }
38 Rule 96 : 不要对类对象使用memcpy或者memcmp
memcpy和memcmp会破坏类型系统
Date: Thu Jun 14 19:23:07 2012
Org version 7.8.11 with Emacs version 24