C++编程规范-101条规则-准则与最佳实践
【书名】:C++编程规范-101条规则-准则与最佳实践
【作者】:Herb Sutter
【译者】:刘基诚
尽早进入正轨,以同样的方式实施同样的过程,不断积累惯用法,将其标准化,如此,你与莎士比亚之间的唯一区别将只是掌握惯用法的多少——而非词汇的多少。——Alan Perlis(首届图灵奖获得者)
编程规范与人的关系
- 改善代码质量;
- 提高开发速度;
- 增进团队精神;
- 在正确的方向上取得一致;
组织和策略问题
第0条:不要拘泥于小节(了解哪些东西不应该标准化)
-
类、函数、枚举名称形如:LikeThis;
-
变量形如:likeThis;
-
私有变量: likeThis_;
-
宏 LIKE_THIS;
第1条:在高警告级别干净利落地进行编译
降低警告数量,再编译;
第三方头文件、未使用的函数参数、定义了从未使用过的变量;变量使用前未初始化、遗漏了return语句、有符号数和无符号数不匹配。
第2条:使用自动构建系统
异常按键就解决问题,使用完全自动化的构建系统,无需用户敢于即可构建整个项目。我们不应该将时间和精力浪费在机器可以干的更快更好的事情上,自动的,可靠的,但操作的构建是非常必要的。
第3条:使用版本控制系统
不要破坏构建,版本控制系统中的代码必须总能构建成功,目前一般使用Git。
第4条:在代码审查上投入
更多的关注有助于提高质量,亮出自己的代码,阅读别人的代码,互相学习,彼此都会受益。
设计风格
第5条:一个实体应该只有一个紧凑的职责
一次只解决一个问题:只给一个实体(变量、类、函数、名称空间、模块和库)赋予一个定义良好的职责,随着实体变大,其职责范围自然也会扩大,但是职责不应该发散。
如果一个实体有几个不同的目的,则给使用带来的难度往往会激增,因为这种实体除了会增加理解难度、复杂性和各部分中的错误外,还会导致其他问题。这种实体不仅更大,而且更难以使用和维护。
realloc是一个不良设计。
basic_string也是另一个不良设计。
第6条:正确、简单和清晰第一
软件简单为美(Keep It Simple Software, KISS原则),正确优于速度,简单优于复杂,清晰优于机巧,安全优于不安全。
程序必须为阅读它的人而编写,只是顺便使用于机器执行。
编写程序应该以人为本,计算机第二。
计算机系统中最便宜,最快速,最可靠的组件都还不存在。
第7条:编程中应知道何时和如何考虑可伸缩性
应该集中精力改善算法的O(N)复杂性,而不是进行小型的优化,比如节省一个多余的加法运算。
- 使用灵活的、动态分配的数据,不要使用固定大小的数组
- 了解算法的实际复杂性
- 优先使用线性算法或者尽可能快的算法
- 尽可能避免劣于线性复杂性的算法
- 永远不要使用指数复杂性的算法,除非已经山穷水尽,确实别无选择
总而言之,要尽可能使用线性算法,尽可能合理地避免使用比线性算法差的多项式算法,竭尽全力避免使用指数算法。
第8条:不要进行不成熟的优化
优化的第一原则:不要优化,优化的第二原则,还是不要优化。
永远记住:让一个正确的程序更快速,比让一个快速的程序正确,要容易得太多,太多。
默认时,不要把注意力集中在如何使代码更快上,首先关注的应该是使代码尽可能地清晰和易读,清晰的代码更容易正确编写,更容易理解,更容易重构——当然也更容易优化,使事情复杂的行为,包含优化,总是以后再进行的——而且只在必要的时候进行。
第9条:不要进行不成熟的劣化
所谓不成熟的劣化,指的是编写如下这些没有必要的,可能比较低效的程序:
- 在可以用通过引用传递的时候,却定义了通过值传递的参数;
- 在使用前缀++操作符很合适的场合,却使用后缀版本;
- 在构造函数中使用赋值操作而不是初始化列表
构造清晰又有效的程序有两种重要的形式:使用抽象和库;
第10条:尽量减少全局和共享数据
共享会导致冲突:避免共享数据,尤其是全局数据,共享数据会增加耦合度,从而降低可维护性,通常还会降低性能。
全局名字空间中的对象名称还会污染全局名字空间。
如果必须使用全局的,名字空间作用域的或者静态的类对象,一定要仔细地对其进行初始化,这种对象在不同编译单位中这种对象的初始化顺序是未定义的,正确处理它们需要特殊的技术,初始化顺序规则是非常难于掌握的,应该尽量避免使用;如果不得不用的话,则应该充分了解,小心使用。
应该尽量降低类质检的耦合,尽量减少交互。
第11条:隐藏信息
不要泄密,不要公开提供抽象的实体的内部信息;
绝对不要将类的数据成员设为public,或者公开指向它们的指针 或句柄而使其公开
第12条:懂得何时和如何进行并发性编程
如果应用程序使用了多个线程或者进程,应该知道如何尽量减少共享对象,以及如何安全地共享必须共享的对象。
如果应用程序需要跨线程共享数据,请如下安全行事:
- 参考目标平台的文档,了解该平台的同步化原语:
- 最好将平台的原语用自己设计的抽象包装起来,也可以使用程序库pthreads为我们代劳;
- 确保正在使用的类型在多线程程序中使用时安全的。
在自己编写可用于多线程程序的类型时,也必须完成两项任务:首先,必须保证不同现成能够不加锁的使用该类型的不同对象。
其次,必须在文档中说明使用者在不同线程中使用该类型的同一个对象需要做什么,基本的设计问题是如何在类及客户之间分配执行的职责,
第13条:确保资源为对象所拥有,使用显式的RAII和智能指针
利器在手,不要再徒手为之;
绝对不要在一条语句中分配一个以上的资源
void Fun(shared_ptr<Widget> sp1, shared_ptr<Widget> sp2);
// ...
Fun(shared_ptr<Widget>(new Widget), shared_ptr<Widget>(new WIdget));
以上代码是不安全的,可以改为如下:
shared_ptr<Widget> sp1(new Widget), sp2(new Widget);
Fun(sp1, sp2);
编程风格
一个人的常量可能是另一个人的变量
第14条:宁要编译时和链接时错误,也不要运行时错误
能够在编译时做的事情,就不要推迟到运行时,编写代码时,应该在编译期间使用编译器检查不变式;
- 静态检查与数据和控制流无关;
- 静态表示的模型更加可靠
- 静态检查不会带来运行时开销;
有些情况下,可以用编译时检查代替运行时检查:
- 编译时布尔条件,那么可以使用静态断言取代运行时测试;
- 编译时多态:定义泛型函数或者类型时,考虑用编译时多态代替运行时多态,牵着产生的代码能够更好地进行静态检查;
- 枚举:在需要表示符号变量或受限整数值时考虑定义enum
- 向下强制,如果经常使用dynamic_cast,执行向下强制,则可能说明基类提供的功能太少了,
第15条:积极使用const
const是我们的朋友,不变的值更易于理解、跟踪和分析,所以应该尽可能地使用常量代替变量,定义值的时候,应该把const作为默认的选项;
定义值的时候,应该把const作为默认的选项,常量很安全,在编译时会对齐进行检查。
第16条:避免使用宏
它是披着函数外衣的饥饿的狼,很难驯服,它会我行我素地游走于各处,要避免使用 宏;
用const或者enum定义易于理解的常量,用inline避免函数调用的开销。
关于宏的第一条规则就是:不要使用它,除非不得不同,几乎每个宏都说明程序设计预研,程序或者程序员存在缺陷。
宏会忽略作用域,忽略类型系统,忽略所有其他的语言特性和规则。
即使在极少的情况下,有正当理由编写宏,也绝不要考虑编写一个以常见词或者缩略词为名字的宏,尽可能取消宏的定义(#undef)
第17条:避免使用魔数
程序设计并非魔术,所以不要故弄玄虚,要避免在代码中使用诸如42,和3.14159这样的文字常量。
第18条:尽可能局部地声明变量
避免作用域膨胀,对于需求如此,对于变量也是如此,变量将引入状态,而我们应该尽可能少地处理状态,变量的生存期也是越短越好。
第19条:总是初始化变量
一切从白纸开始;未初始化的变量是C和C++程序中错误的常见来源;
第20条:避免函数过长,避免嵌套过深
短胜于长,平优于深:过长的函数和嵌套过深的代码块的出现,经常是因为没能赋予一个函数以一个紧凑的职责所致;
请遵循这样的常识和常理:限制函数的长度和嵌套深度:
- 尽量紧凑,一个函数一个职责;
- 不要自我重复:优先使用命名函数,而不要让相似的代码片段反复出现;
- 优先使用&&:在可以使用&&条件判断的地方要避免使用连续嵌套的if
- 不要过分使用try
- 优先使用标准算法
- 不要根据类型标签进行粉质,优先使用多态函数
第21条:避免跨编译单元的初始化依赖
不同编译单元中的名字空间级对象绝不应该在初始化上互相依赖。
第22条:尽量减少定义性依赖,避免循环依赖
过渡相互依赖的一个症状,就是局部发生变化时需要进行增量构建,不得不重新编译项目中的很大一部分代码;
第23条:头文件应该自给自足
应该确保所编写的每个头文件都能够独自进行编译,为此需要包含其内容所依赖的所有头文件;
不要包含并不需要的头文件,它们智慧带来凌乱的依赖性。
第24条:总是编写内部#include保护符,绝不要写外部#include保护符
函数与操作符
如果一个过程有10个参数,那么你很可能还遗漏了一些;——Alan Perlis
第25条:正确选择通过值、(智能)指针或者引用传递参数
正确选择参数是通过值、引用还是指针传递,是一种能够最大程度提高安全性和效率的好习惯;
如果传递参数,对于只输入参数遵循以下原则:
- 始终用const限制所有指向只输入参数的指针和引用
- 优先通过值来取得原始类型(如char、float)和复制开销比较低的值对象
- 优先按const的引用取得其他用户定义类型的输入
- 如果函数需要其参数的副本,则可以考虑通过值传递代替通过引用传递,在概念上等同于通过const引用传递加上一次复制,能够优化掉临时变量;
对于输出参数或者输入/输出参数:
- 如果参数是可选的,或者函数需要保存这个指针的副本或者操控参数的所有权,那么应该优先通过(智能)指针传递;
- 如果参数是必需的,而且函数无需保存指向参数的指针,或者无需操控其所有权,那么应该优先通过引用传递,这表明参数是必须的,而且调用者必须提供有效对象
不要使用C语言风格的可变长参数
第26条:保持重载操作符的自然语义
只在有充分理由时才重载操作符,而且应该保持其自然语义,如果做不到,那么可能误用了;
第27条:优先使用算术操作符和赋值操作符的标准形式
第28条:优先使用++和–的标准形式,优先调用前缀形式
对于++和–而言,后缀形式返回的是原值,而前缀形式返回的是新值,应该用前缀形式实现后缀形式
第29条:考虑重载以避免隐含类型转换
如无必要勿增对象(奥卡姆剃刀原理),隐式类型转换提供了语法上的遍历,如果创建临时对象的工作并不必要而且适于优化,那么可以提供签名与常见参数类型精确匹配的重载函数,而且不会导致转换;
第30条:避免重载 &&、|| 或 ,(逗号)
内置的&&、||和逗号得到了编译器的特殊照顾,如果重载它们,就会变成普通函数,不要轻率的重载这些操作符;
第31条:不要编写依赖于函数参数求值顺序的代码
类的设计与继承
软件开发最重要的一个方面就是弄清楚自己要构建的是什么。——Bjarne Stroustrup
第32条:弄清所要编写的是哪种类
了解自我,有很多种不同的类,弄清楚要编写的是哪一种。
不同种类的类适用于不同用途,值类模仿的是内置类型,一个值类应该:
- 有一个公用析构函数,复制构造函数和带有值语义的赋值;
- 没有虚拟函数
- 用作具体类,而不是基类
- 总是在栈中实例化,或者作为另一个类直接包含的成员实例化;
基类是类层级结构的构成要素,一个基类应该:
- 有一个公用而且虚拟,或者保护而且非虚拟的析构函数,和一个非公用复制构造函数和赋值操作符;
- 通过虚拟函数建立接口
- 总是动态的在堆中实例化为具体派生类对象,并通过一个智能指针来使用。
策略类是可插拔行为的片段,一个策略类应该:
- 可能有也可能没有状态或者虚拟函数;
- 通常不独立实例化,只作为基类或者成员;
异常类提供了不寻常的值与引用语义的混合:它们通过值抛出,但应该通过引用捕获
- 有一个公用析构函数和不会失败的构造函数
- 有虚拟函数,经常实现克隆和访问
- 从std::exception虚拟派生更好
第33条:用小类代替巨类
分而治之,小类更易于编写,更易于保证正确,测试和使用,小类更有可能适用于各种不同情况,应该用这种小类体现简单概念,不要用大杂烩的类。
设计易于组合的更小的,尽量小的类,才是实践中更为成功的方法,这对任何规模的系统都适用。
一个巨大的Matrix类可能实现的功能很多,但是可以将具体的功能隔离开来,单独实现;
巨类会削弱封装性;
巨类通常是因为试图预测和提供完整的问题解决方案而出现的,这种类基本没有成功过,因为需求是在不断的变化;
巨类更难保证正确和错误安全,因为它们经常要应对多种职责;
第34条:用组合代替继承
避免继承带来的重负,继承是C++中第二紧密的耦合关系,仅次于友元关系,紧密的耦合是一种不良现象,应该尽量避免,因此,应该用组合代替继承,
软件工程的一条明智原则,就是尽量减少耦合,如果一种关系不只是一种表示方式,那么应该用可行的最弱关系;
与继承相比,组合有以下重要优点:
- 在不影响用户代码的情况下具有更大的灵活性:
- 更好的编译时隔离,更短的编译时间;
- 减少奇异现象,名字导致
- 更广的适用性
- 更健壮、更安全
- 复杂性和脆弱性降低
第35条:避免从并非要设计成基类的类中继承
将独立类用作基类是一种严重的设计错误,应该避免,要添加行为,应该添加非成员函数而不是成员函数,要添加状态,应该使用组合而不是继承,要避免从具体的基类中继承;
初学者有时会从值类比如string类中派生,以“添加更多功能”,但是,定义自由(非成员)函数比创建super_string要好得多;原因如下:
- 非承运函数在已经处理了string的已有代码中工作良好,如果改而提供super_string,就不得不在整个代码中强制实施相应修改,将类型和函数签名改为super_string;
- 以string为参数的接口函数现在需要在以下三者中选其一:a,避开super_string的附加功能,b,复制其参数为super_string, c, 将string引用强制转换为super_string引用;
- super_string的成员函数对string内部的访问权限并不比非成员函数大,因为string可能没有保护成员
- 如果super_string隐藏了string的一些函数,在处理string的代码中就产生普遍的混淆,以为这些string是从super_string中自动转换而来的。
因此,应该通过新的非成员函数来添加功能,为了避免名字查找问题,一定要将这些函数与要扩展的类型放在同一个名字空间中,有些人不喜欢非成员函数,因为它的调用语法是Fun(str)而不是str.Fun()
第36条:有限提供抽象接口
抽象接口有助于我们集中精力保证抽象的正确性,不至于受到实现或者状态管理细节的干扰。有限采用实现了抽象接口的设计层次结构;
抽象接口是完全由虚拟函数构成的抽象类,没有状态,通常也没有成员函数实现,注意,在抽象接口中避免使用状态能够简化整个层次结构的设计
应该遵守依赖倒置原理(DIP):
- 高层模块不应该依赖于低层模块,相反,两者都应该依赖抽象。
- 抽象不应该依赖于细节,相反,细节应该依赖抽象
DIP有三个设计优点:
- 更强的健壮性
- 更大的灵活性
- 更好的模块性
第37条:公用继承即可替换性。继承,不是为了重用,而是为了被重用
公用继承能够使基类的指针或者引用实际指向某个派生类的对象,既不会破坏代码的正确性,也不需要改变已有代码。
不要通过公用继承重用代码,公用继承是为了被重用;
第38条:实施安全的改写
负责任地进行改写,改写一个虚拟函数时,应该保持可替换性,说的具体一些,就是要保持基类中函数的前后条件,不要改变虚拟函数的默认参数,应该显式地改写函数重新声明为virtual, 谨防在虚拟类中隐藏重载函数;
第39条:考虑将虚拟函数声明为非公用的,将公用函数声明为非虚拟的。
如果派生类需要调用基类版本,则设为保护的。
数据成员设为私有的,除非确实需要公开他们,也可以将虚拟函数设为私有或者保护的。
在面向对象层次结构中进行修改尤其代价昂贵,所以应该实施完整的抽象,将公用函数设为非虚拟的,将虚拟函数设为私有或者保护的,这就是所谓的非虚拟接口(NVI)模式;NVI对析构函数不适用,因为它们的执行顺序很特殊;
通过将公用函数与虚拟函数分离,可以蝴蝶明显的好处:
- 每个接口都能自然成形
- 基类拥有控制权
- 基类能够健壮的适应变化
第40条:要避免提供隐式转换
隐式转换所带来的影响经常是弊大于利,在为自定义类型提供隐式转换之前,请三思后行,应该依赖的是显式转换;
隐式转换有两个主要的问题:1. 它们会在最意料不到的地方抛出异常,2. 它们并不总是能与语言的其他元素有效地配合;
explicit: 指定为显式调用,而不是隐式转换;
第41条:将数据成员设为私有的,无行为的聚集(c语言形式的struct)除外
信息隐藏是优秀软件工程的关键,应该将所有数据成员都设为私有的,不管是现在,还是可能发生变化的将来,私有数据都是类用来保持其不变式的最佳方式;
第42条:不要公开内部数据
第43条:明智地使用Pimpl
C++将私有成员指定为不可访问的,但并没有指定为不可见的,虽然这样自有其好处,但是可以考虑通过Pimpl惯用法使私有成员正在不可见,从而实现编译器防火墙,并提高信息隐藏度;
使用Pimpl将私有部分隐藏在一个不透明的指针后面,用Pimpl来存储所有的私有成员,包括成员数据和私有成员函数,这使我们能够随意改变类的私有实现细节,而不用重新编译调用代码;
第44条:有限编写非成员非友元函数
要避免交成员费,尽可能将函数指定为非成员非友元函数;
第45条:总是成对提供new和delete
第46条:如果提供类专门的new,应该提供所有标准形式(普通、就地和不抛出)
不要隐藏好的new,如果定义了 operate new的重载,则应该提供operate new所有三种形式——普通、就地和不抛出的重载,不然,类的用户就无法看到和使用它们;
void* operator new(std::size_t); // 普通new
void* operator new(std::size_t, std::nothrow_t) throw(); // 不抛出new
void* operator new(std::size_t, void*); // 就地new
构造、析构与复制
第47条:以同样的顺序定义和初始化成员变量
与编译器一致,成员变量初始化的顺序要与类定义中的声明的顺序始终保持一致;不用考虑构造函数初始化列表中编写的顺序,要确保构造函数代码不会导致混淆地制定不同的顺序;
第48条:在构造函数中用初始化代替赋值
设置一次,到处使用;在构造函数中,使用初始化代替赋值来设置成员变量,能够放置发生不必要的运行时操作;
第49条:避免在构造函数和析构函数中调用虚拟函数
从构造函数或析构函数直接或者简介调用未实现的纯虚拟函数,会导致未定义的行为;
第50条:将基类析构函数设为公用且虚拟的,或者保护且非虚拟的;
如果允许通过指向基类Base的指针执行删除操作,则Base的析构函数必须是公用且虚拟的,否则,就应该是保护且非虚拟的。
如果允许多态删除,则析构函数必须是公用的,而且必须是虚拟的;
如果不允许多态删除,则析构函数必须是非公用的,而且应该是非虚拟的。
第51条:析构函数、释放和交换绝对不能失败
第52条:一致地进行复制和销毁
既要创建,也要清除,如果定义了复制构造函数,复制赋值操作符或者析构函数中的 任何一个,那么可能也需要定义另一个或者另外两个;
- 如果编写或者禁用了复制构造函数或者复制赋值操作符,那么可能需要对另一个也如法炮制;
- 如果显式的编写了复制函数,那么可能也需要编写析构函数;
- 如果显式的编写了析构函数,那么可能也需要编写或者禁止复制;
第53条:显式地启用或者禁止复制
要确保类能够提供合理的复制,否则就根本不要提供,
- 显式地禁止复制或赋值
- 显式地编写复制和赋值
- 使用编译器生成的版本,最好是加上一个明确的注释;
第54条:避免切片,在基类中考虑用克隆代替复制
在基类中,如果客户需要进行多态复制的话,那么请考虑禁止复制构造函数和复制赋值操作符,而改为提供虚拟的Clone成员函数;
第55条:使用赋值的标准形式
在实现operator=时,应该使用标准形式——具有特定签名的非虚拟形式;
第56条:只要可行,就提供不会失败的swap
swap既可无关痛痒,又能举足轻重,应该考虑提供 一个swap函数,高效且绝对无误地交换两个对象,这样的函数便于实现许多惯用法,从流畅地将对象四处移动以轻易地实现赋值,到提供一个有保证的,能够提供强大防错调用代码的提交函数;
名字空间与模块
第57条:将类型及其非成员函数接口置于同一名字空间中
非成员也是函数;如果要将非成员函数设计成类X的接口的一部分,那么就必须在于X相同的名字空间中定义它们,一遍正确调用。
第58条:应该将类型和函数置于不同的名字空间中,除非有意想让它们一起工作
经常会出现一些神秘的编译错误,一种可能的原因是ADL不正确地从其他名字空间中导入了名字,只是因为附近使用了哪些名字空间中的类型;
这个问题不只是和标准库的使用有关,在C++中,使用任何与自己不特别相关的函数,在同一名字空间中定义的类型,都可能而且确实会出现这一问题,切莫为之;
第59条:不要在头文件中或者#include之前编写名字空间 using
名字空间using是为了使我们更方便,而不是让我们用来叨扰别人的,绝对不要编写using声明或者在#include之前编写using指令;
可以而且应该在实现文件中的#include指令之后自由地使用名字空间级的using声明和指令,而且会感觉良好。
第60条:要避免在不同的模块中分配和释放内存
第61条:不要在头文件中定义具有链接的实体
重复会导致膨胀,具有链接的实体,包括名字空间级的变量或函数,都需要分配内存,在头文件中定义这样的实体将导致连接时错误或者内存的浪费,请将所有具有链接的实体放入实现文件;
// 要避免在头文件中定义具有外部链接的实体
int fudgeFactor;
string hello("hello, world")
void foo(/**/)
以上代码可能会导致链接器出现错误;
解决方法:
extern int fudgeFactor;
extern string hello;
void foo();
实际的定义则放在一个单独的实现文件中:
int fudgeFactor;
string hello("hello, world")
void foo();
同样,不要在头文件中定义名字空间级的static实体;static的错误使用比在头文件中只定义全局实体还要危险;
第62条:不要允许异常跨越模板边界传播
第63条:在模块的接口中使用具有良好可移植性的类型
模板与泛型
第64条:理智地结合静态多态性和动态多态性;
动态多态性是以某些类的形式出现的,这些类含有虚拟函数简介操作的实例,静态多态性则与模板类和模板函数有关;
第65条:有意地进行显式自定义
有意胜过无意,显式强似隐式,在编写模板时,应该有意地,正确地提供自定义点,并清晰地计入文档,在使用模板时,应该了解模板想要你如何进行自定义以将其 用于你的类型,并且正确地自定义;、
第66条:不要特化函数模板
只有在能够正确实施的时候,特化才能起到好作用;在扩展其他人的函数模板时,要避免尝试编写特化代码,相反,要编写函数模板的重载,将其放在重载所用的类型的名字空间中,编写自己的函数模板时,要避免鼓励其他人直接特化函数模板本身;
第67条:不要无意地编写不通用的代码
依赖抽象而非细节,使用最通用、最抽象的方法来实现一个功能;
毫无道理地依赖细节的代码将是僵化而脆弱的;
- 使用 != 代替 < 对迭代器进行比较
- 使用迭代代替索引访问
- 使用empty() 代替 size() == 0
- 使用层次结构中最高层的类提供需要的功能
- 编写常量正确的代码
错误处理与异常
第68条:广泛地使用断言记录内部假设和不变式
断言的强大怎么高估都不算过分;断言一般智慧在调试模式下生成代码,因此在发行版本中是不存在的,所以尽管进行检查好了;
要避免使用 assert(false) ,应该使用 assert(“info msg“)
第69条:简历合理的错误处理策略,并严格遵守
应该在设计早期开发实际、一致、合理的错误处理策略,并予以严格遵守,许许多多的项目对这一点的考虑都相当草率,应该对此有意识的规定,并认真应用,策略必须包含以下内容:
- 鉴别
- 严重程度
- 检查
- 传递
- 处理
- 报告
第70条:区别错误与非错误
第71条:设计和编写错误安全代码
确保出现错误时程序会处于有效状态,这是所谓的基本保证,要小心会破坏不变式的错误;
第72条:优先使用异常报告错误
出现问题时,就使用异常,应该使用异常而不是错误码来报告错误,但不能使用异常时,对于错误以及不是错误的情况,可以使用状态码来报告异常,当不可能从错误中恢复或者不需要恢复时,可以使用其他方法,比如正常终止或者非正常终止;
第73条:通过值抛出,通过引用捕获
学会正确捕获,通过值(而非指针)抛出异常,通过引用捕获异常,当重新抛出相同的异常时,应该优先使用throw避免使用throw e;
第74条:正确地报告、处理和转换错误
只要函数检查出一个它自己无法解决而且会使函数无法继续执行的错误,就应该报告错误;
第75条:避免使用异常规范
不要在函数中编写异常规范,除非不得已而为之,异常规范的主要问题在于,它们只不过“有些像”类型系统的一部分,它们的行为与大多数人所想象的都不同,而且它们实际所做的几乎总是与我们想要的不符。
STL:容器
第76要:默认时使用vector。否则,选择其他合适的容器
如果有充分的理由使用某个特定容器类型,那就用;
选择容器的原则:
- 编程时正确、简单和清晰是第一位的;
- 编程时只在必要时才考虑效率;
- 尽可能编写事务性的,强错误安全的代码;
vector有如下性质:
- 保证具有所有容器中最低的空间开销
- 保证具有所有容器中对所存放元素进行存取的速度最快;
- 保证具有与身俱来的引用局部性,也就是相邻对象在内存中相邻;
- 保证具有与C预研兼容的内存布局;
- 保证具有最灵活的迭代器;
- 几乎肯定具有最快的迭代器;
应该优先使用标准库的容器和算法,而不是特定于厂商的或手工编写的代码;
第77条:用vector和string代替数组
应该使用标准设施代替C风格数组,部分理由如下:
- 他们能够自动管理内存
- 他们具有丰富的接口
- 他们与C的内存模型兼容
- 他们能够提供更大范围的检查
- 他们支持上述特性并未牺牲太多效率
- 他们有助于优化;
第78条:使用vector(和string::c_str)与非C++API交换数据
vector不会在转换中迷失,不要将迭代器当做指针,要获取vector<T>::iterator iter所引用的元素地址,应该使用 &*iter
vector的存储区总是连续的,要获取第n个元素的指针,应该先做运算再取址;
第79条:在容器中只存储值和智能指针
第80条:用push_back代替其他扩展序列的方式
尽可能使用 push_back, 如果不需要操心插入位置,就应该使用 push_back 在序列中添加元素,其他方法更慢;
第81条:多用范围操作,少用单元素操作
第82条:使用公认的管用法真正地压缩容量,真正地删除元素
使用有效减肥法,要真正地压缩容器的多余容量,应该使用“swap 魔术”惯用法,要真正地删除容器中的元素,应该使用erase-remove惯用法
有些容器(如:vector,string,deque)可能最后会具有不再需要的多余容量,使用如下方法可以去除类似container类型容器c的多余容量:
container<T>(c).swap(c) // 去除多余容量的惯用法
完全情况,清除所有存放的元素并去除所有可能的容量,惯用法为:
container<T>().swap(c); // 去除全部内容和容量的惯用法
算法只操作于迭代器范围,不调用容器的成员函数,是不可能真正从容器中删除内容的。remove所做的就是移动值的位置,将不应该删除的元素移至范围的开始处,并返回一个迭代器指向最后一个不应删除元素的下一位置,要真正删除,需要在调用remove之后再调用erase——这就是 erase-remove惯用法,
c.erase(remove(c.begin(), c.end(), value), c.end())
STL:算法
多用算法,少用循环;
第83条:使用带检查的STL实现
安全第一,即使智能在发行前的测试中使用,也仍然使用带检查的STL实现;
以下STL错误很普遍:
- 使用已失效或未初始化的迭代器
- 传递越界索引
- 使用并非真是范围的迭代器范围
- 传递无效的迭代器位置
- 使用无效顺序
第84条:用算法调用代替手工编写的循环
使用STL的程序所用的显式循环比非STL程序的要少,而且使用了较高层的,定义更加的抽象操作代替了低层的、无语义的循环,应该采取“处理此范围”的算法性思维方式,跑气那种“处理每个元素的”循环思路;
还可以考虑尝试一下Boost的lambda函数,lambda函数是一个重要的工具,它解决了算法的最大问题——可读性;
第85条:使用正确的STL查找算法
选择查找方式应“恰到好处”——正确的查找方式应该使用STL,
查找无序范围,应使用 find/find_if 或者 count/count_if
查找有序范围,应使用lower_bound, upper_bound, equal_range 或者 binary_search
第86条:使用正确的STL排序算法
应该如下选择标准排序算法: partition, stable_partition, nth_element, partial_sort, sort, stable_sort
如果不是非用不可,应该不用任何排序算法,如果用的是标准的关联容器,那么其元素总是有序的。
第87条:使谓词成为纯函数
保持谓词的纯洁性,谓词就是返回是或否的函数对象,从数学的意义来说,如果函数的结果只取决于其参数,则该函数就是一个纯函数;
第88条:算法和比较器的参数应多用函数对象少用函数
对象的适配性比函数好,应该向算法传递函数对象,而非函数,关联容器的比较器必须是函数对象,函数对象的适配性好。
inline bool IsHeavy(const Thing&) {/\*…\*/}
find_if(v.begin(), v.end(), not1(IsHeavy)); // 错误,不可适配
find_if(v.begin(), v.end(), not1( ptr_fun( IsHeavy))); // 错误,不可适配
// 函数对象
struct IsHeavy:unary_function<Thing, bool>
{
bool operator()(const Thing&) const{ /* ... */ }
};
find_if(v.begin(), v.end(), not1(IsHeavy())); // 正确,可以适配了
第89条:正确编写函数对象
成本要低,而且要可适配,将函数对象设计为复制成本很低的值类型,尽可能地让它们从unary_function或binary_function集成,从而能够适配。
类型安全
第90条:避免使用类型粉质,多使用多态
避免通过对象类型分支来定制行为,使用模板和虚函数,让类型自己来决定行为。
通过类型分支来定制行为既不牢固,容易出错,又不安全。要添加新特性时必须贵过头对现有代码进行修改,它还不安全,因为添加新类型时,如果忘记修改所有分支,编译器也不会告知。
基于这一事实引入了开放——封闭原则,应该对扩展开放,而对修改封闭;
第91条:依赖类型,而非其表示方式
第92条:避免使用reinterpret_cast
不要尝试使用reinterpret_cast 强制编译器将某个类型对象的内存表示重新解释成另一种类型的对象,这违反了类型安全性的原则。
再次告诫:欺骗编译器的人,最终将自食恶果——henry Spencer
如果需要在不相关的指针类型之间强制转换,应该通过 void* 进行转换,不要直接用 reinterpret_cast ,
T1* p1 = ….;
T2* p2 = reinterpret_cast<T2*>(p1);
应该写成
T1* p1 = …;
void* pV = p1;
T2* p2 = static_cast<T2*>(pV);
第93条:避免对指针使用static_cast
不要对动态对象的指针使用 static_cast, 安全的替代方法有很多,包括使用 dynamic_cast, 重构,乃至重新设计
第94条:避免强制转换const
莫因恶小而为之,强制转换const有时会导致未定义的行为,即使合法,也是不良编程风格的主要表现;
如果对象的最初定义为const,强制转换掉它的常量行,将使所有保护失效,程序完全处于未定义行为状态。
第95条:不要使用C风格的强制转换
第96条:不要堆非POD进行memcpy操作或者memcmp操作
第97条:不要使用联合重新解释表示方式
通过在union中写入一个成员而读取另一个的滥用方式可以获得“无需强制转换的强制转换”,这比起 reinterpret_cast更阴险,也更难预测。
第98条:不要使用可变长参数(…)
省略号是来自C语言的危险遗产,要避免使用可变长参数,应该用高级的C++结构和库;
可变长参数有许多严重的缺点:
- 缺乏类型安全性
- 调用者与被调用者质检存在紧密耦合,而且需要手动协调
- 类类型对象的行为未定义
- 参数的数量未知;
第99条:不要使用失效对象,不要使用不安全函数
第100条:不要多态地处理数组
数组的可调整性很差,多态地处理数组是绝对的类型错误,而且编译器有可能不会做出任何提示,不要掉入这一陷阱;