• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2022-01-05 21:39 Aet 隐藏边栏 |   抢沙发  14 
文章评分 3 次,平均分 5.0

组织与策略

不拘泥小节

  1. 大括号位置
  2. 空格和制表符
  3. 匈牙利记法
  4. 单入口,单出口

高警告级别

  1. 第三方头文件
  2. 未使用的函数参数
  3. 定义了从未使用过的变量
  4. 变量使用前可能未经初始化
  5. 遗漏了return语句
  6. 有符号数、无符号数不匹配

自动构建系统

  1. 增量构建
  2. 完全构建

版本控制系统

  1. 版本空值系统中的代码必须总能构建成功。

代码审查

设计风格

单实体只有一个紧凑的职责

正确、简单和清晰

可伸缩性

  1. 动态分配的数据
  2. 算法的实际复杂度
  3. 优先使用线性算法或者尽可能快的算法
  4. 尽可能避免劣于线性复杂度的算法
  5. 永远不要使用指数复杂度的算法

不要做不成熟的优化

  1. 以性能为名,使设计或代码更加复杂,降低了可读性。

不要做不成熟的劣化

  1. 在可以通过引用传递的时候,定义了通过值传递的参数
  2. 在使用前置++很适合的场景,使用了后置++
  3. 在构造函数中使用了赋值操作而不是用初始化列表

尽量减少全局和共享数据

  1. 共享数据尤其是全局数据,会增加耦合度,从而降低可维护性,通常还会降低性能。

隐藏信息

  1. 不要公开提供抽象的实体的内部信息

懂得何时和如何进行并发性编程

  1. 如果应用程序使用了多个线程或进程,应该知道如何尽量减少共享对象,以及如何安全地共享必须共享的对象。
  2. 最重要的问题是避免死锁,活锁和恶性的竞争条件。
  3. 对于需要跨线程共享数据的情况:
    1. 参考目标平台的文档,了解该平台的同步化原语。
    2. 最好将平台的原语用自己设计的抽象包装起来。
    3. 确保正在使用的类型在多线程程序中使用是安全的:
      保证非共享的对象独立。
      记载调用者在不同线程中使用该类型的同一个对象需要做什么。
    4. 外部加锁
      调用者负责加锁
    5. 内部加锁
      每个对象将所有对自己的访问串行化,通常采用为每个公用成员函数加锁的方法来实现,这样调用者就可以不用串行化对象的使用了。
    6. 不加锁的设计,包括不变性(只读对象),无需加锁。

确保资源为对象所有,显式的RAII和智能指针

  1. C++的”资源获取即初始化“(即RAII)惯用法是正确处理资源的利器。
  2. 每当需要处理一个配对的获取/释放函数调用的资源时,都应该将资源封装在一个对象中,让对象为我们强制配对,并在其析构函数中执行资源释放。
  3. 另外,最好用智能指针而不是原始指针来保存动态分配的资源。
  4. 绝对不要在一条语句中分配一个以上的资源,应该在自己的代码语句中执行显式的资源分配,而且每次都应该马上将分配的资源赋予管理对象。

编程风格

宁要编译时和链接时错误,不要运行时错误

  1. 在编译器做的事情,不要推迟到运行期。
    1. 静态检查与数据和控制流无关。
    2. 静态表示的模型更加可靠。
    3. 静态检查不会带来运行时开销。
  2. 有些情况可用编译时检查代替运行时检查:
    1. 编译时布尔条件。
    2. 编译时多态。
    3. 枚举。
    4. 向下强制。

积极使用const

避免使用宏

避免使用魔法数字

尽可能局部地声明变量

  1. 变量将引入状态,而我们应该尽可能少地处理状态,变量的生存期也是越短越好。
  2. 变量生命周期长的缺点:
    1. 会使程序更难理解和维护
    2. 它们的名字会污染上下文
    3. 不能总是被合理地初始化

总是初始化变量

  1. 未初始化的变量是C和C++程序中错误的常见来源。

避免函数过长,避免嵌套过深

  1. 尽量紧凑
    1. 对一个函数值赋予一个职责
  2. 不要自我重复
    1. 优先使用命名函数,不要让相似的代码片段重复出现
  3. 优先使用&&
    1. 在可以使用&&条件判断的地方要避免使用连续嵌套的if
  4. 不要过分使用try
    1. 优先使用析构函数进行自动清除而避免使用try代码块
  5. 优先使用标准算法
    1. 算法比循环嵌套要少,通常也更好
  6. 不要根据类型标签(try tag)进行分类(switch)
    1. 优先使用多态函数

避免跨编译单元的初始化依赖

  1. 不同的编译单元中的命名空间中的对象,绝不应该在初始化上相互依赖,因为其初始化顺序是未定义的。

尽可能减少定义性依赖,避免循环依赖

  1. 如果用前置声明就可以实现,就不要用include包含
  2. 不要相互依赖
    1. 循环依赖是值两个模块直接或间接的互相依赖。

头文件应该自给自足

  1. 应该确保所编写的每个头文件都能够独自进行编译,为此需要包含其内容所依赖的所有头文件。

总是编写内部include保护符,绝不要外部include保护符

函数与操作符

正确的传递参数

  1. 始终用const限制所有指向只输入参数的指针和引用。
  2. 优先通过值来取得原始类型(如char,float等)和复制开销比较低的值对象的(如Pointcomplex<float>)输入。
  3. 优先按const的引用取得其他用户定义类型的输入。
  4. 如果函数需要其参数的副本,可以考虑通过值传递代替通过引用传递。
  5. 如果参数是可选的(可以传null表示不适用或不关心),或者函数需要保存这个指针的副本或者操控参数的所有权,那么应该优先通过(智能)指针传递。
  6. 如果参数是必需的,而且函数无需保存指向参数的指针,或者无需操控其所有权,那么应该优先通过引用传递。

保持重载操作符的自然语义

  1. 只在有充分理由时才重载操作符,而且应该保持其自然语义。
    1. 比如不应该在某个类的operator+的实现中实现减法操作。

优先使用算术操作符和赋值操作符的标准形式

  1. 在定义算术操作符时,也应该提供操作符的赋值形式,并且应该尽量减少重复,提高效率。

优先使用++和--的标准形式,优先使用前缀形式

  1. 定义前置++,也应该定义后缀++

考虑重载以避免隐式类型转换

  1. 隐式类型的转换提供了语法上的便利。但是如果创建临时对象的工作并不必要而且适于优化,那么可以提供签名与参数类型精准匹配的重载函数,而且不会导致转换。

避免重载&&,||或者,(逗号)

  1. 不能重载这几个的主要原因是,无法在3种情况下实现内置操作符的完整语义。而程序员通常会需要这种语义。
  2. 具体一点,内置版本的特殊之处在于:从左到右求值,而&&和||还使用了短路特性。

不要编写依赖于函数参数求值顺序的代码

  1. 函数参数的求值顺序是不确定的。

类的设计与继承

弄清要编写的是哪种类

用小类代替巨类

  1. 小的类只体现了一个概念,粒度层次恰到好处。
  2. 小的类更易于理解。
  3. 小的类更易于部署。
  4. 巨类会削弱封装性。
  5. 巨类通常是试图预测或提供“完整”的问题解决方案而出现的。
  6. 巨类更难保证正确和错误安全,因为它们经常要应付多种职责。

用组合代替继承

  1. 继承是C++中第二紧密的耦合关系,仅次于友元。
  2. 如果组合就能表示类的关系,那么应该优先使用组合。
  3. 组合优点:
    1. 在不影响调试代码的情况下拥有更大的灵活性。
    2. 更好的编译时隔离,更短的编译时间。
    3. 奇异现象减少。
    4. 更广的适用性。
    5. 更健壮,更安全。
    6. 复杂性和脆弱性降低。
  4. 例外情况:
    1. 需要改写虚函数。
    2. 需要访问保护成员。
    3. 需要在基类之前构造已使用过的对象,或者在基类之后销毁此对象。
    4. 如果需要操心虚基类。
    5. 如果需要控制多态。

避免从并非要设计成基类的类中继承

优先提供抽象接口

  1. 因遵循DIP(依赖倒转)原则:
    1. 高层模块不应依赖于底层模块。相反,两者都应该依赖于抽象。
    2. 抽象不应该依赖于细节。相反,细节应该依赖于抽象。
  2. DIP优点:
    1. 更强的健壮性。
    2. 更大的灵活性。
    3. 更好的模块性。

继承即可替换性,继承是为了被重用

  1. 公用继承能够使基类的指针或引用实际指向某个派生类的对象,既不会破坏代码的正确性,也不需要改变已有代码。
  2. 不要通过公用继承重用基类中已有的代码,而是通过公用继承,让以及多态的使用了基类对象的已有代码重用当前的派生实现。
  3. 按照里氏替换原则:
    1. 公用继承所建模的必须总是"is-a"关系。

实施安全的覆盖

  1. 覆盖一个虚函数时,应该保持可替换性。

考虑将虚函数声明为非公用的,将公用函数声明为非虚的

  1. NVI(非虚拟接口)模式:

    1. 将公用函数设为非虚拟的,将虚拟函数设为私有的。
  2. 特殊:

    1. NVI对析构函数不适用,因为它们的执行顺序很特殊。

要避免提供隐式转换

  1. 隐式转换的问题:
    1. 它们会在最意料不到的地方抛出异常。
    2. 它们并不总是能与语言的其他元素有效的配合。
  2. 解决:
    1. 默认时,为单参数构造函数加上explicit
    2. 使用提供转换的命名函数代替转换操作符。

将数据成员设为私有的,无行为的聚集

不要公开内部数据

  1. 避免返回类所管理的内部数据的句柄,这样类的客户就不会不受控制地修改对象自己拥有的状态。

明智的使用Pimpl

  1. 将私有部分隐藏在一个不透明的指针后面。

优先编写非成员非友元函数

  1. 非成员非友元函数通过尽量减少依赖提高了封装性:
    1. 函数体不能依赖于类的非公用成员。
    2. 它们还能够分离巨类,释放可分离的功能,进一步减少耦合。

总是一起提高new和delete

  1. 例外:
    1. operator new的就地形式

如果提供类专门的new,应该提供所有标准形式

  1. 如果类定义了operator new的重载,则应该提供operator new的三种形式,普通(plain),就地(in-place),和不抛出(nothrow)的重载,不然类的用户就无法看到和使用它们。

构造、析构与复制

以同样的顺序定义和初始化成员变量

在构造函数中用初始化代替赋值

避免在构造函数和析构函数中调用虚函数

  1. 从构造函数或析构函数直接或间接调用未实现的纯虚函数,会导致未定义行为。

将基类析构函数设为公用且虚的,或者保护且非虚的

  1. 如果允许通过指向基类Base的指针执行删除操作,则Base的析构函数必须是公用且虚的。否则,就应该是保护且非虚的。

析构函数、释放和交换绝对不能失败

  1. 绝不允许析构函数、资源释放函数或者交换函数报告错误。
    换句话,绝对不允许将那些析构函数可能会抛出异常的类型用于C++标准库。

一致的进行复制和销毁

  1. 如果定义了拷贝构造函数、拷贝复制操作符或者析构函数中任何一个,那么可能也需要定义另一个或另外两个。

显式的启用或者禁止复制

  1. 在以下3种行为之间需要谨慎选择:
    1. 使用编译器生成的拷贝构造函数和拷贝复制操作符
    2. 编写自己的版本
    3. 如果不应允许拷贝的话,显式的禁用前两者

避免切片,在基类中考虑用克隆代替拷贝

  1. 对象切片是自动的、不可见的,而且可能会使多态设计戛然而止。
  2. 在基类中,如果客户需要进行深拷贝的话,应该考虑禁止拷贝构造函数和拷贝复制操作符,而改为提供虚的Clone成员函数。

使用赋值的标准形式

  1. 在实现operator时,应该使用标准形式--具有特定签名的非虚形式。

只要可行,就正确的提供不会失败的swap

本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0 最后编辑于:2022-01-06
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享