每一个函数都有 构造函数,析构函数,copy assignment操作符。控制初始化,摆脱对象时的清理工作。确保它们的行为正确非常重要。本章节提供把这些函数良好的集结起来的方式。
Rule 05 了解C++默认编写并且调用了哪些函数
当创建一个空类,经过C++处理过后,在自己没有声明的情况下编译器就会为它声明一个copy构造函数,一个copy assignment操作符和一个析构函数。若没有声明任何构造函数,也会声明一个default构造函数。所有函数都是public inline.
特别讨论copy构造函数和copy assignment operator.只有当代码合法且有适当机会证明它有意义。否则编译器拒绝生成。
eg:
template<class T> class NamedObject { NameObject(string& name, const T& name); private: string& nameValue; const T objectValue; } string newDog("Persephone"); string oldDog("Satch"); NamedObject<int> p(newDog, 2); NamedObject<int> s(oldDog, 36); p = s;这里定义不清晰。因为nameValue是一个reference.那么到底是p.nameValue指向s.nameValue.还是把p.nameVaule的值改掉。
C++不允许reference改指到别的地方。
改值后会影响其他指向改值的对象。
C++在这里拒绝编译赋值操作。
-
编译器可以暗自为class创建default构造函数,copy构造函数,copy assignment以及析构函数。
Rule 06 若不想使用编译器自动生成的函数,应该明确拒绝
书中对拒绝自动编译出函数举了例子。
每一位真正的地产中介商都会说,任何一笔资产都是天上地下独一无二,没有两笔完全相像。因此我们也认为,为HomeForSale对象做一份副本有点没道理。你怎么可以复制某些先天独一无二的东西呢?因此,你应该乐意看到HomeForSale的对象拷贝动作以失败收场.
HomeForSale hl; HomeForSale h2; HomeForSale h3(hl);//企图拷贝hl一不该通过编译 hl=h2;//企图拷贝h2一也不该通过编译拒绝拷贝:
class HomeForSale{ public: private: HomeForSale(const HomeForSale&); HomeForSale& operator=(const HomeForSale&); //只有声明 }; class Uncopyable{ protected://允许derived对象构造和析构 Uncopyable(){) }Uncopyable(){} private: Uncopyable(const Uncopyable&);//但阻止copying Uncopyable& operator=(coast Uncopyable&); }; //为求阻止HomeForSale对象被拷贝,我们唯一需要做的就是继承Uncopyable: class HomeForSale: private Uncopyable{ //class不再声明 //copy构造函数或 //copy assign.操作符 };
把函数声明为private函数并且不去实现它。
生成一个Uncopyable作为父类,把父类的相应函数设置为private。当子类调用相应操作时回去调用父类版本,但是此时父类声明为private无法实现,编译报错。
-
为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class也是一种做法。
Rule 07 为多态基类声明virtual 析构函数
考虑这种情形,Base pointer指向一个Derived class 对象。如果这个时候调用Delete。在Base Class有析构函数的情况下,调用了Base的析构函数,而子类的部分没有被析构掉。造成"局部析构"的现象。
标准:
当类中有virtual函数(基类),通常会把析构函数声明为virtual。
当类不被当做基类使用,声明析构函数为virtual是坏主意。
attention:
有些库类不是虚析构函数。在继承的时候如果转为Base指针,Delete的时候就会出问题
乍看似乎无害,但如果你在程序任意某处无意间将一个pointer-to-SpecialString。转换为一个pointer-to-string,然后将转换所得的那个string指针delete掉,你立刻被流放到“行为不明确”的恶地上:
SpecialString* pss=new SpecialString("Impending Doom"); std::string* ps; ps=pss;//未有定义!现实中*ps的Specials七ring资源会泄漏, //SpecialString=>std::string; delete ps;//因为SpecialString析构函数没被调用。
-
polymorphic(带多态性质的)base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
-
Classes的设计目的如果不是作为base classes使用,或不是为了具备多态性(polymorphically ),就不该声明virtual析构函数。
Rule 08 别让异常逃离析构函数
面对析构函数的异常通常有两种做法,一种是抛出异常。一种是abort让程序挂死。但是为了程序能够正常运行,比如析构一个数组,不能因为其中一个析构失败而终止所有的析构。
较好的方式:
首先把析构函数的内容封装到一个函数中提供给客户,交由客户处理。再增加判断,若客户不处理的情况下析构函数处理。
-
析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
-
如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
Rule 09 绝不在构造和析构过程中调用virtual函数
在调用Base构造函数时,看不到derived阶层,因为其中的许多变量未初始化,不应该下降到derived层。
但是侦测“构造函数或析构函数运行期间是否调用virtual函数”并不总是这般轻松。如果Transaction有多个构造函数,每个都需执行某些相同工作,那么避免代码重复的一个优秀做法是把共同的初始化代码(其中包括对logTransaction的调用)放进一个初始化函数如init内:
class Transaction{ public: Transaction() {init();} virtual void logTransaction()const = 0; private: void init() logTransaction(); }
-
在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)。
Rule 10 令operator= 返回一个reference to *this
主要是为了遵从内置的类型,保持行为一致。
关于赋值,有趣的是你可以把它们写成连锁形式:int x,y, z;x = y = z;同样有趣的是,赋值采用右结合律,所以上述连锁赋值被解析为:x=(Y=(z=15));
为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参。这是你为classes实现赋值操作符时应该遵循的协议.
-
令赋值(assignment)操作符返回一个reference to *this.
Rlue 11 在Operator= 中处理“自我赋值”
存在这种情况:
class Widget {};
Widget w;
w = w;
如果operator=实现版本如下:
class Bitmap{...}; class Widget private: Bitmap* pb;//指针,指向一个从heap分配而得的对象 };Widget& Widget::operator=(const Widget& rhs)//一份不安全的。perator一实现版本 { delete pb;//停止使用当前的bitrnap pb=new Bitmap(*rhs.pb);//使用rhs's bitmap的副本(复件)。 return *this;//见条款l00 }如果两个对象相同,以上代码先把pb delete,pb已经无意义然后再去取值。错误。
Widget& Widget::operator=(const Widget& rhs) { if(this == &rhs) return *this;//证同测试(identity test): //如果是自我赋值,就不做任何事。 delete pb; pb = new Bitmap(*rhs.pb); return *this; }还是没有解决new 失败的问题:
widget& Widget::operator=(const Widget& rhs) { Bitmap* pOrig = pb; //记住原先的pb pb = new Bitmap(*rhs.pb);//令pb指向*pb的一个复件(副本) delete pOrig;//删除原先的pb return *this; }
-
确保当对象自我赋值时operator一有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap.
-
确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
Rule 12 复制对象时勿忘其每一个成分
设计良好之面向对象系统(OO-system)会将对象的内部封装起来。只留两个负责函数负责对象拷贝(复制 copy构造函数 copy assignment操作符)。
当自己声明了自己的copying函数,编译器不回去检查是否复制完全。
void logCall(const std::string& funcName); class Customer{ public: //制造一个log Customer(const Customer& rhs);. Customer& operator=(const Customer& rhs); private: std::string name; }; Customer::Customer(const Customer& rhs) :name(rhs.name) {//复制rhs的数据 logCall("Customer copy constructor"); } Customer& Customer::operator=(const Customer& rhs) { logCall("Customer copy assignment operator"); name = rhs.name;//复制rhs的数据 return *this;//见条款10 }如果再添加成员变量lastTransaction:
class Date {//日期 class Customer{ public: ...//同前 private: std::string name; Date lastTransaction; }这些必须自己保证,编译器不做此检测。
当继承时,Derived copy函数必须调用父类的copy函数。
在copying函数中,两个函数的实现相近,切不可copy构造函数调用copy assignment,反之亦不可。可以尝试在封装在一个函数中,两者去调用。
-
Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。
-
不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。