local static对象:函数内的static对象。因为它们对函数而言是local。
non-local static对象:其他static对象。一般指global 对象、定义于namespace作用域内的对象、在class内、以及在file作用域内被声明为static的对象。(所谓static对象,其寿命从被构造出来直到程序结束为止。其使用有作用域限定)
类的构造函数被声明为explicit,这可阻止它们被用来执行隐式类型转换(implicit type conversions),但它们仍可被用来进行显示类型转换(explicit type conversions)
lhs代表“left-hand side”左手端,rhs代表“right-hand side”右手端,即分别是左操作数和右操作数。
将“指向一个T型对象”的指针命名为pt,意思是“pointer to T”。比如 Widget* pw; //pw="ptr to Widget"
class Airplane; Airplane* pa; //pa="ptr to Airplane"
对于references使用类似的习惯:rw可能是个 reference to Widget,ra则是个reference to Airplane.
当讨论成员函数时,偶尔会以mf为名。
常量定义是通常被放在头文件内(以便被不同的源码含入);
inline函数被放在头文件内。
如果你不想让别人获得一个pointer或reference指向你的某个整数常量,enum可以帮助你事先这个约束。
对于单纯常量,最好以const对象或enums替换#defines。
对于形式函数的宏(macros),最好改用inline函数替换#defines。
“左被指,右自身”:如果关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量。迭代器与之相反。
两个成员函数如果只是常量性(constness)不同,可以被重载。
利用C++的一个与const相关的摆动场:mutable(可变的),可以解决常成员函数会改变non-static成员变量的问题。mutable释放掉non-static成员变量的bitwise constness约束。
永远在使用对象之前先将它初始化。总是使用成员初始化列表。这样做有时候绝对必要,且又往往比赋值更高效。
对于无任何成员的内置类型,你必须手工完成此事。至于内置类型以外的任何其他东西,初始化责任落在构造函数(constructors)身上。
C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。因此default构造函数实现初始化成员变量的操作,应该采用member initialization list(成员初始化列表),而不是在函数体内执行assignment操作。这样效率会更高。
为内置型对象进行手工初始化,因为C++不保证初始化它们。
构造函数最好使用成员初始化列表(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初始化列表列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
为免除“跨编译单元之初始化次序”的问题,请以local static对象替换non-local static对象。
条款06:“若不想使用编译器自动生成的函数,就该明确拒绝”——————策略是:所有编译器产出的函数都是public的。为了阻止这些函数被创建出来,你可以自行声明它们。通过明确声明一个成员函数,你阻止了编译器暗自创建其专属版本;而令这些函数为private而且故意不去定义它们,使得你得以成功阻止人们调用它。
具体原因:通过将其声明为private,可以将执行copy constructor或assignment操作的错误从连接期移至编译期。但不是在该类本身,而是在一个专门为了阻止这些函数而设计的base class内。同时,不定义它们,让member函数和friend函数调用它们时,会获得一个连接错误(likage error)。---------从而实现了禁用赋值构造函数和赋值操作!
应用实例:类的每个实体都是独一无二的,不可能有两个相同的实体。文中的例子是 待售的房子HomeForSale类,函数指赋值构造函数和赋值函数。
问题:当derived class 对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义———实际执行时通常发生的是对象的derived成分没被销毁。即“经由base class接口处置derived class对象”的情形。
解决办法:给base class一个virtual析构函数。此后删除derived class对象就会是你想要的。它会删除整个对象,包括所有derived class成分。
注:
在派生类的构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class。——————按照构造函数中变量属性的初始化顺序,应该是先构造基类的成员变量,然后再顺序构造派生类的成员变量;析构函数的析构顺序相反。因此,在派生类的构造函数执行期间,只调用基类的virtual函数。
条款10:令 operator= 返回一个reference to *this。
理由是:为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参。这样,返回值也必须是引用了。
int x,y,z;
x=y=z=13; //赋值连锁形式
如果你发现你的copy构造函数和copy assignment操作符有相近的代码,消除重复代码的做法是,建立一个新的成员函数给两者调用。这样的函数往往是private,而且常被命名为init。
原因:避免new出来的内存,因为过早返回或出现异常 而 没有delete的情况。
把资源放进对象内,我们便可倚赖C++的“析构函数自动调用机制”确保资源被释放。
“资源取得时机便是初始化时机”(RAII):我们总是在获得一笔资源后于同一语句内以它初始化某个管理对象。有时候获得的资源被拿来赋值(而非初始化)某个对象。由此,可以实现在构造函数中获得资源并在析构函数中释放资源。
注意:
auto_ptr和tr1::shared_ptr两者都在其析构函数内做delete而不是delete[]动作。因此,在动态分配而得的array身上使用auto_ptr或tr1::shared_ptr是个馊主意。
Boost库中的boost::scoped_array 和 boost::shared_array_classes提供了针对动态分配数组的内存管理行为。
deep copying行为:某些标准字符串类型是由“指向heap内存”之指针构成(那内存被用来存放字符串的组成字符)。这种字符串对象内含一个指针指向一块heap内存。当这样一个字符串对象被复制,无论指针或其所指内存都会被制作出一个复检。
1. 是否该提供一个explicit转换函数(例如shared_ptr中的get成员函数)将RAII class转换为其底部资源,或是应该提供implicit转换?
答案主要取决于RAII class被设计执行的特定工作,以及它被使用的情况。通常是显示转换函数如get是更安全的,因为它将“非故意之类型转换”的可能性最小化了。从而很好的防止了内存资源泄露
2.APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个“取得其所管理之资源”的办法。
考虑下面这个typedef:
typedef std::string AddressLines[4]; //每个人的地址有4行,每行是一个string
由于AddressLines是个数组,如果这样使用new:
std::string* pal=new AddressLines; //注意,"new AddressLines"返回一个string*,就像“new string[4]”一样
那就匹配“数组形式”的delete:
delete pal; //行为未有定义
delete []pal; //很好
条款17:以独立语句将newed对象置于智能指针
资源被创建 —— 经由 "new Widget" 创建资源
资源转换为资源管理对象 —— 使用智能指针来管理被创建的资源
问题场景:
在 “资源被创建” 和 “资源被转换为资源管理对象” 两个时间点之间有可能发生异常干扰。 即,在使用 以智能指针作为形参之一的函数时,C++不是以特定次序完成函数参数的核算的,也就是:资源被创建 和 资源被转换为资源管理对象 两个动作发生的时间不连续,期间可能会发生另外的实参核算,但是它们的先后顺序是一定的。因此,可能在 发生另外的实参核算的过程中发生异常,从而导致了heap分配出来的内存被遗失,导致内存资源泄露。
因此,需要以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露。
问题1:绝不要返回pointer 或 reference 指向一个 local stack 对象;
问题2:返回reference 指向一个 heap-allocated 对象(new出来的)。比如在operator重载的连续多层调用时,容易造成资源泄露。实例,重载 operator* ,函数返回值是new出来的heap-allocated对象,出错代码:w=x*y*z。
问题3:返回 pointer 或 reference 指向一个 local static 对象。因为有可能一行代码同时返回多个这样的对象,然而local static对象只有一个。由于返回值都是reference,导致调用端看到的用于都是 static对象的“现值”,就造成不期望的结果。常见场景是两个对象进行比较,比如: bool operator== (const Rational& lhs, const Rational& rhs)。
注意:条款4已经为“在单线程环境中合理返回reference指向一个local static对象”提供了一份设计实例。
条款4: 明确对象被使用前已先被初始化。
需要明白的是:读取未初始化的值会导致不明确的行为。在某些平台上,仅仅只是读取未初始化的值,就可能让你的程序终止运行。更可能的情况是读入一些“半随机”bits,污染了正在进行读取动作的那个对象,最终导致不可测知的程序行为,以及许多令人不愉快的调试过程。
最佳处理办法就是:永远在使用对象之前先将它初始化。如果是非内置型对象,总是使用 成员初始化列表。
1)对于C++内置型对象,需要进行手工初始化,因为C++不保证初始化它们。实例:int x=0;
2)对于内置类型以外的任何其他东西,初始化责任落在构造函数(constructors)身上。规则很简单:确保每一个构造函数都将对象的每一个成员初始化,并且初始化的顺序应该和它们在class中的声明次序相同。值得注意的是:C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。因此,构造函数应该使用 成员初始化列表(member initialization list) 达到初始化的目的,而不是赋值操作(assignment)。甚至当你想要default构造一个成员变量,你都可以使用 成员初始化列表,只要指定无物作为初始化实参即可(前提是各成员变量都有自己的默认构造函数)。
3)为免除“跨编译单元之初始化次序”问题,请以local static 对象替换non-local static对象。注:这里就有一份“为单线程环境中合理返回reference指向一个local static对象”的设计实例。
被广泛使用的classes是最需要封装的一个族群,因为它们最能够从“改采用一个较佳实现版本”中获益。(这里的“封装”指的是将成员变量声明为private)原因是,某些东西的封装性与“当其内容改变时可能造成的代码破坏量”成反比。也就是类中的成员变量封装得越好,当你想取消这个变量的时候,需要改变的代码量越少,尤其是对于被广泛使用的基类。
成员变量应该是private,因为如果它们不是,就有无限量的函数可以访问它们,它们也就毫无封装性。
切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
面向对象守则要求数据应该尽可能被封装。如果某些东西被封装,它就不再可见。越多东西被封装,越少人可以看到它,我们就有越大的弹性去改变它,因为我们的改变仅仅直接影响看到改变的那些人和事物。因此,他使我们能够改变事物而只影响有限客户。
我们可以通过计算能够访问该数据的函数数量,来量测数据的封装性。越多函数可访问它,数据的封装性就越低。
因此,如果对于同一个功能函数,如果它可以被声明为member函数或一个non-member,non-friend函数,那么我们应该选择后者,因为它保证了数据的封装性。C++中,比较自然的做法是让该函数声明为一个non-member函数,并且与对应类位于相同namespace内。 这里需要注意的是,这个功能函数可以被声明为另一个类的member函数。
namespace与classes不同,前者可以跨越多个源码文件,而后者不能。即在不同头文件可以定义相同的namespace,只要它的内部成员命名没有冲突。典型实例就是C++标准程序库的组织方式,它有数十个头文件,每个头文件都声明了std的某些机能。这意味着客户可以轻松扩展这一组便利函数,他们需要做的就是添加更多non-member non-friend函数到此命名空间内,新函数就像其他旧有的便利函数那样可用且整合为一体。
降低编译依存性!!!条款31
总结:宁可拿non-member non-friend函数替换member函数。这样做可以增加封装性、包裹弹性(packaging flexibility,即函数内部有更大的弹性做相关工作)和机能扩充性(函数会被声明在相同的namespace内)。