组织与策略
不拘泥小节
- 大括号位置
- 空格和制表符
- 匈牙利记法
- 单入口,单出口
高警告级别
- 第三方头文件
- 未使用的函数参数
- 定义了从未使用过的变量
- 变量使用前可能未经初始化
- 遗漏了return语句
- 有符号数、无符号数不匹配
自动构建系统
- 增量构建
- 完全构建
版本控制系统
- 版本空值系统中的代码必须总能构建成功。
代码审查
设计风格
单实体只有一个紧凑的职责
正确、简单和清晰
可伸缩性
- 动态分配的数据
- 算法的实际复杂度
- 优先使用线性算法或者尽可能快的算法
- 尽可能避免劣于线性复杂度的算法
- 永远不要使用指数复杂度的算法
不要做不成熟的优化
- 以性能为名,使设计或代码更加复杂,降低了可读性。
不要做不成熟的劣化
- 在可以通过引用传递的时候,定义了通过值传递的参数
- 在使用前置++很适合的场景,使用了后置++
- 在构造函数中使用了赋值操作而不是用初始化列表
尽量减少全局和共享数据
- 共享数据尤其是全局数据,会增加耦合度,从而降低可维护性,通常还会降低性能。
隐藏信息
- 不要公开提供抽象的实体的内部信息
懂得何时和如何进行并发性编程
- 如果应用程序使用了多个线程或进程,应该知道如何尽量减少共享对象,以及如何安全地共享必须共享的对象。
- 最重要的问题是避免死锁,活锁和恶性的竞争条件。
- 对于需要跨线程共享数据的情况:
- 参考目标平台的文档,了解该平台的同步化原语。
- 最好将平台的原语用自己设计的抽象包装起来。
- 确保正在使用的类型在多线程程序中使用是安全的:
保证非共享的对象独立。
记载调用者在不同线程中使用该类型的同一个对象需要做什么。 - 外部加锁
调用者负责加锁 - 内部加锁
每个对象将所有对自己的访问串行化,通常采用为每个公用成员函数加锁的方法来实现,这样调用者就可以不用串行化对象的使用了。 - 不加锁的设计,包括不变性(只读对象),无需加锁。
确保资源为对象所有,显式的RAII和智能指针
- C++的”资源获取即初始化“(即RAII)惯用法是正确处理资源的利器。
- 每当需要处理一个配对的获取/释放函数调用的资源时,都应该将资源封装在一个对象中,让对象为我们强制配对,并在其析构函数中执行资源释放。
- 另外,最好用智能指针而不是原始指针来保存动态分配的资源。
- 绝对不要在一条语句中分配一个以上的资源,应该在自己的代码语句中执行显式的资源分配,而且每次都应该马上将分配的资源赋予管理对象。
编程风格
宁要编译时和链接时错误,不要运行时错误
- 在编译器做的事情,不要推迟到运行期。
- 静态检查与数据和控制流无关。
- 静态表示的模型更加可靠。
- 静态检查不会带来运行时开销。
- 有些情况可用编译时检查代替运行时检查:
- 编译时布尔条件。
- 编译时多态。
- 枚举。
- 向下强制。
积极使用const
避免使用宏
避免使用魔法数字
尽可能局部地声明变量
- 变量将引入状态,而我们应该尽可能少地处理状态,变量的生存期也是越短越好。
- 变量生命周期长的缺点:
- 会使程序更难理解和维护
- 它们的名字会污染上下文
- 不能总是被合理地初始化
总是初始化变量
- 未初始化的变量是C和C++程序中错误的常见来源。
避免函数过长,避免嵌套过深
- 尽量紧凑
- 对一个函数值赋予一个职责
- 不要自我重复
- 优先使用命名函数,不要让相似的代码片段重复出现
- 优先使用&&
- 在可以使用&&条件判断的地方要避免使用连续嵌套的if
- 不要过分使用try
- 优先使用析构函数进行自动清除而避免使用try代码块
- 优先使用标准算法
- 算法比循环嵌套要少,通常也更好
- 不要根据类型标签(try tag)进行分类(switch)
- 优先使用多态函数
避免跨编译单元的初始化依赖
- 不同的编译单元中的命名空间中的对象,绝不应该在初始化上相互依赖,因为其初始化顺序是未定义的。
尽可能减少定义性依赖,避免循环依赖
- 如果用前置声明就可以实现,就不要用include包含
- 不要相互依赖
- 循环依赖是值两个模块直接或间接的互相依赖。
头文件应该自给自足
- 应该确保所编写的每个头文件都能够独自进行编译,为此需要包含其内容所依赖的所有头文件。
总是编写内部include保护符,绝不要外部include保护符
1 2 3 4 |
#ifndef FOO_H #define FOO_H // ... #endif |
函数与操作符
正确的传递参数
- 始终用const限制所有指向只输入参数的指针和引用。
- 优先通过值来取得原始类型(如char,float等)和复制开销比较低的值对象的(如
Point
,complex<float>
)输入。 - 优先按const的引用取得其他用户定义类型的输入。
- 如果函数需要其参数的副本,可以考虑通过值传递代替通过引用传递。
- 如果参数是可选的(可以传null表示不适用或不关心),或者函数需要保存这个指针的副本或者操控参数的所有权,那么应该优先通过(智能)指针传递。
- 如果参数是必需的,而且函数无需保存指向参数的指针,或者无需操控其所有权,那么应该优先通过引用传递。
保持重载操作符的自然语义
- 只在有充分理由时才重载操作符,而且应该保持其自然语义。
- 比如不应该在某个类的operator+的实现中实现减法操作。
优先使用算术操作符和赋值操作符的标准形式
- 在定义算术操作符时,也应该提供操作符的赋值形式,并且应该尽量减少重复,提高效率。
优先使用++和--的标准形式,优先使用前缀形式
- 定义前置++,也应该定义后缀++
考虑重载以避免隐式类型转换
- 隐式类型的转换提供了语法上的便利。但是如果创建临时对象的工作并不必要而且适于优化,那么可以提供签名与参数类型精准匹配的重载函数,而且不会导致转换。
避免重载&&,||或者,(逗号)
- 不能重载这几个的主要原因是,无法在3种情况下实现内置操作符的完整语义。而程序员通常会需要这种语义。
- 具体一点,内置版本的特殊之处在于:从左到右求值,而&&和||还使用了短路特性。
不要编写依赖于函数参数求值顺序的代码
- 函数参数的求值顺序是不确定的。
类的设计与继承
弄清要编写的是哪种类
用小类代替巨类
- 小的类只体现了一个概念,粒度层次恰到好处。
- 小的类更易于理解。
- 小的类更易于部署。
- 巨类会削弱封装性。
- 巨类通常是试图预测或提供“完整”的问题解决方案而出现的。
- 巨类更难保证正确和错误安全,因为它们经常要应付多种职责。
用组合代替继承
- 继承是C++中第二紧密的耦合关系,仅次于友元。
- 如果组合就能表示类的关系,那么应该优先使用组合。
- 组合优点:
- 在不影响调试代码的情况下拥有更大的灵活性。
- 更好的编译时隔离,更短的编译时间。
- 奇异现象减少。
- 更广的适用性。
- 更健壮,更安全。
- 复杂性和脆弱性降低。
- 例外情况:
- 需要改写虚函数。
- 需要访问保护成员。
- 需要在基类之前构造已使用过的对象,或者在基类之后销毁此对象。
- 如果需要操心虚基类。
- 如果需要控制多态。
避免从并非要设计成基类的类中继承
优先提供抽象接口
- 因遵循DIP(依赖倒转)原则:
- 高层模块不应依赖于底层模块。相反,两者都应该依赖于抽象。
- 抽象不应该依赖于细节。相反,细节应该依赖于抽象。
- DIP优点:
- 更强的健壮性。
- 更大的灵活性。
- 更好的模块性。
继承即可替换性,继承是为了被重用
- 公用继承能够使基类的指针或引用实际指向某个派生类的对象,既不会破坏代码的正确性,也不需要改变已有代码。
- 不要通过公用继承重用基类中已有的代码,而是通过公用继承,让以及多态的使用了基类对象的已有代码重用当前的派生实现。
- 按照里氏替换原则:
- 公用继承所建模的必须总是"is-a"关系。
实施安全的覆盖
- 覆盖一个虚函数时,应该保持可替换性。
考虑将虚函数声明为非公用的,将公用函数声明为非虚的
-
NVI(非虚拟接口)模式:
- 将公用函数设为非虚拟的,将虚拟函数设为私有的。
-
特殊:
- NVI对析构函数不适用,因为它们的执行顺序很特殊。
要避免提供隐式转换
- 隐式转换的问题:
- 它们会在最意料不到的地方抛出异常。
- 它们并不总是能与语言的其他元素有效的配合。
- 解决:
- 默认时,为单参数构造函数加上explicit
- 使用提供转换的命名函数代替转换操作符。
将数据成员设为私有的,无行为的聚集
不要公开内部数据
- 避免返回类所管理的内部数据的句柄,这样类的客户就不会不受控制地修改对象自己拥有的状态。
明智的使用Pimpl
- 将私有部分隐藏在一个不透明的指针后面。
1 2 3 4 5 6 |
class Map { // ... private: struct Impl; shared_ptr<Impl> pimpl_; }; |
优先编写非成员非友元函数
- 非成员非友元函数通过尽量减少依赖提高了封装性:
- 函数体不能依赖于类的非公用成员。
- 它们还能够分离巨类,释放可分离的功能,进一步减少耦合。
总是一起提高new和delete
- 例外:
- operator new的就地形式
1 2 3 |
void* T::operator new(size_t, void* p) { return p; } |
如果提供类专门的new,应该提供所有标准形式
- 如果类定义了operator new的重载,则应该提供operator new的三种形式,普通(plain),就地(in-place),和不抛出(nothrow)的重载,不然类的用户就无法看到和使用它们。
构造、析构与复制
以同样的顺序定义和初始化成员变量
在构造函数中用初始化代替赋值
避免在构造函数和析构函数中调用虚函数
- 从构造函数或析构函数直接或间接调用未实现的纯虚函数,会导致未定义行为。
将基类析构函数设为公用且虚的,或者保护且非虚的
- 如果允许通过指向基类Base的指针执行删除操作,则Base的析构函数必须是公用且虚的。否则,就应该是保护且非虚的。
析构函数、释放和交换绝对不能失败
- 绝不允许析构函数、资源释放函数或者交换函数报告错误。
换句话,绝对不允许将那些析构函数可能会抛出异常的类型用于C++标准库。
一致的进行复制和销毁
- 如果定义了拷贝构造函数、拷贝复制操作符或者析构函数中任何一个,那么可能也需要定义另一个或另外两个。
显式的启用或者禁止复制
- 在以下3种行为之间需要谨慎选择:
- 使用编译器生成的拷贝构造函数和拷贝复制操作符
- 编写自己的版本
- 如果不应允许拷贝的话,显式的禁用前两者
避免切片,在基类中考虑用克隆代替拷贝
- 对象切片是自动的、不可见的,而且可能会使多态设计戛然而止。
- 在基类中,如果客户需要进行深拷贝的话,应该考虑禁止拷贝构造函数和拷贝复制操作符,而改为提供虚的Clone成员函数。
使用赋值的标准形式
- 在实现operator时,应该使用标准形式--具有特定签名的非虚形式。
只要可行,就正确的提供不会失败的swap
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ C++标准模板库编程实战_关联容器12/07
- ♥ C++14_第一篇12/14
- ♥ C++17_第三篇06/29
- ♥ CLion:配置C++下lua开发环境06/03
- ♥ C++_智能指针08/31
- ♥ C++11_第三篇12/06