第一章:让自己习惯C++
1、视C++为一个语言联邦
a、C++由四个部分组成: C、Object-Oriented C++、Template C++、 STL。
b、每一个部分有自己独立的高效编程守则
2、用const、enum、inline代替#define
a、#define是预处理器处理的部分,当编译出错时,不容易发现错误发生在哪里,其次由于预处理器盲目的替换,会导致目标码的增大
b、定义常量指针时采用如下两种方法: const char* const ptr = ”hello world“、const std::string ptr(”hello world“)
c、#define并不重视作用域,所以无法定义class中常量变量
d、对于类中常量,某些编译器不允许声明时进行初值设定,而类其他成员依赖这个常量,可以采用enum来实现
e、用inline来代替形似函数的宏
3、尽可能的使用const
a、char* const ptr、const char* ptr、char const *ptr、和const char* const ptr的含义
b、const成员函数的作用:使接口容易被理解、消除误操作的隐患、使操作const对象成为可能
c、两个成员函数如果只有常量性不同,可以被重载。
d、当const与non-const发生重复时,可以使用non-const版本调用const(使用const_cast和static_cast)
4、确定对象使用前已经初始化
a、为内置型对象进行手工初始化,因为C++不保证初始化它们;
b、构造函数最好使用成员初值列,而不是在构造函数本体内进行复制操作,初值列列出的成员变量,其排列顺序应该跟声明相同
c、以local static对象代替non-local static对象;
第二章:构造/析构/赋值运算
5、了解C++默默地编写了并调用哪些函数
a、default构造函数,复制构造函数,赋值操作符,析构函数;
b、如果是一个独立的类,上面的函数默认为public,如果是一个派生类,上面的函数与基类保持一致
c、如果类里面有引用变量,则default构造函数,赋值操作符都无效;
6、若不想编译器自动生成函数,就应该明确的拒绝
a、因为默认的上述四个函数都是public,我们可以只显示的声明他们,而不去实现他们,并且将它们定义为private; 不实现他们是因为成员函数和友元函数可以访问private;如果不实现他们,而在成员函数或友元函数调用它的时候,就会提示连接错误
b、还有一种方式就是定义一个上述这样的基类,然后不干其他的事,如果你需要其他的类来拒绝默认函数,就直接继承这个基类就可以了
7、为多态基类声明虚析构函数
a、我们知道,虚函数的作用主要体现在当用表面上是基类指针,实际上市指向派生类对象进行操作的时候,在这种情况下,当我们销毁对象时,如果不是虚函数,就会造成基类部分被释放,派生类部分不被释放的局部销毁情况。
b、带有多态性质的基类应该声明虚析构函数;如果一个类中有一个以上的虚函数,则应该声明虚析构函数。
c、类不是作为基类用,或不具备多态性,就不应该声明虚析构函数。
8、别让异常逃离析构函数
a、析构函数绝对不要吐出异常,如果一个析构函数调用的函数可能会抛出异常,那么析构函数应该吞掉它们(不传播)或是结束程序
b、如果客服需要对某个操作函数运行期间可能抛出的异常做反应,那么class应该提供一个普通的成员函数(而不是在析构函数中)执行该操作。
9、绝不在构造和析构函数中调用虚函数
a、简单的理解就是基类的构造函数在调用时派生类还没有创建,所以试图实现这种多态是不可能的,对于析构函数一样,因为派生类对象已经销毁。
10、令operator=返回一个reference *this
a、与内置内型和标准库共同遵守
11、在operator=中处理自我赋值
a、确保对象在进行自我赋值时operator=有良好的行为,具体的方法有比较来源和目标的地址,正确的语句顺序、copy-and-swap
b、确定一个函数在操作多个对象是,而多个对象实际上是同一个对象,其行为仍然正确。
12、复制对象时勿忘复制每一个成分
a、编译器会提供默认的copy构造函数和copy assignment操作符。如果我们认为默认的copy函数无法满足我们要求时,我们必须重写这两个函数,在重写这两个函数时,对于对象里面每个成分都要显示的进行复制
b、在有继承关系的类中,对于自己写的赋值构造函数,如果在派生类构造函数中只对派生类成员进行了复制的话,会导致部分复制的错误,因为这个时候派生来不会调用基类的复制构造函数进行构造,而是会调用无参数的构造函数进行构造,这样基类的成员就没有复制下来,所以在派生类中要显示的加上基类的复制构造函数。
c、不要尝试一个copying函数调用另一个copying函数,因为我们无法知道在copying函数幕后到底工作了什么,而是可以将共同的部分交由第三个函数,然后copying函数共同调用这个函数
第三章:资源管理
13、以对象管理资源
a、当我们在一个函数开始时候new出来一个类对象,在函数结束时delete这个对象,并不是总能释放掉这块资源,因为在new和delete之间可以有return和goto等控制语句,导致执行不到delete,还有就是当中间有异常发生时,正常的控制流被终止,也不会执行到delete,这样就导致了资源的泄露
b、为防止资源泄露,应该使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源
c、两个常用的RAII class分别是trl::shared_ptr和auto_ptr,注意两者的区别
14、在资源管理类中小心copying行为
a、复制RAII对象必须一并复制它所管理的资源,并以资源的copying行为决定RAII对象的copying行为
b、普通而常见的RAII class copying行为有:抑制copying、引用计数法。
15、在资源管理类中提供对原始资源的访问
a、API往往要求访问原始资源,所以每一个RAII class应该提供一个“取得其所管理的资源”的方法
b、对原始资源的访问有两种方式,显示和隐示。
16、成对使用new和delete要采用相同的形式
a、如果你在new表达式的时候采用了[],必须在相应的delete表达式加上[];如果你在new表达式的时候没有采用[],在在相应的delete表达式一定不要加上[]
b、如下代码: typedef T A[4]; T* pA = new A; 则要使用delete[];因为new A 返回一个T*指针,就像new T[4]一样。注:T表示一个对象
17、以独立语句将newed对象置入智能指针中
a、以独立语句将newed对象置入智能指针中,如果不这么做,一旦异常被抛出,可能导致难以察觉的资源泄露。
第四章:设计与声明
18、让接口容易正确使用,不易被勿用
a、好的接口应该容易使用,不易被勿用,在设计接口时应该努力达到这点
b、要保持接口的一致性,以及与内置类型的行为相同
c、阻止勿用的措施有建立新类型,限制类型上的操作,束缚对象值以及消除客户的资源管理责任
d、trl::shared_ptr支持定制性删除器
19、设计class应该考虑的问题
a、class如何被创建和销毁 ------------------- 构造函数和析构函数的写法,以及new delete操作符的运用相关问题
b、对象的初始化和对象的赋值之间的差别---------构造函数和赋值操作符的行为,以及成员初始化列表相关问题
c、对象以pass-by-value意味着什么-------------copy构造函数的实现、用来作为pass-by-value之用
d、新类的合法值-------------------------------错误检查机制
e、是否配后某个集成类图-----------------------是否为基类或是子类或不包含继承关系的类
f、新类需要什么样的转换-----------------------是否需要类型转换函数
g、什么样的操作符和函数对该类是合理的--------成员函数的依据
h、什么样的函数调用应该驳回------------------申明为private类型的依据
i、新类的成员属性-----------------------------成员变量的public、private、和protect属性
j、新类的一般性-------------------------------考虑是否写成模板类
20、用pass-by-reference-to-const替换pass-by-value
a、默认情况下,C++采用pass-by-value来进行函数参数的传递
b、pass-by-value由于有对象的赋值以及构造函数、析构函数的调用,会造成内存的增大,效率的下降,同时在将派生类作为基类函数参数传递时,会造成派生类数据截断
c、对于内置类型、STL迭代器和函数对象,一般采用pass-by-value
21、必须返回对象时,别妄想返回reference
a、如果在函数内定义对象,即在stack上创建对象,然后再返回改对象的引用,这样可以避免返回参数时候的构造开销,但问题是随着函数的执行完成,对象的生命周期结束,对象也就自动销毁了,返回的引用也就指向了一个销毁了的对象,会产生不明确行为。
b、如果在heap上创建对象,返回指针,这样一种策略会导致new 和delete分属不同的使用者,极容易导致内存泄露
c、定义静态对象,返回引用,这样一种策略也会导致隐患。比如说一个重载乘法的操作符,当执行下面语句时if(a*b == c*d)就会产生不符合预期的效果。其实对于多线程也会产生不符合预期的结果
d、采用返回对象的方式会有对象构造的开销,但相对于上述的不安全行为,这样一种开销是值得的。
22、将成员变量申明为private
a、可以对每个变量进行读写控制
b、获得更加的封装性,比如你对一个public成员变量进行改变时,会影响到所用使用该类的客服端代码,对于protect成员变量,会影响到所有派生类代码
23、以non-member-non-friend函数代替member函数
a、提供封装性,因为采用这种方式可以减少对类private成分的访问函数,从而更有利于封装
b、通过使用namespace可以降低编译依赖以及功能进行扩充
24、若所有参数皆需要类型转换,请为此采用non-member函数
a、只有当参数被列为参数列时,才有可能执行隐示类型转换
25、不抛出异常的swap函数
第五章:实现
26、尽量延后变量定义式的出现时间
a、如果一个对象过早的定义,而中途或是因为异常或是由于控制语句导致这个对象没有用到,这就会无缘无故的增大了开销
b、不仅要延后到确实需要使用对象,更要延后到对象具有初值时,这样可以避免调用default构造函数的开销。
c、同时这样还可以增加程序的可读性。
27、尽量少做转型动作
a、C++提供的四种新式转型:const_cast<T>(epr)、dynamic_cast<T>(epr)、static_cast<t>(epr)、reinterpret_cast<T>(epr)
b、尽可能的用C++类型转换,在注重效率的代码中避免使用dynamic_cast,如果一定需要,可以设计一个无转型的方案
c、尽量避免转型
28、避免返回Handle指向对象的内部成分
a、注意return-by-reference、return-by-reference-to-const的问题
b、可以尽量防止Handle管理一个空对象
30、详解Inline
a、编译器会对Inline函数进行最优化
b、 目标码和函数调用效率的问题
c、将小型、频繁调用的函数声明为Inline
31、将文件间的编译依存关系尽可能的降到最低
a、相依与声明式,而不是定义式,有两种方法:Handle class 和Interface class
b、程序库头文件应该以“完全声明式”的形式存在
第六章:继承与面向对象设计
32、确保public继承是is-a关系
a、“public 继承”应该是is-a关系,适用于base class的每一件事应该同样适用derived class,因为每一个derived class对象也是一个base class对象
33、避免遮掩继承而来的名称
a、编译器对变量名称的匹配是由内向外,一旦找出就停收索,然后再检查变量类型是否匹配正确
b、derived class内的名称会遮掩base class内的名称,在public继承的情况下就不可能满足is-a的关系了
c、为了让base class中被遮掩的名称起作用,可以采用using class或转交函数
34、区分接口继承和实现继承
a、pure-virtual只指定接口继承
b、impure-virtual指定接口继承和缺省的实现继承
c、non-virtual指定接口继承和强制性的实现继承
35、考虑virtual函数以外的选择
a、NVI手法实现Template-Method模式,优势在于可以做一些virtual函数调用之前和之后的处理,这时候重写virtual函数的意义在于某事如果被完成,而重写常态写的virtual函数表示某事何时被完成
b、用Function Pointer实现stragegy模式,优势在于即使同一类型对象也可以有不同的处理方法,缺陷在于降低了封装性。
c、由trl::function实现stragegy模式,优势在于可以提供一个泛化的调用对象。
36、绝不重新定义继承而来的non-virtual函数
a、non-virtual是静态绑定的,如果重新定义,打断了is-a的关系
37、绝不重新定义继承而来的的缺省参数值
a、所针对的是virtual函数,对于non-virtual函数不应该重新定义
b、virtual函数是动态绑定,而缺省参数值是静态绑定,
c、编译器会产生以下怪异现象:
class A{ virtual int Get(int a=1){ return a;}};
class B:public A { virtual int Get(int a=2){ return a+4;}};
A *p =new B; int c = p->Get();
会得到c=5;这是因为编译器会调用B::Get(int a= 1);
d、如果虚函数确实需要带缺省参数,base class 和derived class都要声明该借口,会导致代码重复,一旦更改也很麻烦,增加了维护的难度,解决方法可以采用NVI手法进行
38、通过复合实现“has-a”或“is-implemented-in-term-of”
a、“has-a”表示复合的对象都是其复合的一部分,也就是要用到里面的相关数据,而“is-implemented-in-term-of”并不是获得其相关数据,而是用到它的相关方法来对数据进行快捷处理。
39、使用private继承
a、private意味着“is-implemented-in-term-of”,它在软件设计层面无意义,而在软件实现层面有意义。
b、和复合一样,那什么时候使用复合,什么时候使用private继承呢,通常情况下,使用复合来实现,只有当protected成员或virtual函数牵扯进来时使用private继承
c、private继承可以造成empty base最优化
40、明智的使用多重继承
a、会造成歧义,如两个base class 有相同的函数名
b、对于环状继承而言,会生成两份base class对象,除非采用virtual继承。
c、virtual继承会增加大小,速度、初始化复杂度成本
d、多重继承有它自己的使用情形,其中之一就是“以public继承某个 interface class”和“以private继承某个协助实现的class”的两相组合。
第七章:模板和泛型编程
41、了解隐式调用接口和编译器多态
a、面向对象编程总是以显示接口和运行期多态解决问题,而泛型编程更多的是隐式接口和编译期多态
b、显示接口以函数签名式为中心,运行期多态通过virtual实现,隐式接口以有效表达式为中心,编译期多态通过template具体化和函数重载解析实现
42、typename的双重意义
a、声明template参数时,class和typename可以互换
b、编译器遇到嵌套从属名称时,会假设它不是一个类型,
b、要使用typename表示嵌套从属类型名称,但不能再base class list或member initalization list中使用typename。
43、学会处理模板化基类内的名称
a、因为存在模板特化,所以在derived class直接调用成员函数,就可能不能通过编译,因为编译器无法获知基类中是否一定有这样一个函数存在。
b、有三个解决方法:一是在base-member-function前加上this->,第二一个是使用using声明,告诉编译器假设有这样一个函数存在,第三一个是使用作用域限定符告诉编译器这个函数将被继承下来
44、将与参数无关的代码抽离template
第八章:定制new和delete
第九章:杂项
(目前还没能力弄懂这两章,呵呵)
注:需要《Effective C++》电子档同学的可以留下Email,我会第一时间发给你们