2.3 过程式程序设计:确定你需要哪些过程;采用你能找到的最好的算法。
2.4 模块式程序设计:确定你需要哪些模块;将程序分为一些模块,使数据隐藏于模块之中。
2.5.2 用户定义类型(亦抽象数据类型):确定你需要哪些类型;为每个类型提供完整的一组操作。
2.6.2 面向对象的程序设计:确定你需要哪些类;为每个类提供完整的一组操作;利用继承去明确地表示共性。
2.7 泛型程序设计:确定你需要哪些算法;将它们参数化,使它们能够对各种各样的类型和数据结构工作。
(thy——以上的设计范型无所谓好与坏,而在于其适用范围。比如在交互式图形软件中用面向对象设计比较多,而在库的设计中范型设计则比较多。但在经典的算术类型计算中,则是模块化与抽象类型的设计比较多。)
2.8 没有一种程序设计语言是完美无缺的。幸运的是,一种程序设计语言不必是完美无缺的,也可以成为构造会大系统的良好工具。
学习一种语言的工作就应该集中于去把握对该语言而言固有的和自然的风格——而不是去理解该语言的所有语言特征的细枝末节。
把一种特征孤立起来看并没有什么意思,只是在由技术和其他特征所形成的环境里,这一特征才获得了意义和趣味。
2.9 请特别关注程序设计技术,而不是各种语言特征。
3.7.2 对vector的一个简单包裹以使其带有下标检查:
STL中operator[]没有使用下标检查是为了保证其效率。
3.7.3 一个小技巧:
3.8.6 算法的一个一般性的定义是:“一组有穷的规则,它给出了为解决一个特定问题集合的一个操作序列,[并]包含五个重要特征:有穷性……定义性……输入……输出……效率”
3.11 在main()中捕捉公共异常
4.9.3 选择好的名字也是一种艺术。(让名字反映其意义而不是反映其实现方式 )
应设法保持一种统一的命名风格。(如:对非标准库的用户定义类型都用大写Shape,非类型的开头用小写current_token,宏和常量用全部大写的名字HACK,并用下划线分隔名字中的单词)(thy——按《代码大全》的说法,好的变量名字应该是名词+形容词,好的函数名字应该是动词+名词 )
4.9.5 如果没有提供初始式,全局的、名字空间内的和局部静态的对象(以上统称静态变量)将被自动初始化为适当类型的0。局部对象(自动对象)和在*存储区里建立的对象(动态对象、堆对象)将不会用默认值做初始化。
4.10 保持较小的作用域,在一个声明中只声明一个名字,仔细选择名字,反映其意义而不是反映实现方式。
5.2.2 为使程序比较整洁,可以将长字符串通过空白字符断开。如
带有前缀L的字符串,如L"angst",是宽字符的字符串。其类型是const wchar_t[]。
5.3.1 指针相加没有任何意义,因此是不允许的。
5.4.1 指针与const
char * const cp; //到char的const指针
char const * cp; //到const char的指针
const char * cp; //到const char的指针
有人发现从右向左(按英文)读这种定义很有帮助。(即:读到*则已经读完指针的const属性,剩下的则是指针的类型——thy )
5.5 为了提高程序的可读性,通常应该尽可能避免让函数去修改它们的参数。
5.6 在系统中较高层次上出现的void*应该认为是可疑的,它们就像是设计错误的指示器。
5.7 结构类型对象的大小未必是其成员的大小之和,这是因为......在这类机器上,对象被称为具有对齐的性质。对齐会在结构中造成“空洞”。
5.8 避免非平凡的指针算术(非平凡nontrival,即做了非零的、不是什么也不做什么也没有的——thy )。尽量少用普通的引用参数。避免void*,除了在某些低级代码里。
6.1.2 (从cin扫描计算语言的例程:)
6.1.7 (自适应从命令行参数读入数据还是从cin读入)
6.2.1 int* p = &++x; //p指向x
int* q = &(x++); //错误:x++不是一个左值(它并不存储在x中)
6.2.2 在一个表达式里,子表达式的求值顺序是没有定义的。
int x = f(2)+g(3); //未知哪个先调用
v[i] = i++; //无定义结果
运算符','、‘'&&'、'||'保证了位于它们左边的运算对象一定在右边运算对象之前求值。 如b=(a=2, a+1); 将把3赋给b。
6.2.5 while(*p++ = *q++); //实现拷贝以0结尾的字符串
6.2.6 delete运算符对0应用不会造成任何影响。
6.2.6.2 当new无法找到需要分配的空间时,默认将抛出bad_alloc异常。
当new失败时,它将先去调用(如果存在)由<new>里声明的set_new_handler()设定的函数。
此程序不会到达写出done的地方。
6.2.7 int* p = static_cast<int*>(malloc(100));
IO_device* dio = reinterpret_cast<IO_device*>(0Xff00);
//位于0Xff00的设备
static_cast完成相关类型之间的转换,而reinterpret_cast完成无关类型之间的转换。
6.3.3 按照我的经验,do语句是错误和混乱的一个根源。所以要避免使用它。
6.4 一组经过良好选择和良好书写的注释是好程序中最具本质性的一个组成部分。写出好注释可能就像写出好程序一样困难,但这是一种值得去好好修养的艺术。
6.5 只对定义良好的构造使用T(e)记法。
避免带有无定义求值顺序的表达式。
7 迭代的是人,递归的是神(——L. Peter Dautsch )
7.5 int f(int, int=0, char* =0);
注意,在最后*和=之间的空格是重要的。(*=是一个赋值运算符)
7.8 关于宏的第一规则是:绝不应该去使用它,除非你不得不这样做。
7.8.1 有一种宏的使用几乎不可避免。(条件编译)
8.2.2 将同义词(如using std::cout)尽可能地保持在局部中以避免混乱,这是一种很好的想法。
8.2.3 在一个函数里,可以安全地将使用指令(如using namespace std)作为一种方便的记法方式。(但要避免全局性使用)
8.2.4 设计界面是最基本的设计活动之一,而且是一种可以获得或者丧失重要利益的活动。
8.2.4.1 界面的作用就是尽可能减小程序不同部分之间的相互依赖。最小的界面将会使程序易于理解,有很好的数据隐蔽性质,容易修改,也编译得更快。
(这样在使用时便可以与Parser的实现解耦——thy )
还可以在Parser_interface中实现函数代理:
8.2.5.1 无名名字空间的目的只是保持代码的局部性,而不是为用户提供界面。(是C++标准的推荐写法)
无名名字空间有一个隐含的使用指令(using namespace)。
实际上等价于:
这里$$$仅仅是指代某个在这此名字空间定义所在的作用域里具有惟一性的名字。(这样无名名字空间内的变量和函数便只能在本编译单元内使用,而提供给其它编译单元使用的必须在有名字的名字空间内——thy )
8.2.7 名字空间别名即可以解决短名字易冲突,又可以解决长名字不实用的矛盾。
名字空间别名还可以用于解决引用的库的版本更新问题:
namespace Lib = Foundation_library_v2r11;
当这个库的版本更新到v3r01时,只要改变Lib的定义就可以了:
namespace Lib = Foundation_library_v3r01;
8.2.8.1 通过一个使用声明就可以将一个重载函数的所有变形都带进来:
using My_string::to_string;
8.2.9.2 重载可以跨名字空间工作。
8.2.9.3 名字空间是开放的:
namespace A { int f();}
接下来再定义:
namesapce A { int g();}
此时A就有了成员f()和g()。
通过这种方式,我们就能支持将一个名字空间放入几个大的程序片段。但这样也会无意中重新打开名字空间并塞入新的名字。
名字空间别名不能重新打开它所代表的名字空间。
8.3.3
使用:throw Error::Syntax_error("bad token");
catch(Error::Syntax_error e) {}
8.4 将每个非局部的名字放入某个名字空间。
只有在转换时,或者在局部作用域中才用using namespace。
采用用户定义类型作为异常,不用内部类型(如int)。
当局部控制结构中心应付时,不用异常。
9.2 为了保证一致性,你一般应该把inline和全局的const都仅仅放在头文件中。
9.2.1 头文件里绝不应该有:常规的函数定义(而应该放函数声明extern int func(){}和在线函数定义inline int func(){})、数据定义(而应该放数据声明extern int a;、常量定义const int a=1;、枚举enum Light{red,yellow,green})、聚集量(aggregate)定义如数组、无名名字空间(而应该放命名名字空间)、导出的模板定义export template<class T>f(T t){}(而应该放模板声明template和模板定义)
9.2.2 相对于每一个C标准库文件<X.h>,存在着一个与之对应的标准C++头文件<cX>。
9.2.4 extern "C"中的C表示一个连接约定,而不是一种语言(它也可以用于连接Fortran和汇编例程)
可以由预编译建立起公共的C和C++头文件:
9.3.2 对于中等以上规模的程序,可以采用多个头文件的组织方式:
实现:
9.3.3 包含保护符(我总选择相当长的非常难看的名字作为我的包含保护符 ):
9.4.1 对于不同编译单位里的全局变量,其初始化的顺序则没有任何保证。
通过函数返回的引用可以作为全局变量的一种很好的替代物。
对use_count()的调用在行为上就像是一个全局变量,除了它是在第一次使用时才被初始化之外。
10.4.6 构造函数按照成员在类中声明的顺序执行,而不是按这些成员在初始式表中出现的顺序。为了避免混乱,最好还是按照各成员的声明顺序描述这些初始式。
析构的顺序与构造的顺序相反。
10.4.6.1 const和引用只能使用初始式进行初始化。
10.5 如果适用的话就尽可能使用具体类,而不要采用更复杂的类,也不要用简单的数据结构。(可以用namespace来提供具体类的协助函数,这样可以使得类的界面更清晰、更小,尤其是针对某个类的全局运算符重载函数——thy )
11.2 不能重载的运算符有::(作用域解析)、.(成员选择)、.*(通过到成员的指针做成员选择)、?:(三元条件运算符)、sizeof、typeid。
11.3.1 +运算符涉及到3个对象(两个运算对象和一个结果),而在+=运算中只涉及到两个对象。对于+=可通过清除对临时变量的需要,可以改进运行效率。
11.4 总而言之,少定义一些转换运算符是更明智的。
11.4.1 能合法进行的用户定义隐式转换只有一层。
11.5.1 一个友元类必须是在外围作用域里先行声明的,或者是在将它作为友元的那个类的直接外围的非类作用域里定义的。
例如:
一个友元函数或者需要在某个外围作用域里显式声明,或者以它的类或由该类派生的类作为一个参数。
例如:
11.9 (operator()()可以使一个对象成为一个函数对象,可以与STL算法很好地搭配——thy )
注意for_each的定义:
11.14 将只有一个“大小参数”的构造函数做成explicit
12.1 这并不是从“糟糕的老技术”到“惟一的正确途径”的简单过渡。当我指出某种技术的局限性,作为通往另一种技术的推动力时,我总是在一个特定问题的环境中做这件事;而对于不同的问题或其它环境,第一种技术很可能反而成为更好的选择。
12.2 用一个类作为基类,相当于声明一个该类的(匿名)对象。所以,要想作为基类,这个类就必须有定义。(而不能仅仅class Class; )
12.2.5 使用类型域是一种极容易出错的技术,它还引起维护中的大麻烦。
12.4.1 对于几乎所有情况,保护界面都只应该包含函数、类型和常量。
12.5 一个系统展现给用户的应该是一个抽象类的层次结构,其实现所用的是一个传统的(类)层次结构。
12.6 用抽象类去尽可能减少用户代码的重新编译。
用抽象类使不同的实现能够共存。
抽象类通常不需要构造函数。
13.2.3 模板参数可以是常量表达式,具有外部连接的对象或者函数的地址,或者非重载的指向成员的指针。特别地:字符串文字量不能被接受为模板参数。
13.2.5 与模板参数相关的错误能被检查出来的最早位置,也就是在这个模板针对该特定模板参数的第一个使用点。这一点通常被称为第一实例化点,简称实例化点。
13.3.1 (对于函数模板的参数,)编译器能够从一个调用推断出类型参数和非类型参数,条件是由这个调用的函数参数表能够惟一地标识出模板参数的一个集合。(否则我们就必须显式地去描述它——像模板类一样用<> )
13.5 如果要对元素为指针的Vector做一个专门的形式,使之产生如下效果:
template<class T> class Vector {};
而Vector<int> vi;与Vector<int*> vpi;能够生成不同样的Vector。
首先定义一个到void的指针的专门化的Vector版本。
template<> class Vector<void*> {};
前缀template<>说明可以不用模板参数描述,将用于定义Vector<void*> vpv;
要定义一个专门化,使它能够用于所有的指针的Vector,且仅仅用于指针的Vector,就需要一个部分专门化:
template<class T> class Vector<T*>: private Vector<void*> {};
中间的<T*>说明这个专门化将被用于每个指针类型。
这里所用的继承可以将关键性的实现细节隐藏在一个公共界面之后,这种技术在抑制代码膨胀方面非常有效。
通用模板必须在所有专门化之前声明(且在同一个名字空间)。
13.6.1 什么时候我们应该选用模板,什么时候应该依靠一个抽象类?在这两种情况下,我们都是在操纵一些共享着一组操作的对象。如果在这些对象间并不需要某种层次性的关系,那么最好是将它们作为模板的参数。如果在编译时无法确知这些对象的类型,那最好是将它们表示为一个公共抽象类的许多派生类。如果对运行时效率的要求特别严格,即必须将各种操作在线化,那么就应该使用模板。
13.6.2 成员模板
一个类或者模板也可以包含本身就是模板的成员。如果
当且仅当能够用Scalar对T进行初始化(即存在Scalar到T的转换),就可以从一个complex<Scalar>构造出一个complex<T>,比如:
complex<float> cf(0,0);
complex<double> cd=cf; //可以,用float到double的转换
complex<int> ci=cf; //不可以,不存在float到int的转换
13.6.3.1 模板转换(用来处理继承关系)。考虑:
现在有两个类:
class Shape {};
class Circle: public Shape {};
现在可以处理继承关系了:
Ptr<Circle> pc;
Ptr<Shape> ps = pc; //可以,Circle*能转换到Shape*
Ptr<Circle> pc2 = ps; //不可以,Shape*不能转换到Circle*
(thy——成员模板与模板转换均可以处理继承关系,模板转换实际上是显示地调用了成员模板的拷贝构造 )
13.7 在任何地方要使用模板,都要求它的定义必须位于作用域中(thy——通常是在本编译单元中,或者是在头文件中 ),否则(thy——想放在实现文件中 )必须将模板定义显式地声明为export。
13.8 考虑一个模板是否需要有针对C风格字符串和数组的专门化。
用表述策略的对象进行参数化。
在修改为通用模板之前,在具体实例上排除程序错误。
如果模板定义需要在其他编译单位里访问,请记住写export。
对大模板和带有非平凡环境依赖性的模板,应采用分开编译的方式。
通过显式实例化减少编译和连接时间。
如果运行时的效率非常重要,那么最好用模板而不是派生类。
如果增加各种变形而又不重新编译是很重要的,最好用派生类而不是模板。
如果无法定义公共的基类,最好用模板而不是派生类。
当有兼容性约束的内部类型和结构非常重要时,最好用模板而不是派生类。
14.1.1 异常处理机制是一种非局部的控制结构,基于堆栈回退,因此也可以看做是另一种返回机制。
“异常”并不意味着“几乎不出现”或者“灾难性的”。最好是把异常想象为表示“系统的某些部分不能完成要它做的事情了”。
14.2 将异常组织为一些层次结构对于代码的健壮性可能很重要。
14.2.1 (thy——对抛出异常的调用最好是通过引用调用而非值调用,这样即可以提高效率,又可以避免多态异常被捕捉时的“切割问题”。 )
14.3 我们不应该抛出一个不允许复制的异常。
系统中总存在着足够的存储,使new可以抛出一个标准的存储耗尽异常bad_alloc。
14.4 在堆栈回退的过程中,将会对所有构造起来的局部对象调用析构函数。
14.4.3 并不是所有的程序都需要有从所有破坏中恢复起来的能力(即需要“资源申请即初始化”技术)。但特别是对于库的设计者,他们通常不能在容错需求方面对使用这个库的程序作出任何假设,因此就必须避免所有无条件的运行时失败,而且必须要求库函数在返回程序前释放所有的资源。“资源申请即初始化”策略,结合用异常去发出失败信号等,对于许多这样的库十分合适。
14.4.5 与去调用由某调用函数提供的一个帮助例程相比,异常处理机制对这种(代码)隔离的支持更好一些。
14.4.6.1 如果一个成员的初始式抛出异常,这个异常将传到调用这个成员的类的构造函数的位置。构造函数本身也可以通过将完整的函数体包在一个try块里,自己设法捕捉这种异常。例如:
14.4.7 绝不能让析构函数里抛出异常,否则,那将被认为是异常处理机制的一次失败,并调用std::terminate()。如果某个析构函数要调用一个可能抛出异常的函数,它可以保护自己。例如
14.5 与局部控制结构如果if和for相比,异常处理是一种结构化更差的机制,通常在实际抛出异常时效率也更低。因此,应该只把异常机制用在那些使用常规控制结构很不优美甚至不可能的地方。
对于结束检索函数(for terminating search functions)的工作而言,采用异常作为另一种返回方式也可以是一种很优雅的技术——特别是在高度递归的检索函数里。
然而,异常的这类应用很容易过度,并导致结构模糊的代码。在任何时候只要可能,我们还是应该坚持“异常处理就是错误处理”的观点。
14.6 如果违反了异常描述,将会调用std::unexpected(),而unexpected()的默认意义是std::terminate(),它通常将转而调用abort()。
14.6.1 并没有要求跨过编译单位的边界对异常描述进行准确的检查。
要去覆盖一个虚函数,这个函数所带的异常描述必须至少是与那个虚函数的异常描述一样受限的(显式或隐式的)。例如:
与此类似,你可以用一个指向具有更受限的异常描述的函数的指针,给一个指向带有不那么受限的异常描述的函数的指针赋值,但反之不行。例如:
void f() throw(X);
void (*pf1)() throw(X, Y) = &f; //ok
void (*pf2)() throw() = &f; //错误:f()不如pf2那么受限
特别地,不能用一个指向没有异常描述的指针,给一个指向带异常描述的函数的指针赋值。例如:
void g();
void (*pf3)() throw(X) = &g; //错误:g()不如pf3那么受限
异常描述并不是函数类型的一部分,typedef不能带有异常描述。
14.8 无论如何,应该记住那些能代替异常的东西也不是无代价的。
14.9 异常处理机制本质上就是非局部的,因此,与整体策略相符是最根本的要求。这也意味着,异常处理策略问题最好在设计的初始阶段予以考虑。它还意味着这个策略必须是简单而明晰的。
成功的容错系统只能是多层次的,每个层次在不至于导致过度扭曲的条件下,设法处理尽可能多的错误,将其余的错误留给更高的层次。
错误处理应该————尽可能地————层次化。
14.11 用异常做错误处理。
当更局部的控制机构足以应付时,不要使用异常。
尽量少用try块,用“资源申请即初始化”技术,而不是显式的处理器代码。
并非每个函数都需要处理每个可能的错误。
在构造函数里通过抛出异常指明出现失败。
避免从析构函数里抛出异常。
让main()捕捉并报告所有的异常。
在构造函数里抛出异常之前,应保证释放在此构造函数里申请的所有资源。
对于主要界面使用异常描述。
如果一个函数可能抛出某个,就应假定它一定会抛出这个异常。
库不应该单方面终止程序。相反,应该抛出异常,让调用者去做决定。
库不应该单方面生成面向最终用户的错误信息,相反,应该抛出异常,让调者者去做决定。
在设计的前期开发出一种错误处理策略。
15.2.1 最好是通过在派生类里定义新函数的方式消解歧义:
这样避免每次手工去调用基类的歧义函数。
如果不想用p->Task::debug(1)的调用方式,可以使用声明using Task::debug;
15.2.5 如果基类不应该重复,那么就需要采用虚基类,而不是常规基类。如果虚基类或者由虚基类直接派生的类是抽象类,钻研形继承将特别容易控制。
如果两个类覆盖了同一个基类函数,但它们又互不覆盖,这个类层次结构就是错的。对此将无法构造出虚函数表,因为在最终完成的对象上,对这个函数的调用将是歧义的。
15.3.1 所有这些批评(指成员变量用protected的混乱)对protected成员函数都不重要,protected是描述供派生类使用的操作的极好方式。
15.4 检查对象类型的最明显最有用的操作是一种类型转换操作,当对象具有所以期望类型时它返回一个合法的指针,如果不是就返回空指针。dynamic_cast做的就是这件事。例:
15.4.1 dynamic_cast的专长是处理那些编译器无法确定转换正确性的情况,注意,这里要求该转换能惟一确定一个对象。dynamic_cast要求一个到多态类型的指针或者引用,以便做向下强制或者交叉强制。
dynamic_cast确实能很有效地实现,所以涉及到的全部工作不过是对代表基类的type_info对象做几次比较,不需要昂贵的检索或者字符串比较。
如果对象类型已知,我们就根本不需要用dynamic_cast。
如果对指针的dynamic_cast的操作对象不具有所需要的类型,就会返回0。
如果对引用的dynamic_cast的操作对象不具有所需要的类型,就会抛出bad_cast异常。
15.4.2 dynamic_cast不能从void*出发进行强制,因为它必须去查看对象,以便确定其类型。对于这种情况就需要static_cast。例:
15.4.4 如果一个多态类型的指针或者引用的操作对象的值是0,typeid()将抛出一个bad_typeid异常。
在(type_info::)before定义的顺序关系和继承关系之间没有任何联系。
不能保证系统中对应于每个类型只存在着惟一的一个type_info对象。事实上,如果使用了动态链接库,将很难避免重复出现type_info对象的情况。因此我们总应该用==去检测type_info对象的相等,而不应该用==去比较这种对象的指针。
15.4.5 只有在必须用的时候,才应该直接去用运行时类型信息。静态的检查更安全,引起的开销更少,而且————在能用的地方————将导致结构更好的程序。
15.6 成员operator new()和operator delete()默认为static成员。因此它们没有this指针,也不会修改任何对象。
15.6.2 要构造一个对象,构造函数必须掌握所以创建的对象的确切类型。因此,构造函数不能是虚的。
但可以通过迂回方式绕过去,方法就是定义一个函数,由它调用构造函数并返回构造起来的对象。例:
派生类可以覆盖new_expr()和/或clone()去返回它们自己类型的对象:
覆盖函数的类型必须与它要去覆盖的那个函数的类型完全一样,除了返回值类型可以松动之外(为被覆盖函数返回类型的派生类)。
15.7 用virtual基类表达在类层次结构里对某些类(不是全部类)共同的东西。
在不可避免地需要漫游类层次结构的地方,使用dynamic_cast。
尽量用dynamic_cast而不是typeid。
不要声明protected数据成员。
如果某个类定义了operator delete(),它也应该有虚析构函数。(thy——因为它实现了不同于默认的析构方式,必须籍由虚析构让系统找它自身的析构。实际上是对Effective中凡有虚函数的均应定义虚析构观点的一个补充 )。
在构造和析构期间不要调用虚函数。(thy——此时的对象是不完整的对象 )
16.3.3 (vector::operator[])默认时采用不检查的方式也是为了与数组相匹配。另外,你可以在一个快速机制之上构建起一套(带检查的)安全功能,但却无法在慢速机制上构建起一套快速功能。
16.4 绝不要认为标准库比什么都好。
在定义一种新功能时,应考虑它是否能够纳入标准库所提供的框架中。
用base()从reverse_iterator抽取出iterator。
17.2.2.2 如果可以选择的话,最好是用后端操作而不是前端操作。采用后端操作写出的代码不但可以对list使用,也能对vector使用。
在完成一个工作时,只使用最小的基本操作集合通常是更明智的。
17.6.1 对于大型容器而言,hash_map能够提供比map快5到10倍的元素查找速度是很常见的,尤其是在查找速度特别重要的地方。对于大而密集使用的表示,hash_map必然具有速度优势,只要空间不紧张就应该用它。
有效散列的关键在于散列函数的质量。如果无法找到一个好的散列函数,map就很容易在性能上超过hash_map。
17.6.2.3 选出一个好的散列函数是一种艺术。不过,将关键码的表示通过异或运算放入一个整数常常是一种可接受的方式。例:
一个hash_map的实现将至少包含针对整数关键码和字符串关键码(使用字符int值产生散列)的散列函数。
特别地,如果在对象里包含指针,如果对象很大,或者如果成员的对齐要求使得表示中留下了无用空洞,在这些情况下我们都可以做得更好一些。
经验告诉我们,在选择散列函数时,做一些好的实测是必不可少的,直觉在这个领域里常常不那么管用。
17.7 在插入和删除元素时,最好是使用序列末端的操作。
18.2 就像语言特征一样,程序员应该去使用那些自己实际需要并已经理解了的算法,而且只使用它们。
18.5.1 for_each()的一种最常见用途就是序列的元素中提取信息。如果你希望积累由元素中得到的信息,那么请考虑accumulate()。如果你希望在一个序列中找出某些东西,请考虑find()和find_if()。如果你想要改变或者删除元素,请考虑replace()或者remove()。
for_each()是一个很典型的非修改性算法,因为它并不显式地修改序列。
18.12 多用算法,少用循环。
常规性地重温算法集合,看看是不是能将新应用变得更明晰。
设计时应该让使用最频繁的操作是简单而安全的。
仅在没有更特殊的算法时,才使用for_each()和tranform()。
19.5 使用未初始化的存储去改善那些扩展数据结构的算法的性能。
使用临时缓冲区去改善需要临时数据结构的算法的性能。
20.5 用string作为变量或者成员,不作为基类。
用string::npos表示“string的剩余部分”。
如果你使用string,请在某些地方捕捉length_error和out_of_range异常。
当你需要知道字符的类别时,用isalpha()、isdigit()等函数,不要自己去写对字符值的检测。
21.3.6 对于在函数里通用局部控制结构处理的问题,采用异常也很少能做得更好。
21.3.8 这种通过一个类,采用构造函数和析构函数提供公共的前缀和后缀代码的技术(哨位)在许多环境中都非常有用。(将许多函数的入口和出口代码集中到一个地方)例:
这样就可以把有关的公共代码提取出来:
21.4.1 通过显式设置和清除标志的方式控制I/O选项既显得生硬也很容易出错。用标志位的方式控制流状态,最好是作为一种实现技术来研究,而不是在界面设计时使用。
21.9 使用低级输入函数(如get()和read())主要是为了实现高级输入函数。
在控制I/O时,尽量采用操控符,少用状态标志。
用字符串流(stringstream)做内存里的格式化。
在程序的执行中第一次流I/O操作之前调用一次sync_with_stdio(bool=true),就可以保证C风格的和C++风格的I/O操作能够共享缓冲区。在第一次流I/O操作之前调用sync_with_stdio(false)将阻止缓冲区共享,这样做,在有些实现中可能改进I/O的性能。
22.4 在观察valarray的功能时,应该记住它们的意图就是作为高性能计算的一种相对低级的构件。特别要注意,valarray的基本设计准则并不是易用性,而是在极度优化的代码的基础上,更有效地利用高性能计算机。如果你的目标是灵活性和通用性而不是效率,那么最好不要去适应valarray简单、高效、精致脆弱的传统构架。
22.4.4 一个slice描述了从整数到下标的一个映射。这种映射提供了一种高效、通用和合理的方式,使我们能在一个一维数组中模拟一个二维数组。
22.4.7 这种技术所基于的想法(将U=M*V+W转换为mul_add_and_assign(&U,&M,&V,&W)而避免引进任何临时量、复制任何向量),就是通过编译时的分析和闭包对象包装,将子表达式的求值传递到一个代表组合计算的对象里。这种技术可能应用于各种各样的问题,只要它们具有共同的性质,其中需要将一些信息片段收集到一起,而后在一个函数里求值。我把这种为推迟求值而产生出来的对象称作组合闭包对象,或简称Compositor。
22.8 数值问题常常很微妙。如果你对数值问题的数学方面不是100%有把握,请去找专家或者做试验。
用numeric_limits去确定内部类型的性质。
为用户定义的标题类型描述numeric_limits。
用切割表述在数组的一部分上的操作,而不是用循环。
利用Compositor,通过清除临时量和更好的算法来获得效率。
最好是用具有特定分布的随机数类,少直接用rand()。
注意使你的随机数充分随机。
23.2 在考察这些观点时,请一定带着一种健康的怀疑态度。
就像造小船、骑自行车和编程一样,设计也不是一种只通过理论学习就能掌握的技能。
只存在一种对付复杂性的基本方法:分而治之。
各个部分的选择以及不同部分之间界面的刻画都是最需要经验和鉴赏图片的地方。
无论是对人还是对程序,分开总是容易的,更困难的部分是保证分界两边各个部分间的有效通信,而又不破坏这种分解,不抑制为协作所需要的通信。
23.3 我们不只要为当前项目的下一个版本做计划,还要考虑更长远的问题。
23.4 软件开发是一个不断重复的递进的过程。一般来说,这一过程既没有开始,也没有结束。
23.4.1 在初始设计时就将全球性作为目标,实际上就是命令这个项目永远也不能完成。开发循环之所以是一个循环,也因为取得一个能工作的系统是必不可少的,因为只有从那里才能取得经验。
23.4.2 最理想的情况是,一个概念的修改可能通过一个派生类,或者通过给某模板传递一个不同参数而完成。
23.4.3.1
步骤1:发现类——找出概念/类及其最基本的相互关系。
听听某些在系统完成后将成为专家用户的人,以及对现有的将被取代的系统不满意的人说些什么。注意他们所用的词汇
找出初始的关键性概念/类的最好工具就是黑板,对它们的初始精华的最好方法就是与应用领域的专家和一些朋友讨论。
一个用例(case)就是关于一个系统的一次特定使用的描述。
当你试图评价一组用例对系统的覆盖情况时,将它们划分为主要用例和次要用例常常很起作用。
做报告是一种最宝贵的设计工具。
高质量的报告材料并不能保证所描述的系统也有高的质量。
23.4.3.2
步骤2:描述操作——精化有关的类,描述它们的一组操作。
在选择函数时,最重要的就是集中关心它应该做什么,而不是去关心它应该怎样做。
23.4.3.3
步骤3:描述依赖性——精化有关的类,描述它们之间的依赖关系。
在设计环节中,特别应该考虑的是参数化、继承关系和使用关系。
如果某件事情现在可以在这里做,就应该在这里完成。
(在设计阶段而不是实现阶段就需要考虑继承关系和使用关系 )也意味着设计的单位应该是组件,而不是类。
23.4.3.4
步骤4:描述界面
私用函数通常不必在设计阶段考虑。
公用基类和友元也是一个类的界面的一部分。通过分别定义保护界面和公用界面,为继承和普通客户提供分离的界面,这种工作会有很好的回报。
在典型情况下,一个类里的所有操作都应该支持同一个抽象层次。若非如此,那么就应该考虑重新组织这个类及其相关的类。
23.4.3.5
因为类和概念之间的紧密关系,与类层次结构组织有关的问题常常表现为类的命名问题,或者有关设计的讨论中对类名字的使用问题。
如果你正好找不到其他人一起讨论设计,那么可以写一个有关设计中类名字使用的讲稿,这也是一种很有帮助的做法。
设计中最重要的目标之一就是提供一个界面,使之能够在变化之上保持稳定。做到这一点的最好方式是将一个被许多类所依赖的类做成抽象类,其中只给出最一般的操作。
依赖于某个类的类越多,前者就应该越一般,它所揭示的细节也应该越少。
23.4.3.6 模型的使用是无价之宝,因为任何设计都是其设计师经验的集成。
在许多情况下,只有在理解为了使一个模型适应于特定新应用而必须做的主要修改之时,才能说它是合适的。
模仿是最真挚的恭维形式,而利用模型和以前的工作作为启示则是在任何领域中开展创新性工作的一种可以接受的技术。
作为我们目标的系统越大、越雄心勃勃,有一个可以开始工作的模型也就越重要。对于一个大型系统而言,能实际接受的模型只有那些在某种意义上小一些的、与这相关的正在工作中的系统。
23.4.4 除了极少的例外,设计都是一种社会性活动,设计在报告和讨论中逐步发展。
一个“几乎是产品”的“原型”将用掉许多时间和资源,而这些时间和资源最好还是用到别的地方。
做原型是一种做试验的方式。在构造原型时最希望得到的结果是在构造它的过程中获得的洞察力,而不是原型本身。对一个原型的最重要的评判标准是,它应该是如此的不完全,以致很显然它是一个明显的试验性的媒介,不经过大范围的重新设计和重新实现就不可能转为产品。让原型“不完全”有助于将注意力集中在试验,也使原型变成产品的危险性减到最小。
在利用之后,就应该将原型丢掉。
实际上可以看到一种连续系,从数学模型,穿过牵涉到越来越多细节的模拟器,再到原型,到部分实现,直到完整的系统。(最好优化使用前者,因为它们更严格得多,而且对设计师的时间要求、对系统的资源要求都更少。 )
除了通过试验性设计提供有关各种设计选择的洞察力之外,对设计和/或实现本身的分析也是获取认识的重要源泉。
从本质上说,设计就是一种很容易出错的且难于用有效工具支持的活动。这些都使经验和反馈成为不可或缺的。
需要强调反复进行的设计和实现,以便从开发的各个不同阶段所取得的经验中获得反馈。
23.4.5 “何时测试”有一个一般性的回答:尽可能早做,尽可能经常去做。
测试策略应该作为设计和实现工作的一部分产生出来,或者至少应该与它们平行地开发。
作为一条经验规则,我要建议在时间、工作量和智力方面,分配给系统测试的资源应该多于构造其初始实现的资源。测试应当集中于那些可能造成灾难性后果的问题,以及那些可能频繁发生的问题。
23.4.7 “不成熟的优化是一切罪恶之源。”(Donald Knuth——《计算机程序设计艺术》的作者 )
处理效率问题的最佳策略就是产生出一个清晰简单的设计。只有这样的设计,才能在项目的整个生存期间,既能够作为性能调整的基础,同时又保持相对的稳定性。
优化应该是细致分析和性能实测的结果,而不能随机地摆弄代码。特别是在大型系统里,设计师或者程序员的“直觉”对于指导与性能有关的事项而言都是极不可靠的。
最重要的是避免那些在本质上就是低效的结构,避免那些需要花费过多的时间和聪明才智才可能将优化到可以接受的性能水平的结构。
23.5.1 我的经验是,使重用得以存在的必备条件是有人将它作为自己的事情。
这样一个“标准组件”小组的重要性无论怎样估计都不过分。
这种“组件小组”的成功需要依据其客户的成功情况进行评价。
说一个组件是“可重用的”,就意味着它可以在某个确定的框架里,只要求做较少工作或者无需任何工作就可以重用。
最重要的是将设计目标定位于重用,基于经验去精化组件,以及有意搜寻可能重用的现在部件而取得的结果。重用不会从漫不经心地使用某些特定语言特征或者编码技术中魔术般地冒出来。
23.5.2 应当为特定项目的开发选择适当的规范性水平。
在每个软件项目中,最关键的问题就是如何维持设计的完整性。
不让每个个人或小组都有明确的任务去维护设计的完整性是一种导向失败的方法,不让每个个人或小组在作为整体的项目上付诸努力也是一种导向失败的方法。
对于一个项目或一个组织而言,缺乏一种长远目标比缺少某种孤立性质的危害性大得多。应该有一小群人去做这种工作,形成这样一种整体目标,将这一目标牢记在心,写出关键性的整体设计文档,写出对关键性概念的介绍,并一般性地帮助其他人将这一目标牢记在心里。
23.5.3 无论个人还是小组,都应该依据其工作质量而不是粗糙的数量测试给予回报。
不要忘记管理者也是人,管理者至少需要在新技术方面受到与他们所管理的那些人同样的教育。
最成功的“老顽固”常常就是昨天的“年轻斗士”。更有经验的程序员和设计师将能够成为转变的最成功、最有远见的拥护者。他们健康的怀疑态度、有关用户的知识、对于组织性阻碍的熟识都是极其宝贵的。
23.7 用类比来证明是有意的欺骗。
保持一个特定的实实在在的目标。
不要试图用技术方式去解决社会问题。
在设计和对待人员方面都应该有长期考虑。
设计过程应鼓励反馈。
不要推广超出了所需要的、你已经有直接经验的和已经测试过的东西。
系统中也存在一些不应该用类表述的性质。
在其他领域中的分类方式未必适合作为应用中的继承模型的分类方式。(如圆和椭圆)
用现在系统作为模型、灵感的源泉和出发点。
在原型成为负担时就抛弃它。
为变化而设计,将注意力集中到灵活性、可扩展性、可移植性和重用。
将注意力集中到组件设计。
让每个界面代表在一个抽象层次中的一个概念。
通过将广泛频繁使用的界面做得最小、最一般和抽象来使设计稳定。
尽早、尽可能频繁地进行试验、分析和测试。
将目标与细节一起写进文档里。
将为新开发者提供的教学材料作为文档的一部分。
鼓励设计、库和类的重用,并给予回报。
24.2 虽然在设计一座木桥时,你个人不必是熟练的术匠,但你却需要有比本匠更多的有关木材性质的知识。类似问题也出现在为某个软件选择一种程序设计语言的时候,你需要了解几种语言,要使所设计的软件片段取得成功,你需要有对所选的实现语言的相当细节性的知识——即使你个人根本不去写有关软件中的一行代码。
24.2.1 忽视类:
与此(系统中的任何部分都不能是面向过程的观点 )相反,关键问题在于松弛程序不同部分之间的联系,以更好地反映应用中的概念。
过程风格的使用应该是有意识的决策,而不应该是默认方式。
24.2.2 忽视继承:
认为继承仅仅是一种实现细节,就是忽略了类层次结构的方式可以直接模拟应用领域中概念之间的关系。
24.2.3 忽视静态类型检查:
静态类型检查等价于插头相容性,而动态检查对应于保护电路。
静态类型检查是保证由不同小组开发的C++软件之间能相互合作的基本媒介。有关界面的文档(包括所涉及的准确类型)是分开工作的程序员小组间交流的基本手段。这些界面是设计阶段的输出中最重要的部分之一,也是设计师和程序员之间交流的汇合点。
24.2.4 忽视程序设计:
设计工具、库和框架是一种最高形式的设计和编程。为某个应用领域构造出一种有用的基于数学的模型是一种最高形式的分析。
24.2.5 排他性地使用类层次结构:
然而,并非每个概念最好就表示为某个类层次结构的一部分,而且也并非某个软件部件最好就表示为一个类层次结构。
只有通过分析揭示出的概念间的共性,或是在设计和编程中发现了结构中存在着有利于实现概念的有用共性时,才应该使用类层次结构。对于后一种情况,我们还需要仔细区分真正的共性(应该通过公用继承反应为子类型关系)或用于简化实现(反应为私用继承)。
这种思考线路带给程序的将是若干相互无关的或者弱相关的类层次结构,每个都表示了一组紧密相关的概念。它也会导致具体类的概念。这些类不在任何类层次结构里,
因为将其放入的话可能损害性能或者损害与系统其他部分之间的独立性。
为了产生效用,在作为类层次结构中一个组成部分的类里,大多数关键性操作都必须是虚函数。进一步说,这种类的大部分数据必须是保护的而不是私用的,而这也将使它容易受到来自派生类的修改的损害,这也将大大增加测试的复杂性。从设计的观点看,如果在某些地方做更严格封装确实有意义,那么就应该采用非虚函数或者私用数据。
让操作的某一个参数特殊(它用于指定“那个对象”)也有可能产生扭曲的设计。当几个参数最好是同样对待时,该操作最好就是表示为非成员函数。这并不意味着该函数就是全局的,事实上,几乎所有这种自立的函数都应该是某个名字空间的成员。
24.3 简而言之,类应该对其外部世界有一个极小化且定义良好的依赖关系,它给出了一个界面,向世界的其余部分提供了必须而又是最小量的信息。
24.3.1 并不是每个小小的细节都应该用一个单独的类表示,也不是类间的所有关系都需要表达为类继承。应该试着记住,设计的目标是在某个适当的细节层次上和在某个适当的抽象层次上去模拟一个系统。
24.3.2.1 很自然,派生类将依赖于它的基类。认识到反过来也成立的人就少多了。
为此,就必须能描述这一基类对象的预期行为,使得可以在没有任何有关派生类信息的情况下写出程序来。例如,类Shape的虚函数rotate()完成形状的旋转,在像Circle,Triangle等派生类里的rotate()就必须去旋转它们各自类型里的对象,否则,就破坏了有关Shape类的基本假设。
对于虚函数的行为期望的规范也是类设计时需要重点关注的一个问题。为类和函数选择好的名字非常重要——但却常常很不窖易。
24.3.4 从设计的观点看,私用继承等价于包容,除了(偶尔必须的)覆盖问题之外、这种方式的一个重要应用是一种技术,这仲技术让一个类从一个定义界面的抽象基类通过公用方式派生,同时让它从另一个提供实现的具体类通过私用或者保护方式派生。由于私用或保护派生所隐含的继承是一种并不反应在派生类的类型中的实现细节,这种继承有时称称为实现继承。与此相对的是公用派生,不仅基类的界面被继承,也允许到基类型的隐式转换。后一种继承有时也被称为子类型或者界面继承。
Liskov替换原理:派生类的对象应该能够用到其基类对象能够使用的所有地方。
24.3.5 通常一个设计的目标都是使界面的依赖性最小化,因为这种依赖性将变成类用户的依赖性。
24.3.6 在任何地方,只要可能,都应该去做从设计概念到程序设计语言概念间的一对一映射。一对一映射保证了简单性,保证设计真正能反应在程序里,也就使程序员和工具可以利用它。
保证在一个程序的转换图里不存在环是很重要的。如果存在环,所引起的歧义性错误将使涉及环路的类型无法组合在一定使用。例如:
以下是使用:
避免这种相互转换的一种方式就是使其中的某个(或者某些)显式化。如,将operator B()定义为make_B(),而不是作为转换运算符。或者为operator+定义许多不同版本。
24.4 有许多重要成分的表述最好不采用创建某种单一类的对象的方式。一个类层次结构表述的是一组相互有关的类型。但是,表述一个组件的各个成员的最佳方式未必就是类,也不是所有的类都具有所需要的相似性。能放人某个有意义的类层次结构里。因此,名字空间就成为在c++里组件概念的最直接最一般的体现。组件也常常被称做“类范畴”。
理想情况下,一个组件的描述包括它所使用的一组界面,再加上由它提供给用户的一组界面。除此之外的所有东西都是“实现细节”,对系统其他部分是隐藏的。这可能就是设计师的界面描述。为使它成为现实,程序员需要将它们映射到一些声明。类和类层次结构提供了各种界面,名字空间使程序员可以将界面组织起来,将所使用的界面与所提供的界面分开。
是否嵌入内部的决策依赖于设计目标以及所涉及的概念本身的一般性。嵌入内部和“非嵌入”郁是在表达设计时广泛使用的技术。默认选择应该是尽可能使类局部化,直到出现某种需要,说明应该将它做得更为广泛可用。
注意,头文件是一种强有力的机制,可以用于针对不同用户提供有关组件的不同观点,也可以用于将某些作为细节的类从用户的视线中消除掉。
24.4.1 如果所有操作对象都能放人一个类层次结构,特别是如果需要在运行时加入新运算对象类型,那么这个运算对象类型最好是表示为一个类——常常是一个虚基类。如果运算对象类型不能放人一个类层次结构里,特别是如果对运行时间有严酷的要求,那么这个操作最好用模板表示。标准容器及其支持算法就是这方面的例子。在这里需要处理各种不相关类型的运算对象,而且有运行性能的需求。所以使用的是模板。
24.4.3 因此,在运行时性能很重要的地方、要求对代码正确性有较强的保证的地片或者一般说仔在替代方式的地方,最好是避免肥人的界面。使用肥大的界面将削弱概念与类之间的对应关系,这样也就使派生沦落为仅仅用于方便实现。(——thy:给派生类展现自我的机会,正像是对待儿童 )
24.5 设计应该与编程风格相互匹配。
将类/概念作为设计中最基本的关注点,而不是功能/处理。
用户继承(仅仅)表示概念间的层次结构关系。
保持不同层次的抽象相互分离。
关注组件设计。
保证虚函数有定义良好的意义,每个覆盖函数都实现预期行为。
在表示简单包容时最好用户直接成员,不用户指向单独分配的对象的指针。
对于所有的类,定义好不变式。
显式地将前条件、后条件和其他断言表述为断言。
定义的界面应该只暴露出尽可能少的信息。
保持界面为强类型的。
利用应用层的类型来表述界面。
将界面表述得使请求可以传递给远程的服务器。
25.1 新手设计师通常最好是通过避免使用混成的东西,通过遵循某个现存组件在性质方面的风格,让新组件具有类似的所需要的性质。
25.2 典型情况下,具体类型不适合放进某个相关类的层次结构中,每个具体类型都能够独立地理解,极少需要参考其他的类。
25.2.1 具体类型很少被用于作为派生的基类。
25.3 企图去限制抽象类型的通用性,以便在速度上与具体类型媲美的做法通常是要失败的。抽象类型所允诺的就是具有互换性的实现,以及在修改之后不需要大量的重新编泽。与此类似,企图让具体类型提供“通用性”以便与抽象类型概念媲美,一般说也会失败。
为什么在可以有抽象类型的时候还要有具体类型呢?为了高效率、重用和多重界面。
25.6 类中最重要的一种就是那些最卑微、最不受重视的界面类(没有更特殊的功能,仅仅完成界面调整的类 )。
界面类一般都相当小,做的事情也相当少。只要依据不同传统写出的软件需要合作,它们就可能冒出来,因为在这里需要弥合不同规定之间的差异。例如,界面类常常被
用于为非C++代码提供C++界面。以及用于将应用代码与库的细节隔离(给使用其他库替代这个库留下可能性 )。
界面类的另一项重要应用是提供带检查的或者受限的界面。
控制对其他类的访问或调整其界面的界面类有时也被称为包装器(wrapper)。
25.7 处理这类问题(抽象类必须通过指针或引用去享受虚函数的利益、以及类对象具有固定的大小)的一种流行技术就是将一个对象分戚两个部分:一个提供用户界面的句柄部分,另一个是保存着对象状态的几乎所有信息的表示部分。句柄与表示之间的联系通常用句柄中的一个指针表示。
我们希望有非侵入式的句柄,而不是侵入式的。(如引用计数——thy )
有一个想法是为由一个基类定义出的一族类做一个句柄类。由这样的基类派生可以成为一种非常强有力的技术,这种技术除了能应用于抽象类型之外,也可以应用于结点类。
如果一个句柄类所提供的界面接近或者等同于以它作为句柄的那个类,过种句柄通常被称为代理。要引用位于远程计算机上的对象,常常需要使用这种形式的句柄。
25.8 一个框架通常都是一种结点类的层次结构,让应用程序员为深层嵌套的内部提供叶结点,这样就可以利用这个层次结构来提供应用之间的共性,提供可重用的服务。框架也将由一个库支持,这个库提供一些有用的类,使程序员可以利用它们去描述自己的动作类。
25.9 用具体类型去表示简单的独立概念。
用具体类型去表示那些最佳效率极其关键的概念。
不要从具体类派生。
用结点类去逐步扩充一个实现。
在那些能预先定义控制结构的应用领域中使用应用框架。(类似于Template Method——thy )
E.2 一个抛出了异常的操作不仅应该将它的操作对象都留在定义良好的状态中,还应该保证它已经申请的所有资源(最终)都能够释放掉。
应该记得存储并不是惟一的一种可能流失的资源。打开的文件、锁、网络连接和线程
等也都是系统资源的示例,一个函数在抛出异常之前必须释放它们,或者将它们转交给某个对象。
E.3 写出具有异常时安全性的代码的基本工具包括:
[1]try块。
[2]对于“资源申请即初始化”技术的支持。
应遵循的普遍性原则是:
[3]在我们有了能存入某信息片段的替代物之前,绝不要将原片段丢掉。
[4]在抛出和重新抛出异常时,总将对象留在合法的状态中。
按此方式,我们就总能从错误境况中全身而退。坚持这些原则的实际困难在于一些看起来无害的操作(例如<、=和sort())也可能抛出异常。要理解,应该到一个应用里的什么地方去查看需要做试验。
E.3.4 我想,安排操作顺序的方法和“资源申请既初始化”方法比广泛使用try块更有效,其根源就在于对局部控制流的简化。简单的规范化的代码更容易理解,因此也更容易做正确。
E.3.5.3 总结一下,两阶段构造的方法将导致更复杂的不变式.通常也将导致更不优美、更容易出错、更难维护的代码。因此,只要可行,就应该优先选用语言所支持的“构造函数方法”,而不是“init()函数方法”。也就是说,只要延迟资源申请并不是某个类的内在语义要求,就应该在构造函数里申请资源。
E.7 弄清楚你想要什么级别的异常时安全性。
(a.“对于所有操作的基本保证 ”:保持标准库的基本不变式,而且不会出现资源流失。
b.“对于关键操作的强保证 ”:除了提供基本保证之外,这些操作或者成功、或者毫无影响。如push_back()、对于list的单个元素insert()以及uninitiallized_copy()。
c.“某些操作的不抛出 ”:除了提供基本保证之外,有些(简单的)操作保证不会抛出异常。例如swap()、pop_back())
异常时安全性应该是整体容错策略的一部分。
不要从析构函数里抛出异常。
不要从一个遍历合法序列的迭代器里抛出异常。(要保证迭代器有效——thy )
异常时安全性涉及到仔细检查各个操作。
将模板设计为对异常透明的。(重新throw异常,否则必须列出所有异常的可能性——thy )
更应该用申请资源的构造函数方式,不要采用init()函数。
为类定义一个不变式,使什么是合法状态变得非常清晰。
确保总将对象放在合法状态中,也不要怕抛出异常。
在抛出异常之前,让所有操作对象都处于合法状态。
在可能时依靠操作的顺序,而不是显式地使用try块。
在替代物已经安全生成之前不销毁“老”信息。
依靠“资源申请即初始化”技术。