1。组织和策略问题
如果人们按照程序员编程的方式修建房屋,那么一只啄木鸟就能毁灭整个文明。
第0条:不要拘泥于小节(又名:了解哪些东西不应该标准化)
不要规定缩进多少,应该规定要用缩进来体现代码的结构
不要强制行的具体长度,应该保证代码行的长度有利于阅读
不要在命名规范方面规定太多,应该规定的是使用一致的命名规范
不要规定注释体例(除非需要使用工具从特定的体例中提取文档),应该编写有用的注释
第1条:在高警告级别干净利落地进行编译
比如常见的警告:
第三方头文件引起的
未使用的函数参数
定义了从未使用的变量
变量使用前可能未经初始化
遗漏return语句
有符号/无符号数的不匹配
第2条:使用自动构建系统
单操作构建过程
第3条:使用版本控制系统
VCS
第4条:在代码审查上投入
2。设计风格
复杂性啊,愚人对你视而不见。实干家受你所累。有些人避而远之。惟智者能够善加消除。
不成熟的优化是程序设计中的万恶之源。
第5条:一个实体应该只有一耳光紧凑的职责
失败的设计:
realloc:如果传入的指针参数为NULL就分配空间,如果传入的大小参数为0就释放空间
basic_string:毫无道理地重复了许多标准算法,而扩展所留的裕度很小
第6条:正确、简单和清晰第一
程序以人为本,第二是计算机
不要使用不必要的或者小聪明式的操作符重载
应该使用命名变量,而不要使用临时变量,作为构造函数参数
第7条:编程中应该知道何时和如何考虑可伸缩性
动态分配内存和使用高效算法。。。
第8条:不要进行不成熟的优化
让一个正确的程序更快速,比让一个快速的程序正确,要容易的太多、太多。
inline悖论
第9条:不要进行不成熟的劣化
避免不必要的劣化哦~
第10条:尽量减少全局和共享变量
共享会导致冲突
为“无共享”而奋斗吧,用通信方式代替数据共享。
第11条:隐藏信息
不要泄密:不要公开提供抽象实体的内部信息。
第12条:懂得何时和如何进行并发性编程
这是个大课题:
参考目标平台的文档,了解该平台的同步化原语。
最好将平台的原语用自己设计的抽象包装起来。
确保正在使用的类型在多线程程序中使用时安全的:保证非共享的对象独立,记载调用者在不同线程中使用该类型的同一对象需要做什么。加锁力度要适合,不便对象无须加锁
第13条:确保资源为对象所有。使用显示的RAII和智能指针
RAII:resource acquisition is initialization,资源获取即初始化
3。编程风格
一个人的常量是另个人的变量
第14条:宁要编译时和链接时错误,也不要运行时错误
第15条:积极使用const
常量很安全,在编译时会对其进行检查,而且与c++系统已浑然一体
函数中值传递const与非const的值是等效的
第16条:避免使用宏
抽象设施中最生硬的东西
在c++中几乎从不需要宏。可以用const或者enum定义易于理解的常量,用inline避免函数调用的开销,用template指定函数系列和类型系列,用namespace避免名称冲突
不要使用宏,除非不得不用
宏不能理解尖括号
第17条:避免使用魔数
常量应该是枚举或者const值,有合适的作用域和名称。
第18条:尽可能局部地声明变量
第19条:总是初始化变量
例外情况:硬件或者其他进程直接写入的输入缓冲区数据和volatile型数据不需要程序对其进行初始化
第20条:避免函数过长,避免嵌套过深
尽量紧凑:对一个函数只赋予一种职责
不要自我复制:优先使用命名函数,而不要让相似的代码片段反复出现
优先使用&&:在可以使用&&条件判断的地方要避免使用连续嵌套的if
不要过分使用try:优先使用析构函数进行自动清楚而避免使用try代码块
优先使用标准算法:算法要比循环嵌套要少,通常也更好
不要根据类型标签进行分支:优先使用多态函数
第21条:避免跨编译单元的初始化依赖
保持初始化顺序:不同编译单元中的名字空间级对象绝不应该在初始化上互相依赖,因为其初始化顺序是未定义的。
第22条:尽量减少定义性依赖。避免循环依赖
第23条:头文件应该自给自足
第24条:总是编写内部的#include保护符,决不编写外部#include保护符
防止多次包含
例如在foo.h中使用
#iifndef FOO_H_INCLUDE_
#define FOO_H_INCLUDE_
//文件内容
#endif
而非下面类型
#iifndef FOO_H_INCLUDE_
#include “foo.h”
#define FOO_H_INCLUDE_
#endif
4。函数与操作符
如果一个过程有10个参数,那么你很可能还遗漏了一些
第25条:正确地选择通过值、(智能)指针或者引用传递参数
对于只输入参数:
始终用const限制所有指向只输入参数的指针和引用
优先通过值来取得原始类型(如char、float)和赋值开销比较低的值对象(如Point、complex<float>)的输入
优先按const的引用取得其他用户定义类型的输入
如果函数需要其参数的副本,则可以考虑通过值传递代替通过引用传递。这在概念上等同于通过const引用传递加上一次复制,能够帮助编译器更好地优化掉临时变量。
对于输出参数或者输入\输出参数:
如果参数是可选的或者参数需要保存这个指针的副本或者操控参数的所有权,那么应该优先通过(智能)指针传递
如果参数是必须的,而且函数无需保存指向参数的指针,或者无需操纵器所有权,那么优先通过引用传递。这表明参数是必须的,而且调用者必须提供有效对象。
第26条:保持重载运算符的自然语义
第27条:优先使用算术操作符和赋值操作符的标准形式
第28条:优先使用++和--的标准形式。优先调用前缀形式
对于++和--而言,后缀形式返回的是原值,而前缀形式返回的是新值
T& T::operator++(){
// 执行递增
return *this;
}
T T::operator++(int){
T old(*this);
++*this;
return old;
}
第29条:考虑重载以避免隐含类型转换
奥卡姆剃刀原理:如无必要,勿增实体
第30条:避免重载&&、||或,(逗号)
第31条:不要编写依赖于函数参数求值顺序的代码
使用命名对象控制求值顺序
5。类的设计与继承
软件开发最重要的一个方面就是弄清楚自己要构建的是什么。
第32条:弄清所要编写的是哪种类
值类:(模仿内置类型)
有一个公用析构函数,复制构造函数和带有值语义的赋值
没有虚拟函数
是用作具体类而不是基类
总是在栈内实例化,或者作为另一类直接包含的成员实例化
基类:(类层次结构的构造要素)
有一个公共而且虚拟,或者保护而且非虚拟的析构函数,和一个非公用复制构造函数和赋值操作符
通过虚拟函数建立接口
总是动态地在堆中实例化为具体派生类对象,并通过一个(智能)指针使用
trait类:(携带有关类型信息的模板)
只包含typedef和静态函数。没有可修改的状态或者虚拟函数
通常不实例化(其构造一般是被禁止的)
策略类:(通常是模板,可插拔行为的片段)
可能有也可能没有状态或者虚拟函数
通常不实例化,只作为基类或者成员
异常类:(值抛出,引用捕获)
有一个公用析构函数和不会失败(no-fail)的构造函数
有虚拟函数,经常实现克隆和访问(visitation)
从std::exception虚拟派生更好
第33条:用小类代替巨类
第34条:用组合代替继承
避免继承带来的重负:继承是c++中第二紧密的耦合关系,仅次于友元关系
第35条:避免从非要设计成基类的类中继承
第36条:优先提供抽象接口
依赖倒置原理(DIP):
高层模块不应该依赖于低层模块。相反,两者都应该依赖抽象
抽象不应该依赖于细节。相反,细节应该依赖于抽象
二次机会定律:需要保证正确的最重要的东西是接口
第37条:公用继承即可替换性。继承不是为了重用,而是为了被重用
公用继承能够使基类的指针会者引用实际指向某个派生类的对象,既不会破坏代码的正确性,也不需要改变已有代码。
不要通过公用继承重用(基类中已有)代码,公用继承是为了被(已经多态地使用了基对象的已有代码)重用的。
第38条:实施安全的改写
#include <iostream>
using namespace std;
class Base{
private:
int reg;
public:
virtual void foo(int x=1)
{cout<<"Base:"<<x<<endl;}
virtual void foo(int x, int y)
{
cout<<"Base:"<<x<<y<<endl;
}
};
class Derived:public Base{
private:
int reg1;
public:
virtual void foo(int x=2) //改写Base::foo(int),但是隐藏了其他重载函数
{
cout<<"Derived:"<<x<<endl;
}
using Base::foo; //将其他Base::foo重载函数引入作用域
};
int main(int argc, char* argv[])
{
Derived *pd=new Derived;
pd->foo(); //打印Derived:2
Derived dd;
dd.foo(1,2); //打印Base:12 如果不使用using Base::foo;该句编译出错,原因在于隐藏
Base *pb=pd;
pb->foo(); //打印Derived:1,可能会有趣
return 0;
}
第39条:考虑将虚拟函数声明为非公用的,将公用函数声明为非虚拟的
此建议不适合析构函数
第40条:要避免提供隐式转换
explicit构造函数和命名转换函数
隐式转换:会在意料不到的地方抛出异常,并不总是能与语言的其他元素有效地配合
第41条:将数据成员设为私有,无行为的聚集(C语言形式的struct)除外
第42条:不要公开内部数据
避免返回类所管理的内部数据的句柄
第43条:明智地使用Pimpl
pimpl 惯用法(指向实现的指针 "pointer to implementation")。在C++中尽量减少头文件之间依赖,有效缩短编译时间的方法。
第44条:优先编写非成员非友元函数
第45条:总是一起提供new和delete
重载void* operator new(parms)必须伴有与其对应的重载void operator delete(void*, parms),因为编译器要调用它们
第46条:如果提供类专门的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
static void* operator new(std::size_t, void*); //隐藏其他两种形式
应该总是避免隐藏就地new,因为它在STL容器中有广泛的使用
6。构造、析构与复制
标准可能置你于悬崖之畔,但你大可不必仅仅因此就跳下去。
第47条:以同样的顺序定义和初始化成员变量
第48条:在构造函数中用初始化代替赋值
在初始化列表中初始化成员变量
第49条:避免在构造函数和析构函数中调用虚拟函数
第50条:将基类析构函数设为公用且虚拟的,或者保护且非虚拟的
总是为基类编写析构函数,因为隐含生成的析构函数是公用且非虚拟
含有多态删除的基类。如果允许多态删除,则析构函数必须是公用的(否则调用代码就无法调用它),而且必须是虚拟的(否则调用它就会导致未定义行为)
不含多态删除的基类。如果不允许多态删除,则析构函数必须是非公有的(这样调用代码就不能调用它),而且应该是非虚拟的(因为不需要是虚拟的)
策略类经常用作基类,是出于方便考虑,而不是出于多态行为考虑的。建议将他们的析构函数设为保护且非虚拟。
第51条:析构函数、释放和交换绝对不能失败
如果在栈展开期间所调用的析构函数发生异常而退出,将调用terminate。因此,析构函数应该总是能捕捉异常,并且不会让异常传播到析构函数之外
C++标准库中定义的析构函数操作[包括用来实例化标准库模板的任何类型的析构函数]都不会抛出异常。
第52条:一致地进行复制和销毁
第53条:显示的启用或者禁用复制
应该对这两种操作采取主动行动,因为编译器喜欢慷慨地替我们生成,而对非值类型的类型而言,这种编译器所生成的版本默认情况下经常是不安全的
显式地禁止复制和赋值:
class T{ // ……
private: // 使T不能复制
T(const T&); // 未实现
T& operator=(const T&); // 未实现
};
第54条:避免切片。在积累中考虑用克隆代替复制
class B{
public:
B* 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;
};
class D:public B{
};
第55条:使用赋值的标准形式
T& operator=(const &); // 传统的
T& operator=(T); // 可能更方便的优化器
不要返回const T&。虽然这有助于防止(a=b)=c这样奇怪的代码,但是它有负作用:你将不能把T对象放入标准库容器中:容器要求赋值返回一个普通的T&.
要避免将赋值操作符设为虚拟的
第56条:只要可行,就提供不会失败的swap(而且要正确地提供)
class T{
public:
void swap(T& rhs){
member1_.swap(rhs.member1_);
std::swap(member2_, rhs.member2_);
}
private:
U member1_;
int member2_;
};
T& T::operator=(const T& other)
{
T temp(other);
swap(temp);
return *this;
}
T& T::operator =(T temp)
{
swap(temp);
return *this;
}
7。名字空间与模块
系统会有子系统,子系统还会有子系统,以此类推,直至无穷——这正是我们总是从头再来的原因。
第57条:将类型及其非成员函数接口置于同一名字空间中
第58条:应该将类型和函数分别置于不同的名字空间中,除非有意想它们一起工作
避免将类型和模板化函数或者操作符放在相同的名字空间中
第59条:不要在头文件中或者#include之前编写名字空间using
第60条:要避免在不同的模块中分配和释放内存
shared_ptr
第61条:不要在头文件中定义具有链接的实体
具有链接的实体包括名字空间级的变量或函数,都需要分配内存。在头文件中定义这样的实体将导致链接时错误或者内存的浪费
只在头文件放置声明
不要在头文件中定义名字空间级的static实体
例外, 以下具有外部链接的实体可以放在头文件中
内联函数、函数模板、类模板的静态数据成员
第62条:不要允许异常跨越模块边界传播
C++异常处理没有普遍通用的二进制标准
应用程序必须在以下位置捕获所有异常:在main函数附近,在无法控制的代码中执行回调附近,在线程边界的附近,在模块接口边界的附近,在析构函数内部。
第63条:在模块的接口中使用具有良好可移植性的类型
使用的抽象层次越低,可移植性就越好,但复杂性也越高
8。模板与泛型
此处的名言留给读着
第64条:理智地结合静态多态性和动态多态性
动态多态性是以某些类的形式出现的,这些类含有虚拟函数和(通过指针或者引用)间接操作的实例。静态多态性则与模板类和模板函数有关。
第65条:有意地进行显式自定义
将模板内部使用的任何副主函数都放入其自己的内嵌名字空间
要避免依靠依赖名
第66条:不要特化模板
第67条:不要无意地编写不通用的代码
使用!=代替<对迭代器进行比较
使用迭代器代替索引访问
使用empty()代替size()==0
使用层次结构中最高层的类提供需要的功能
编写常量正确的代码
9。错误处理与异常
错误处理是一种困难的任务,为此程序员需要所能提供的一切帮助
编写无措的程序有两种方式,但是只有第三种才真正奏效
第68条:广泛地使用断言记录内部假设和不变式
assert(false)
第69条:建立合理的错误处理侧率,并严格遵守
鉴别错误,严重程度,检查错误代码,传递错误通知,处理错误代码,报告错误
错误处理机制应该只在模块边界改变,按定义回调函数和县城主线函数是(或者可能是)位于模块边界。
第70条:区别错误和非错误
错误就是阻止函数成功操作的任何失败:违反或者无法满足前条件,无法满足后条件,无法重新建立不变式。
第71条:设计和编写错误安全代码
第72条:优先使用异常报告错误
异常不能不加修改地忽略,异常是自动传播的,有了异常处理就不必在控制流的主线中加入错误处理和恢复了,对于构造函数和操作符报告错误来说异常处理要优于其他方案
第73条:通过值抛出,通过引用捕获
通过值(而非指针)抛出异常,通过引用(通常是const引用)捕获异常。当重新抛出相同的异常时,应该优先使用throw;,避免使用throw e。
第74条:正确地报告、处理和转换错误
第75条:避免使用异常规范
不要在函数中编写异常规范,除非不得已为之
10。STL:容器
如果需要容器,默认时应使用vector
第76条:默认时使用vector。否则选择其他合适的容器
第77条:用vector和string代替数组
第78条:使用vector(和string::c_str)与非C++API交换数据
还有string::data:返回是指向连续内存的指针,但不保证以空字符结束
string::c_str:返回一个空自负结束的C风格字符串
要获取vector<T>::iterator iter所引用的元素地址,应该使用&*iter
第79条:在容器中只存储值和智能指针
值类型(直接存放),智能指针,迭代器
第80条:用push_back代替其他扩展序列方式
它是按指数级扩大容量的,而不是按固定增量扩大的
第81条:多用范围操作,少用单元素操作
第82条:使用公认的惯用法真正地压缩容量,真正地删除元素
erase-remove惯用法
要删除容器c中所有等于value的元素:c.erase(remove(c.begin(), c.end(), value), c.end());
shrink-to-fit(压缩到合适位置):container<T>(c).swap(c) //去处多余容量
11。STL:算法
多用算法,少用循环
第83条:使用带检查的STL实现
第84条:用算法调用代替手工编写的循环
第85条:使用正确的STL查找算法
对于无序:find/find_if和count/count_if都能够以线性时间告知,元素是否在某个范围中,以及元素在范围中哪个位置
对于有序:binary_search、lower_bound、upper_bound、equal_range,对数时间
第86条:使用正确的STL排序算法
线性时间:partition、stable_partition\nth_element
随机访问存储器:nth_element、partial_sort、sort、stable_sort
第87条:使谓词成为纯函数
谓词就是返回是或否(返回值通常为bool类型)的函数对象
第88条:算法和比较器的参数应多用函数对象少用函数
第89条:正确编写函数对象
12。类型安全
试图与编译器斗智的人,最终将无法很好地使用编译器
第90条:避免使用类型分支,多使用多态
第91条:依赖类型,而非其表示形式
成员变量根据声明的顺序存储,int至少有16位。
第92条:避免使用reinterpret_cast
不要尝试使用reinterpret_cast强制编译器将某个类型对象的内存表示重新解释成另一种类型的对象
如果需要在不相关的指针类型之间强制转换,应该通过void*进行转换
比如:
T1* p1=..;
void* pV=p1;
T2* p2=static_cast<T2*>(pV);
第93条:避免对指针使用static_cast
不要对动态对象的指针使用static_cast:替代的方法有dynamic_cast,重构,乃至重新设计
当且仅当dynamic_cast的开销确实成问题时,才应该考虑自定义强制转换,在调试时用dynamic_cast,而在“全速无保证”模式下用static_cast。
第94条:避免强制转换const
在调用常量不正确的API时,强制转换掉const可能是必须的。
第95条:不要使用C风格的强制转换
第96条:不要对非普通旧式数据进行memcpy操作或者memcmp操作
第97条:不要使用联合重新解释表示方式|
union{
long intValue_;
char* pointerValue_;
};
pointerValue_somePointer;
long int gotcha=intValue_;
第98条:不要使用可变长参数(...)
第99条:不要使用失效对象。不要使用不安全函数
strcpy,strncpy,sprintf
第100条:不要多态地处理数组