#二构造/析构/赋值运算
几乎你写的每个class都会有一或多个构造函数、一个析构函数、一个拷贝赋值操作符。如果这些函数犯错,会导致深远且令人不愉快的后果,遍及整个classes。所以确保它们行为正确是生死攸关的大事。
条款05:了解C++默默编写并调用哪些函数
如果你自己没声明,编译器就会为类声明(编译器版本的)一个拷贝构造函数,一个拷贝赋值操作符和一个析构函数。此外如果你没有声明任何构造函数,编译器也会成为你声明一个默认构造函数。所有这些函数都是public且inline。
class Empty
{
public :
Empty() { }
Empty(const Empty& rhs){ } //拷贝构造函数
~Empty() { }
Empty& operator=(const Empty& rhs){ } //赋值操作符重载
};
Empty e1;
Empty e2(e1); //拷贝构造函数
e2=e1; //copy assignment 操作符
惟有当这些函数被需要(被调用),它们才会被编译器创建出来。即有需求,编译器才会创建它们。
默认构造函数和析构函数主要是给编译器一个地方用来放置“藏身幕后”的代码,像是调用基类和非静态成员变量的构造函数和析构函数。
至于copy 构造函数和copy assignment 操作符,编译器只是将对象的每一个non-static成员变量拷贝到目标对象。
注意:编译器产生的析构函数是个non-virtual,除非这个类的基类自身声明有virtual析构函数。
如一个类声明了一个构造函数(无论有没参数),编译器就不再为它创建默认构造函数。
编译器生成的拷贝赋值操作符:对于成员变量中有指针,引用,常量类型,我们都应考虑建立自己“合适”的拷贝赋值操作符。因为指向同块内存的指针是个潜在危险,引用不可改变,常量不可改变。
请记住:
编译器可以暗自为类创建默认构造函数、拷贝构造函数、拷贝赋值操作符,以及析构函数。
条款06:若不想使用编译器自动生成的函数,就该明确拒绝
遇到问题:通常如果你不希望类支持某一特定技能,只要不说明对应函数就是了。但这个策略对拷贝构造函数和拷贝赋值操作符却不起作用。因为编译器会“自作多情”的声明它们,并在需要的时候调用它们。
由于编译器产生的函数都是public类型,因此可以将拷贝构造函数或拷贝赋值操作符声明为private。这样可以阻止人们在外部调用它,但是类中的成员函数和友元函数还是可以调用private函数。解决方法:
请记住:
·为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。使用像noncopyable这样的基类也是一种做法。
条款07:为多态基类声明virtual析构函数
前提:深入了解虚函数:
在某基类中声明为 virtual 并在一个或多个派生类中被重新定 义的成员函数,用法格式为:virtual 函数返回类型 函数名(参数表) {函数体};
作用:实现多态性,通过指向派生类对象的基类指针或引用,访问派生类中同名覆盖成员函数
举例:
#include<iostream>
using namespace std;
class A
{
public:
virtual void print(){cout<<"This is A"<<endl;}
};
class B : public A
{
public:
virtual void print(){cout<<"ThisisB"<<endl;}
};
int main()
{
//为了在以后便于区分,我这段main()代码叫做main1
A a;
B b;
A *p1 = &a;
A *p2 = &b;
p1->print();
p2->print();
return 0;
}
当有virtual 时,输出的结果就是This is A和This is B.如果没有virtual,输出结果都是This is A。
总结:基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,这个函数就是虚函数。
当派生类对象由一个基类指针删除时,而基类指针带着一个非虚析构函数,结果未有定义——基类成分会被销毁。而派生类成分未被销毁,于是造成一个诡异的“局部销毁”对象。这可是造成资源泄漏,在调试器上浪费很多时间的途径。
消除以上问题的做法很简单:给基类一个virtual析构函数。此后删除派生类对象就会如你想要的那般,它会销毁整个对象,包含所有derived class成分。
任何类只要带有virtual函数都几乎确定应该也有一个virtual析构函数。
如果一个类不含virtual函数,通常表示它并不意图被用做一个基类,当类不企图被当做基类的时候,令其析构函数为virtual往往是个馊主意。因为实现virtual函数,需要额外的开销(指向虚函数表的指针vptr)。
STL容器都不带virtual析构函数,所以最好别派生它们。
析构函数的运作方式:最深层派生的那个类其析构函数最先被调用,然后其每一个基类的析构函数被调用,所以基类的析构函数要有一个定义。
请记住:
·带有多态性质的基类应该声明一个virtual析构函数。如果一个类带有任何virtual函数,它就应该拥有一个virtual析构函数。
·一个类的设计目的不是作为基类使用,或不是为了具备多态性,就不该声明virtual析构函数。
条款08:别让异常逃离析构函数
C++并不禁止析构函数吐出异常,但它不鼓励你这样做。C++不喜欢析构函数吐出异常。在两个异常同时存在时,程序若不是结束执行就是导致不明确行为。
如果可能导致异常解决方法:
一、如果抛出异常,就结束程序。(强迫结束程序是个合理选项,毕竟它可以阻止异常从析构函数传播出去。)
DBConn::~DBConn()
{
try{ db.close();}
catch(...){
std::abort(); //aort()函数引起不正常程序终止
}
}
如果程序遭遇一个“于析构期间发生的错误”后无法继续执行,“强迫结束程序”是个合理的选项。也就是说abort可以抢先制“不明确行为”于死地。
二、捕获异常,但什么也不做。
try{ …}
catch(…){
}
如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自 析构函数以外 的某个函数。因为析构函数吐出异常就是危险。
插入一段c++异常处理的机制:
C++中处理异常的过程是这样的:在执行程序发生异常,可以不在本函数中处理,而是抛出一个错误信息,把它传递给上一级的函数来解决,上一级解决不了,再传给其上一级,由其上一级处理。如此逐级上传,直到最高一级还无法处理的话,运行系统会自动调用系统函数terminate,由它调用abort终止程序。这样的异常处理方法使得异常引发和处理机制分离,而不在同一个函数中处理。这使得底层函数只需要解决实际的任务,而不必过多考虑对异常的处理,而把异常处理的任务交给上一层函数去处理。
C++的异常处理机制有3部分组成:try(检查),throw(抛出),catch(捕获)。把需要检查的语句放在try模块中,检查语句发生错误,throw抛出异常,发出错误信息,由catch来捕获异常信息,并加以处理。一般throw抛出的异常要和catch所捕获的异常类型所匹配。异常处理的一般格式为:
try
{
被检查语句
throw 异常
}
catch(异常类型1)
{
进行异常处理的语句1
}
catch(异常类型2)
{
进行异常处理的语句2
}
...
请记住:
·析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
·如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么类应该提供一个普通函数(而非在析构函数中)执行该操作。
条款09:决不让构造和析构过程中调用virtual函数
你不该在构造函数和析构函数中调用virtual函数,因为这样的调用不会带来你想要的结果。
因为:基类的构造函数的执行要早于派生类的构造函数,当基类的构造函数执行时,派生类的成员变量尚未初始化。派生类的成员变量没初始化,即为指向虚函数表的指针vptr没被初始化又怎么去调用派生类的virtual函数呢?
所以派生类定义对象,会先调用基类的构造函数,也会先调用基类的虚函数。 析构函数也相同,派生类先于基类被析构,又如何去找派生类相应的虚函数?
唯一好的做法是:确定你的构造函数和析构函数都没有调用虚函数,而它们调用的所有函数也不应该调用虚函数。
解决的方法可能是:既然你无法使用虚函数从基类向下调用,那么我们可以使派生类将必要的构造信息向上传递至基类构造函数。即在派生类的构造函数的成员初始化列表中显示调用相应基类构造函数,并传入所需传递信息。
举例:
#include <iostream>
using namespace std;
class example
{
public:
example()
{
output();
}
virtual void output()
{
cout<<"The construct can call virtual function!"<<endl;
}
};
class exam:public example
{
public:
virtual void output()
{
cout<<"The second one!"<<endl;
}
};
int main()
{
example *newone = new exam;
return 1;
}
这段代码中最后的输出为:The construct can call virtual function!
原因上面已经分析。
请记住:
在构造和析构函数期间不要调用虚函数,因为这类调用从不下降至派生类。
条款10:令operator= 返回一个reference to *this
对于赋值操作符,我们常常要达到这种类似效果,即连续赋值:
int x, y, z;
x = y = z = 15;
为了实现“连锁赋值”,赋值操作符必须返回一个“引用”指向操作符的左侧实参,为接下来的赋值。
即:
class WIdget{
public:
Widget & operator = (const Widget &rhs) //返回类型是个引用,指向当前对象
{
...
return *this; //返回左侧对象
}
...
};
条款适用于 +=,-=, *= 等等
所有内置类型和标准程序库提供的类型如string,vector,complex或即将提供的类型共同遵守。
请记住:
·令赋值操作符返回一个reference to *this。
条款11:在operator =中处理“自我赋值”
先举几个自我赋值的例子:
例:Widget w;
w = w;
a[i] = a[j]; //i == j or i != j
*px = *py;// px,py指向同个地址;
以上情况都是对“值”的赋值,但我们涉及对“指针”和“引用”进行赋值操作的时候,才是我们真正要考虑的问题了。
看下面的例子:
Widget& Widget::operator=(const Widget& rhs)
{
delete pb; //这里对pb指向内存对象进行delete,试想 *this == rhs?情况会如何
pb = new Bitmap(*rhs.pb); //如果*this == rhs,那么这里还能new吗?“大事不妙”。
return *this;
}
也许以下代码能解决以上问题:
Widget& Widget::operator=(const Widget& rhs)
{
if (this == &rhs)
return *this; //解决了自我赋值的问题。
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
“许多时候一群精心安排的语句就可以导出异常安全(以及自我赋值安全)的代码。”,以上代码同样存在异常安全问题。
解决方法:
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap *pOrig = pb; //记住原先的pb
pb = new Bitmap(*rhs.pb); //令pb指向*pb的一个复本
delete pOrig; //删除原先的pb
return *this; //这样既解决了自我赋值,又解决了异常安全问题。自我赋值,将pb所指对象换了个存储地址。
}
请记住:
·确保当对象自我赋值时operator =有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
·确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
条款12:复制对象时勿忘每一个成分
如果你为class新添加一个成员变量,你必须同时修改copying函数,同时也需要修改所有构造函数(条款4和45)以及任何非标准形式的operator=(条款10)。
拷贝构造函数形式:
类名(类名&对象名)//拷贝构造函数的声明/原型
任何时候只要你承担起“为derived class撰写copying函数”的重大责任,必须很小心地也复制其base class成分。那些成分往往是private,你无法直接访问它们,而应该让derived class的copying函数调用相应的base class函数。
class Customer {
};
class PriorityCustomer : public Customer
{
PriorityCustomer(const PriorityCustomer & rhs);
PriorityCustomer& operator=(const PriorityCustomer &rhs);
private:
int priority;
};
PriorityCustomer::PriorityCustomer(const PriorityCustomer & rhs)
: Customer(rhs), priority(rhs.priority) //本拷贝构造函数指定了实参传给基类构造函数,调用base class的copy构造函数 Customer(rhs)
{
}
PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer &rhs)
{
Customer::operator=(rhs); //调用base class的operator=
priority = rhs.priority;
return *this;
}
当编写拷贝构造函数,请确保(1)赋值所有的local成员和变量,(2)调用所有的基类的适当的复制构造函数
尽管copying函数有相似地方,但你也不该令copy assignment操作符调用copy构造函数,copy构造函数调用copy assignment操作符同样无意义。
如果你发现你的copy构造函数和copy assignment操作符有相似的代码,消除重复的最好做法是:建立一个新的private成员函数,供二者调用。
请记住:
(1)copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。
(2)不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。