《C++编程规范 101条规则、准则与最佳实践》 人邮 -- 读书笔记

本文详细阐述了编程规范与最佳实践,包括代码风格、构建系统、版本控制、代码审查、设计原则、并发编程、错误处理等多个方面,旨在帮助开发者编写高质量、可维护的代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

组织和策略问题

第0条  不要拘泥于小节(了解哪此东本西不应该标准化)

编程规范不应施加个人喜好或者过时的做法。

第1条  在高警告级别干净利落地进行编译 

高度重视警告:使用编译器的最高警告级别。应该要求构建是干净利落的(没有警告)。理解所有的警告。通过修改代码而不是降低警告级别来排除警告。

例外:

有时候,编译器可能会发出烦人的甚至虚假的警告(即纯属噪声的警告),但是又没有提供消除的方法,这时忙于修改代码解决这个警告可能是劳而无功的或者事倍功半的。如果遇到这种罕见的情形,作为团队决定,应该避免对纯粹无益的警告再做无用功:单独禁用这个警告,但是要尽可能在局部禁用,并且编写一个清晰的注释,说明为什么必须禁用。

第2条  使用自动构建系统

一次按键就解决问题:使用完全自动化(“单操作”)的构建系统,无需用户干预即可构建整个项目。

第3条  使用版本控制系统

常言道,好记性不如烂笔头:请使用版本控制系统(Version Control System,VCS)。永远不要让文件长时间登出。在新的单元测试通过之后,应该频繁的登入。确保登入的代码不会影响构建成功。

例外:

只有一个程序员且从头至尾只需一周的项目,可能不需要版本控制系统。

第4条  做代码审查

审查代码:更多的关注有助于提高质量。亮出自己的代码,阅读别人的代码。互相学习,彼此都会受益。

设计分格

第5条  一个实体应该只有一个紧凑的职责

一次只解决一个问题:只给一个实体(变量、类、函数、名字空间、模块和库)赋予一个定义良好的职责。随着实体变大,其职责范围自然也会扩大,但是职责不应该发散。

第6条  正确、简单和清晰第一

软件简单为美(Keep It Simple Software,KISS):质量优于速度,简单优于复杂,清晰优于机巧,安全优于不安全。

第7条  编程中应知道何时和如何考虑可伸缩性

小心数据的爆炸性增长:不要进行不成熟的优化,但是要密切关注渐近复杂性。处理用户数据的算法应该能够预测所处理的数据量耗费的时间,最好不差于线性关系。如果能证明优化必要而且非常重要,尤其在数据量逐渐增长的情况下,那么应该集中精力改善算法的O(N)复杂性,而不是进行小型的优化,比如节省一个多余的加法运算。

第8条  不要进行不成熟的优化

不成熟优化的诱惑非常大,而它的无效性也同样严重。优化的第一原则就是:不要优化。优化的第二原则(仅适用于专家)是:还是不要优化。再三测试,而后优化。

例外:

在编写程序库的时候,预测哪些操作最后会用于性能敏感的代码中更加困难。但即使是程序库的编写者,在实施容易令人糊涂的优化之前,也会对很大范围内的客户代码进行性能测试。

第9条  不要进行不成熟的劣化

放松自己,轻松编程:在所有其他事情特别是代码复杂性和可读性都相同的情况下,一些高效的设计模式和编程用法会从你的指尖自然流出,而且不会比悲观的替代方案更难写。这并不是不成熟的优化,而是避免不必要的劣化。

第10条 尽量减少全局和共享数据

共享会导制冲突:避免共享数据,尤其是全局数据。共享数据会增加耦合度,从而降低可维护性,通常还会降低性能。

例外:

程序范围的设施cin、cout、cerr比较特殊,其实现方式很特别。工厂类必须维护一个注册表,记录创建给定类型时要调用哪个函数,而且通常应该有一个用于整个程序的注册表(但最好是属于工厂类,而不是属于共享全局对象)

跨线程共享对象的代码应该总是将这些共享对象的所有访问序列化。

第11条  隐藏信息

不要泄密:不要公开提供抽象的实体的内部信息。

例外:

测试代码经常需要对被测试类或者模块进行白箱访问。

值的聚合(c语言的struct)只是简单地将数据绑在一起,并没有提供任何抽象,所以它不需要隐藏数据,数据本身就是接口。

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

如果应用程序使用了多个线程或者进程,应该知道如何尽量减少共享对象,以及如何安全地共享必须共享的对象。

第13条  确保资源为对象所拥有。使用显示的RAII和智能指针

利器在手,不要再徒手为之:C++的“资源获取即初始化”(Resource Acquisition Is Initialization,RAII)惯用法是正确处理资源的利器。RAII使编译器能提供强大且自动的保证,这在其他语言中可是需要脆弱的手工编写的惯用法才能实现的。分配原始资源的时候,应该立即将其传递给属主对象。永远不要在一条语句中分配一个以上的资源。

例外:

智能指针有可能会被过度使用。如果被指向的对象只对有限的代码(比如纯粹在类的内部,诸如一个tree类的内部节点导航指针)可见,那么原始指针就够用了。

编程风格

第14条  宁要编译时和连接时错误,也不要运行时错误

能够在编译时做的事情,就不要推迟到运行时:编写代码时,应该在编译期间使用编译器检查不变式,而不应该在运行时进行检查。运行进检查取决于控制流和数据的具体情况,这意味着很难知道检查是否彻底。相比而言,编译时检查与控制流和数据无关,一般情况下能够获得更高的可信度。

例外:

有些情况下,无法在编译时检查,必须进行运行时检查。对于这些情况,应该使用断言来检查内部编程错误,对于其他运行时错误比如与数据相关的错误,则要遵循“错误处理与异常”部分的其他建议。

第15条  积极使用const

const是我们的朋友:不变的值更易于理解、跟踪和分析,所以应该尽可能地使用常量代替变量,定义值的时候,应该把const作为默认的选项:常量很安全,在编译时会对其进行检查,而且它与C++的类型系统已浑然一体。不要强制转换const的类型,除非要调用常量不正确的函数。

第16条  避免使用宏

宏是C和C++语言的抽象设施中最生硬的的工具,它是披着函数外衣的饥饿的狼,很难驯服,它会我行我素地游走于各处。要避免使用宏。

例外:

宏仍然是几个重要任务的唯一解决方案,比如#include保护符,条件编译中的#ifdef和#ifndef以及assert的实现。

在条件编译(如与系统有关的部分)中,要避免在代码中到处杂乱地插入#ifdef。相反,该对代码进行组织,利用宏来驱动一个公共接口的多个实现,然后始终使用这个接口。

如果不想到处复制和粘贴代码段,那么可以使用宏(但要非常小心)。

第17 条  避免使用魔数

程序设计并非魔术,所以不要故弄玄虚:要避免在代码中使用诸如42和3.12159这样文字常量。它们本身没有提供任何说明,并且因为增加了难于检测的重复而使维护更加复杂。可以用符号名称和表达式替换它们,比如width*aspectRatio。

第18条  尽可能局部的声明变量

避免作用域膨胀,对于需求如此,对于变量也是如此。变量将引入状态,而我们应该尽可能少地处理状态,变量的生存期也是越短越好。这是第10条的一个特例。

例外:

有时候将变量提出循环是有好处的。

因为常量并不添加状态,所以本条对常量并不适用。

第19 条  总是初始化变量

一切从白纸开始:未初始化的变量是C和C++程序中错误的常见来源。养成在使用内存之前先清除的习惯,可以避免这种错误,在定义变量的时候就将其初始化。

例外:

硬件或者其他进程直接写入的输入缓冲区数据和valotile型数据不需要程序对其进行初始化。

第20条  避免函数过长,避免避免嵌套过深

短胜过长,平优于深:过长的函数和嵌套过深的代码块的出现,经常是因为没能赋予一个函数以一个紧凑的职责所致,这两种情况通常都能通过更好的重构予以解决。

例外:

如果一个函数的功能无法合理的重构为多个独立的子任务(因为任何重构尝试都需要传递许多局部变量和上下文,使重构结果的可读性非但不好,反而更差),那么它的较长和嵌套较多就是合理。但是如果有几个这样的函数都具有相似的参数,那么它们就可能成为一个新类的成员。

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

保持(初始化)顺序:不同编译单元中的名字空间级对象决不应该在初始化上互相依赖,因为其初始化顺序是未定义的。这样做会惹出很多麻烦,轻则在项目中稍做修改就会引发奇怪的崩溃,重则出现严重的不可移植问题--即使是同一编译器的新版本也不行。

第22 条 尽量减少定义依赖。避免循环依赖。

不要过分依赖:如果用前向声明能够实现,那么就不要包含定义。

不要互相依赖:循环依赖是指两个模块直接或者间接地互直依赖。所谓模块就是一个紧凑的发布单元。互相依赖的多个模块不是真正的独立模块,而是紧紧胶着在一起的一个更大的模块,一个更大的发布单元。因此,循环依赖有碍于模块性,是大型项目的祸根。请避免循环依赖。

例外:

类之间的依赖循环并不一定都是坏事--只要类被认为属于同一模块,一起测试,一起发布。诸如Command和Visitor等设计模式的原始实现就会产生天生相互依赖的接口。这种相互依赖可以被打破,但是需要进行明确的设计才行。

第23条  头文件应该自给自足

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

第24条  总是编写内部#include保护符,决不要编写外部#include保护符

为头文件添加保护:在所有头文件中使用带有唯一名称的包含保护符,防止无意的多次包含。

例外:

在一些非常罕见的情况下,可能需要多次包含一个头文件。

函数与操作符

第25条  正确地选择通过值、(智能)指针或者引用传递参数

正确选择参数:分清输入参数、输出参数和输入输出参数,分清值参数和引用参数。正确的传递参数。

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

程序员讨厌意外的情况:只在有充分理由时才重载操作符,而且应该保持其自然语义;如果做到这一点很困难,那么你可能已经误用了操作符重载。

例外:

有些非常专门的程序库为操作符定义了特定于领域的规范,与C++语义迥异。应该为这种不常见的操作符重载寻找替代方案。如果三思之后,还是不得不选择使用操作符,那么一定要为你的规范定义一个一致的框架,谨防止与任何操作符发生冲突。

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

如果要定义a+b,也应该定义a+=b:在定义二元算术运算符时,也应该提供操作符的赋值形式,并且应该尽量减少重复,提高效率。

 

第28条 优先使用++和--的标准形式。优先调用 前缀形式。

如果定义++c,也要定义c++:递增和递减操作符很麻烦,因为它们都有前缀和后缀形式,而两种形式语义又略有不同。定义operator++和operator--时,应该模仿它们对应的内置操作符,如果不需要原值,应该优先使用前缀版本。

第29条  考虑重载以避免隐含类型转换

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

第30条  避免重载&&,||或,(逗号)

明智就是知道何时应该适可而止:内置的&&、||和逗号得到了编译器的特殊照顾,如果重载它们,它们就会变成普通函数,具有完全不同的语义,这肯定会引用微妙的错误和缺陷。不要轻率的重载这些操作符。

例外:

表达式模板是一个例外,设计它的目的就是用来捕获所有操作符。

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

保持求值顺序:函数参数的求值顺序是不确定的,因此不要依赖具体的顺序。

类的设计与继承

第32条 弄清所要编写的是哪种类

了解自我:有很多种不同的类。弄清楚要编写的是哪一种。

第33条  用小类代替巨类

分而治之:小类更易于编写,更易于保证正确、测试和使用。小类更有可能适用于各种不同情况。应该用这种小类体现简单概念,不要用大杂烩式的类,它们要实现的概念既多又复杂。

第34条  用组合代替继承

避免继承带来的重负:继承是C++中第二紧密的耦合关系,公次于友元关系。紧密的耦合是一种不良现象,应该尽量避免。因此,应该用组合代替继承,除非知道后者确实对设计有好处。

例外:

使用公用继承模仿可替换性。

即使不需要为所有调用者都提供替换性关系,但是如果有以任何一种需要,那么还是需要用到非公用的继承,下面是按最常用(头两条)到极为罕见(其余)排序的需要。

(1)如果需要改写虚拟函数

(2)如果需要访问保护成员

(3)如果需要在基类之前构造已使用过的对象,或者在基类之后销毁此对象。

(4)如果需要操心虚拟基类

(5)如果能够确定空基类优化能带来好处,包括这种情况下优化的确很重要,以及这种情况下目标编译器确实能实施这种优化。

(6)如果需要控制多态。相当于说,如果需要可替换性关系,但是关系应该只对某些代码可见(通过友元)

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

有些人并不想生孩子:本意是要独立使用的类所遵循的设计蓝图与基类不同。将独立类用作基类是一种严重的设计错误,应该避免。要添加行为,应该添加非成员函数而不是成员函数。要添加状态,应该使用组合而不是继承。要避免从具体的基类中继承。

第36条  优先提供抽象接口

偏爱抽象艺术吧:抽象接口有助于我们集中精力保证抽象的正确性,不至于受到实现或者状态管理细节的干扰。优先采用实现了(建模抽象概念的)抽象接口的设计层次结构。

例外:

空基类优化是一个纯粹为了优化而使用继承(最好是非公用的)的实例。

基于策略的设计看上去似乎是让高层组件依赖实现细节,然而,那只是使用了静态多态而已。抽象接口仍然存在,只不过是隐式的,并没有通过纯虚拟函数显式地声明而已。

第37条  公用继承即可替换性。继承,不是为了重用,而是为了被重用

知其然:公用继承能鏯使基类的指针或者引用实际指向某个派生的对象,既不会破坏代码的正确性,也不需要改变已有代码。

还要知其所以然:不要通过公用继承重用(基类中已有的)代码,公用继承是为被(已经多态地使用了基对象的已有代码)重用的。

例外 :

策略类和混入类(为虚拟基类提供部分实现的类)通过公用继承添加行为,但这并不是误用公用继承来建模“用...来实现”关系。

第38条  实施安全的覆盖

负责任进行覆盖:覆盖一个虚拟函数时,应该保持可替换性;说得更具体一些,就是要保持基类中函数的前后置条件。不要改变虚拟函数的默认参数。应该显式的将覆盖函数重新声明为virtual。谨防在虚拟类中隐藏重载函数。

第39条  考虑将虚拟函数声明为非公用的,将公用函数声明为非虚拟的

在基类中进行修改代价高昂(尤其是库中和框架中的基类):请将公用函数设为非虚拟的,应该将虚拟函数设为私有的,或者如果派生类需要调用基类版本,则设为保护的。(请注意,此建议不适用于析构函数)

我个人的疑问:虚拟函数设置为私有,还有什么用呢?

第40条  要避免提供隐式转换

并非所有的变化都是进步:隐式转换所带的影响经常是弊大于利。在为自定义类型提供隐式转换之前,请三思而行,应该依赖的是显示转换(explicit构造函数和命名转换函数)

第41条  将数据成员设为私有的,无行为的聚集(C语言形式的struct)除外

它们不关调用者的事:将数据成员设为私有的。简单的C语言形式的struct类型只是将一组值聚集在了一起,并不封装或者提供行为,只有在这种struct类型中才可以将所有数据成员都设成公用的。要避免将公用数据和非公用数据混合在一起,因为这几乎总是设计混乱的标志。

例外:

get/set函数很有用,但是主要由get/set组成的类可能是一种设计不良的表现。遇到这种情况,应该决定一下,它是要提供抽象还是要成为一个struct。

值的聚集中是将一组数据简单地放在一起,但是并没有实际添加什么有效的行为或者要建模什么抽象并实施不变式,它们并不是想提供抽象。它们的数据成员应该都是公用的,因为数据成员本身就是接口。

第42条  不要公开数据

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

例外:

有时由于兼容的原因,比如与遗留的代码或者其他系统进行接口,类必须提供访问其内部句柄的方式。

第43条  明智地使用pimpl

抑制语言的分离欲望:C++将私有成员指定为不可访问的,但并没有指定为不可见的。虽然 这样自有其好处,但是可以考虑通过Pimpl惯用法使用私有成员真正不可见,从页实现编译器防火墙,并提高住处隐藏度。

第44条  优先编写非成员非友元函数

要避免交成员费:尽可能将函数指定为非成员非友元函数。

第45条  总是一起提供new和delete

它们是一揽子交易:每个类专门的重载void* operator new(parms)都必须与对应的重载void operator delete(void*,parms)相随相伴,其中parms是额外参数类型的一个列表。数组形式的new[]和delete[]也同样如此。

第46条  如果提供类专门的new,应该提供所有标准形式(普通、就地和不抛出)

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

void  operator new(std::size_t);//plain

void  operator new(std::size_t,std::nothrow_t) throw();//nothrow

void operator new(std::size_t,void*)//in-place

构造、析构与复制

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

与编译一致:成员变量初始化的顺序与类定义中声明的顺序始终保持一致,不用考虑构造函数初始化列表中编写的顺序。要确保构造函数不会导致混淆地指定不同的顺序。

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

在构造函数中,使用初始化代替赋值来设置成员函数,能够防止发生不必要的运行时操作,而输入代码的工作量则保持不变。

第49条  避免在构造函数和析构函数中调用虚拟函数

在构造函数和析构函数中,它闪并不虚拟。更糟糕的是,从构造函数或析构函数中直接或间接用未实现的纯虚拟函数,会导致未定义的行为。如果设计方案希望从基类构造函数或者析构函数或者析构函数虚拟分配到派生类,那么需要采用其他技术。

第50条  将基类析构函数设为公用且虚拟,或者保护且非虚拟

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

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

决不允许析构函数、资源释放函数或者交换函数报告错误。说得更具体一些,就是绝对不允许将那些析构函数可能会抛出异常的类型用于C++标准库。

第52条  一致进行复制和销毁

如果定义了复制构造函数、复制赋值操作符或者析构函数中的任何一个,那么可能需要定义另一个或者另两个。

例外:

如果声明这个三个特殊函数之一,只是为了将它们设为私有的或者虚拟的,而没有什么特殊语义的话,那么就意味着不需要其余两个函数。

地某些比较罕见的情况下,拥有一些奇怪类型成员的类是本条的例外,因为它们它的复制语义很奇怪。在一个包含引用或者auto_ptr的类中,可能还需要编写复制构造函数和赋值操作符,但是默认析构函数已经能够正确工作了。

第53条  显式地启用或者禁止复制

在下面三种行为之间谨慎选择--使用编译器生成的复制构造函数和赋值操作符;编写自己的版本;如果不应允许复制的话,显式地禁用前两者。

第54条  避免切片。在基类中考虑用克隆代替复制

对象切片是自动的、不可见的,而且可能会使漂亮的多态设计戛然而止。在基类中,发果客户需要进行多态(完整的、深度的)复制的话,那么请考虑禁止复制构造函数和复制操作符,而改为提供虚拟的Clone成员函数。

第55条  使用赋值的标准形式

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

第56条  只要可行,就提供不会失败的swap(而且要正确地提供)

应该考虑提供一个swap函数,高效且绝对无误地交换两个对象。这样的函数便于实现许多惯用法,从流畅地将对象四处游动以轻易实现赋值,到提供一个有保证的、能够提供强大防错调用代码的提交函数。

例外:

对于有值语义的类来说,交换是很有用的。但对于基类来说往往就没有那么有用了,因为我们总是通过指针使用基类。

名字和空间

第57条  将类型及非成员函数接口置于同一名字空间中

如果要将非成员函数(特别是操作符和辅助函数)设计成类X的接口的一部分,那么主必须在与X相同的名字空间中定义它们,以便正确使用。

第58条  应该将类型和函数分别置于不同的名字空间中,除非有意想让它们一起工作

通过将类型(以及与其直接相关的非成员函数)置于自己单独的名字空间中,可以使类型与无意的ADL(参数依赖查找,也称为Koenig查找)隔离开来,促进有意的ADL。要避免将类型和模板化函数或者操作符放在相同的名字空间中。

第59条  不要在头文件或者#include之前编写名字空间using

绝对不要编写using声明或者在#include之前写using指令。

第60条  要避免在不同的模块中分配和释放内存

在一个模块中分配内存,而在另一个模块中释放它,会在这两个模块之间产生微妙的远距离依赖,使程序变得脆弱。必须用相同版本的编译器、同样的标志和相同的标准库实现对它们进行编译,实践中,在释放内存时,用来分配内存的模块最好仍在内存中。

第61条  不要在头文件中定义具有链接的实体

重复会导致膨胀:具有链接的实体,包括名字空间级的变量或函数,都需要分配内存。在头文件中定义这样的实体将导制连接时错误或者内存的浪费。请将所有具有链接的放入实现文件。

例外:

内联函数。它们具有外部链接,但是连接器肯定不会拒绝多个副本。除此之外,它们和常规函数的行为完全一样。实际上,内联函数的地址在整个程序中肯定是唯一的。

函数模板。它们与内联函数相似,其实例化行为与常规函数一样,只不过可以接受重复的副本(而且最好完全一样)。当然,好的编译框架会去除没有必要的副本。

类模板的静态数据成员。这对连接器而言可能非常麻烦,但这不是你的问题--你要做的就是在头文件中定义它们,让编译器和连接器来处理其余的事情。

此外,一种称为“Schwarz计数器”或者“灵巧计数器(nifty counter)”的全局数据初始化技术要求在头文件中放入静态的(或未命名空间中的)数据。

第62条  不要允许异常跨越模块边界传播

不要向邻家的花园抛石头:C++异常处理没有普遍通用的二进制标准。不要在两段代码之间传播异常,除非能够控制用来构建两段代码的编译器和编译选项;否则模块可能无法支付可兼容地实现异常传播。这通常可以以一言以蔽之:不要允许异常跨越模块或子系统边界传播。

第63条 在模块的接口中使用具有良好可移植性在类型

生在(模块的)边缘,必须格外小心:不要让类型出现在模块的外部接口中,除非能够确保所有的客户代码都能正确的理解该类型。应该使用客户代码能够理解的最高层抽象。

模板与泛型

第64条  理智地结合静态多态性和动态多态性

1+1远远大二2:静态多态性和动态多态性是相辅相成的。理解它们的优缺点,善用它们的长处,结合两者以获得两方面的优势。

第65条  有意进行显式自定义

有意胜过无意,显式强似隐式:在编写模板时,应该有意地、正确地提供自定义点,并清晰地记入文档。在使用模板时,应该了解模板想要你如何进行自定义将其用二你的类型,并且正确地自定义。

第66条  不要特化函数模板

只有能够正确实施的时候,特化才能收到好作用:在扩展其他人的函数模板(包括std::swap)时,要避免尝试编写特化代码;相反,要编写函数模板的重载,将其放在重载所用的类型的名字空间中。编写自己的函数模板进,要避免鼓励其他人直接特化函数模板本身。

第67条  不要无意地编写不通用的代码

依赖抽象而非细节:使用最通用、最抽象的方法来实现一个功能。

例外:

在某些情况下,使用索引而不是迭代能够使编译器更好地进行优化。但是这样做之前,请确认是否真地需要这种优化,以及编译器是否真地能够做到。

错误处理与异常

第68条  广泛地使用断言记录内部假设和不变式

使用断言吧!广泛地使用assert或者等价记录模块内部(也就是说,调用代码和被调用代码由同一个人或者小组维护)的各种假设,这些假设是必须成立的,否则就说明存在编程错误(例如,函数的调用代码检查到函数的后置条件不成立)。当然,要确保断言不会产生任何副作用。

条69条  建立合理的错误处理策略,并严格遵守

应该在设计早期开发实际、一致、合理的错误处理策略,并予以严格遵守。许许多多的项目对这一点的考虑(或者错误估计)都相当草率,应该对此有意识地规定,并认真应用。策略必须包含以内容。

  • 鉴别。哪些情况属于错误
  • 严重程度。每个错误的严重性或紧急性。
  • 检查。哪些代码负责检查错误
  • 传递。用什么机制在模块中报告和传递错误通知。
  • 报告。怎样将错误记入日志,或通知用户。

只在模块边界处改变错误处理机制。

第70条  区别错误与非错误

违反错误与非错误:函数是一个工作单元。因此,失败应该视为错误,可根据其对函数的影响而定。在函数f中,当且公当失几违反了f的一个前置条件,或者阻碍了满足其调用代码的任何前置条件、实现自己的任何后置条件或者重新建立f有责任维持的不变式时,失败才是一个错误。

这里我们特别排除了内部的程序错误,这种错误一般可以使用断言来解决。

第71条  设计和编写错误安全代码

承诺,但是不惩罚:在所有函数中,都应该提供最强的安全保证,而且不应惩罚不需要这种保证的调用代码。至少要提供基本保证。

确保出现错误时程序会处于有效状态。这是所谓的基本保证。要小心会破坏不变式的错误(包括但是不限于泄漏),它们肯定都是bug。

应该进一步保证最终状态要么是最初状态(如果有错误,则回滚),要反是所希望的目标状态(如果没有错误,则提交操作)。这就是所谓的强保证

应该进一步保证操作永远不会失败。虽然这对于大多数函数说是不可能的,但是对于析构函数和释放函数这样的函数来说则必须的。这就是所谓的不会失几保证。

第72条  优先使用异常报告错误

出现问题是时,就使用异常:应该使用异常而不是错误码来报告错误。但不能使用异常时,对于错误以及不是错误的情况下,可以使用状态码(比如返回码,errno)来报告异常。当不可能从错误中恢复或者不需要恢复时,可以使用其他方法,比如正常终止或者非正常终止。

例外:

在极罕见的情况下,如果能够肯定以下两点为真,就可以考虑使用错误码。

  • 异常的优点不适用。例如,已知直接的调用代码几乎总是必须马上处理错误,因此决不会或者几乎不会发生异常传播。这非常罕见,因为通常被调用代码了解到的有关它的所有调用代码 的这些特性信息并不多
  • 抛出异常与使用错误码的实测性能差异比较明显。也就是说,性能差异是实际测出的,这它很可能是因为你在循环的内部,而且需要经常抛出异常。后一种情况比较罕见,因为这通常意味着该情况根本就不是真的错误,不过还是假设它是好了。

第73条  通过值抛出,通过引用捕获

学会正确捕获:通过值(而非指针)抛出异常,通过引用(通常是const的引用)捕获异常。这是与异常语义配合最佳的组合。当重新抛出相同的异常时,应该优先使用throw;,避免使用throw e;。

第74条  正确地报告、处理和转换错误

什么时候说什么话:在检查出并确认是错误时报告错误。在能够正确处理错误的最近一层处理或者转换每个错误。

例外:

接受并且再次发送同样的错误以添加测试代码有时候是有用的,虽然错误实际上并没有行到处理。

第75条  避免使用异常规范

不要在函数中编写异常规范,除非不得以而为之(因为其他无法修改的代码已经使用了异常规范)

STL:容器

第76条  默认时使用vector。否则,选择其他合适的容器。

使用“正确的容器”才是正道:如果有充分的理由使用某个特定容器类型,那就用好了,因为我们心中有数:自己做出了正确的选择。

使用vector同样如此:如果没有充分理由,那就编写vector,继续前进,无需停顿,我们同样心中有数:自己做出了正确的选择。

第77条  用vector和string代替数组

何必用贵重的明代花瓶玩杂耍呢?不要使用C语言风格的数组、指针运算和内存管理原语操作实现数组抽象。使用vector或者string不仅更轻松,而且还有助于编写更安全、伸缩性更好的软件。

第78条  使用vector(和string::c_str)与非C++API交换数据

vector不会在转换中迷失:vector和string::c_str是与非C++API通信的通道。但是不要将迭代器当作指针。要获取vector<T>::iterator iter所引用的元素地址,应该使用&*iter。

第79条  在容器中只存储值和智能指针

在容器中存储值对象:容器假设它们所存放在是类似值的类型,包括值类型、智能指针和迭代器。

第80条  用push_back代替其他扩展序列的方式

尽可能使用puhs_back:如果不需要操心插入位置,就应该使用push_back在序列中添加元素。其他方法可能极慢而且不简明。

第81条  多用范围操作,少用单元素操作

在序列容器中添加元素时,应该多用范围操作,而不要连续调用该操作的单元素形式。调用范围操作通常更易于编写,也更易于阅读,而且比显式循环的效率更高。

第82条  使用公认的惯用法真正地压缩容量,真正地删除无素

要真正的压缩容器的多余容量,应该使用“swap魔术”惯用法。要真正地删除容器中的无素,应该使用erase-remove惯用法。

STL:算法

第83条  使用带检查的STL实现

即使只在其中一个编译平台上可用,即使只能在发行前的测试中使用,也仍然要使用带检查的STL实现。

第84条  用算法调用代替手工编写的的循环

对于非常简单的循环而言,手工编写的循环有可能是最简单也是最有效的解决方案。但是编写算法调用代替手工编写的循环,可以使表达力更强、维护性更好、更不易出错,而且同样高效。

调用算法时,应该考虑编写自定义的函数对象以封装所需的逻辑。不要将参数绑定器和简单的函数对象凑在一起,通常这会降低清晰性。还可以考虑尝试[Boost]和Lambda库,这个库自动化了函数对象的编写过程。

第85条  使用正确的STL查找算法

本条款适用于在一个范围内查找某个特定值,或者查找某个值的位置(如果它处在范围内的话)。查找无序范围,应使用find/find_if或者count/cout_if。查找有序范围,应该使用lower_bound、upper_bound、equal_range或者binary_search。

第86条  使用正确的STL排序算法

选择排序方式应“恰到好处”:理解每个排序算法的作用,选择能够实现所需而开销最低的算法。

第87条  使谓词成为纯函数

保持谓词纯洁性:谓词就是返回是或否的函数对象。从数学的意义上来说,如果函数的结果只取决于其参数,则该函数就是一个纯函数

不要让谓词保存或访问对其operator()结果有影响的状态,包括成员状态和全局状态。应该使用operator()成为谓词的const成员函数。

第88条  算法和比较器的参数应多用函数对象少用函数

对象的适配性比函数好:应该向算法传递函数对象,而非函数。关联容器的比较器必须是函数对象。函数对象的适配性好,而且与直觉相反,它们产生的代码一般比函数要快。

第89条  正确编写函数对象

成本要低,而且要可适配:将函数对象设计为复制成本很低的值类型。尽可能让它们从unary_function或binary_function继承,从而能够适配。

类型安全

第90条  避免使用类型分支,多使用多态

避免通过对象类型分支来定制行为。使用模板和虚函数,让类型自己(而不是调用它们的代码)来决定行为。

第91条  依赖类型,而非其表达式

在要对对象在内存中的准确表示方式做任何假设。相反,应该让类型决定如何在内存中读写其对象。

第92条  避免使用reinterpret_cast

不要尝试使用reinterpret_cast强制编译器将某个类型对象的内存表示重新解释成另一种类型的对象。这违反了维护类型安全性的原则,尤其可怕的是,reinterpret_cast甚至不能保证是否能够达到这一目的,也无法保证其他功能。

例外:

有些与特定系统相关的底层编程,要求使用reinterpret_cast在端口串行输入和输出数据,或者转换某些地址的整数。最大限度地减少使用不安全的强制转换,只在将其抽象出的一些隐藏较好的函数中使用,这样代码就能为移植做好准备且无需太多改动。

第93条  避免对指针使用static_cast

不要对动态对象的指针使用static_cast:安全的替代方法有很多,包括使用dynamic_cast,重构,及至重新设计

第94条  避免强制转换const

强制转换const有时会导致未定义的行为,即使合法,也是不良编程风格的主要表现。

第95条  不要使用C风格的强制转换

C语言风格的强制转换根据上下文具有不同的语义,而的有这些都隐藏在相同的语法背后。用C++风格的强制转换代替C风格的强制转换有助于防范意想不到的错误。

第96条  不要对非POD(Plain Old Data)进行memcpy操作或者memcmp操作

不要用memcpy或memcmp来复制或比较任何对象,除非有什么对象的布局就是原始内存

第97条  不要使用联合重新解释表示方式

通过在union中写入一个成员而读取另一个的滥用 方式可以获得“无需强制转换的强制转换”。这比起reinterpret_cast更阴险,也更难预测。

例外:

如果两个POD struct是一个union的成员,而且均以相同的字段类型开始,那么对这种匹配的字段来说,写入其中一个而读取另一个是合法的。

第98条  不要使用可变长参数

省略会导致崩溃:省略号是来自C语言的危险遗产。要避免使用可变长参数,应改用高级的C++结构和库。

第99条  不要使用失效对象。不要使用不安全函数

实效对象和老的但是不安全的函数会对程序的健康产生极大的破坏。

第100条  不要多态地处理数组

数组的可调整性很差:多态地处理数组是绝对的类型错误,而且编译器有可能不会做出任何提示。

 

最后欢迎大家访问我的个人网站: 1024s

组织和策略问题 1 第0 不要拘泥于小节(又名:了解哪些东西不应该标准化) 2 第1 在高警告级别干净利落地进行编译 4 第2 使用自动构建系统 7 第3 使用版本控制系统 8 第4 做代码审查 9设计风格 11 第5 一个实体应该只有一个紧凑的职责 12 第6 正确、简单和清晰第一 13 第7 编程中应知道何时和如何考虑可伸缩性 14 第8 不要进行不成熟的优化 16 第9 不要进行不成熟的劣化 18 第10 尽量减少全局和共享数据 19 第11 隐藏信息 20 第12 懂得何时和如何进行并发性编程 21 第13 确保资源为对象所拥有。使用显式的RAII和智能指针 24 编程风格 27 第14 宁要编译时和连接时错误,也不要运行时错误 28 第15 积极使用const 30 第16 避免使用宏 32 第17 避免使用“魔数” 34 第18 尽可能局部地声明变量 35 第19 总是初始化变量 36 第20 避免函数过长,避免嵌套过深 38 第21 避免跨编译单元的初始化依赖 39 第22 尽量减少定义性依赖。避免循环依赖 40 第23 头文件应该自给自足 42 第24 总是编写内部#include保护符,决不要编写外部#include保护符 43 函数操作符 45 第25 正确地选择通过值、(智能)指针或者引用传递参数 46 第26 保持重载操作符的自然语义 47 第27 优先使用算术操作符和赋值操作符的标准形式 48 第28 优先使用++和--的标准形式。优先调用前缀形式 50 第29 考虑重载以避免隐含类型转换 51 第30 避免重载&&、||或 ,(逗号) 52 第31 不要编写依赖于函数参数求值顺序的代码 54 类的设计继承 55 第32 弄清所要编写的是哪种类 56 第33 用小类代替巨类 57 第34 用组合代替继承 58 第35 避免从并非要设计成基类的类中继承 60 第36 优先提供抽象接口 62 第37 公用继承即可替换性。继承,不是为了重用,而是为了被重用 64 第38 实施安全的覆盖 66 第39 考虑将虚拟函数声明为非公用的,将公用函数声明为非虚拟的 68
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值