条款8: 别让异常逃离析构函数
C++并不禁止析构函数吐出异常,但不鼓励这样做。但如果你的析构函数必须执行一个动作,而该动作可能会在失败时抛出异常,该怎么办?
两个办法解决:
一是 如果抛出异常程序就结束,通常通过调用abort函数完成:
DBConn::~DBConn(){
try{
db.close();
}catch(){
std::abort();
}
}
如果程序遭遇一个于析构期间发生的错误后无法继续执行,强迫结束程序是个合理的选择,因为他可以阻止异常从析构函数传播出去。
二是 吞下异常:
DBConn::~DBConn(){
try{
db.close();
}catch(){
}
}
一般而言,将异常吞掉是个坏主意,因为它压制了某些动作失败的重要信息!然而有时候吞下一场也比负担“草率结束程序”好。
请记住:
1.析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该扑捉异常,然后吞下它们(不传播)或者结束程序。
2.如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。??????
条款9: 绝不在构造和析构过程中调用virtual函数
class Transaction{ //base class
public:
Transaction();
virtual void logTransaction() const = 0;
};
Transaction::Transaction(){
//...
logTransaction();
}
class BuyTransaction: public Transaction{
public:
virtual void logTransaction() const;
//...
};
class SellTransaction: public Transaction{
public:
virtual void logTransaction() const;
//...
};
执行以下语句:
BuyTransaction b;
基类Transaction构造函数一定会被更早调用,但其对象内的基类成分会在派生类自身成分被构造之前先构造妥当。但基类构造函数的最后一行调用virtual函数logTransaction,这时被调用的logTransaction是基类内的版本。是的,基类构造期间虚函数不会下降到派生类阶层。解决方法是:在基类内将logTransaction函数改为non-virtual,然后要求派生类构造函数传递必要信息给基类构造函数,而后那个构造函数便可以安全的调用non-virtual的logTransaction了,像这样:
class Transaction{
public:
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo) const //non-virtual function
//...
};
Transaction::Transaction(const std::string& logInfo){
//...
logTransaction(logInfo);
}
class BuyTransaction: public Transaction{
public:
BuyTransaction(parameters): Transaction(createLogString(parameters))
{
}
private:
static std::string createLogString(parameters);
};
由于无法使用virtual函数从基类向下调用,在构造期间,可以籍由令派生类将必要信息向上传递至基类构造函数的方法代替。
请记住:在构造和析构期间不要调用virtual函数,因为这类调用从不下降至派生类
条款10:令Operator=返回一个reference to *this
条款11:在operator=中处理“自我赋值”
自我赋值发生在对象被赋值给自己时.
如果遵循条款13和14的忠告,你会运用对象来管理资源,而且你可以确定所谓资源管理对象在copy发生时有正确的举措。这种情况下你的赋值操作符或许是自我赋值安全的,不需要额外操心。然而如果你尝试自行管理资源,可能会掉进“在停止使用资源之前意外释放了它”的陷阱。假设你建立一个类来保存一个指针指向一块动态分配的位图:
class Bitmap{
};
class Widget{
private:
Bitmap* pb; //point to a object allocated from heap
};
下面的operator=实现不安全:
Widget& Widget::operator=(const Widget& rhs){
delete pb; //如果此时不delete,那么在下边被赋予新值之后就再也无法delete,造成memary leak
pb = new Bitmap(*rhs.pb);
return *this;
}
问题是operator=内的*this和rhs有可能是同一对象,那么delete就不止delete了当前对象的bitmap,也销毁了rhs的bitmap。那么使用rhs的pb拷贝构造出来的新对象就无从谈起,返回的*this持有来了一个已经被销毁的指针。要阻止这类错误,需要在operator=的最前面加上一个“证同测试”:
Widget& Widget::operator=(const Widget& rhs){
if(this == &rhs){
return *this;
}
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
但这样不足以使代码具有“异常安全性”,因为new 操作可能失败,这样widget最终会持有一个指针指向一块被delete的Bitmap。解决方法是:
Widget& Widget::operator=(const Widget& rhs){
Bitmap* pOrig = pb;
pb = new Bitmap(*rhs.pb); //因为没有先delete掉pb,所以pb可以直接接受新值而不造成内存泄漏,也不需要证同测试,因为即使相同也不会造成内存泄漏
delete pOrig; //确保原始pb的内存会被delete,不造成内存泄漏
return *this;
}
请记住:
1.确保当对象自我赋值时operator=有良好的行为。其中技术包括比较“来源对象”和“目标对象”的地址,精心周到的语句顺序,以及copy-and-swap.
2.确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
条款12:复制对象时勿忘其每一个成分
如果你声明自己的拷贝函数,意思是告诉编译器你不喜欢缺省实现中的某些行为。编译器仿佛被冒犯似得,会以一种奇怪的方式回敬:当你的实现代码几乎必然出错时却不告诉你:
void logCall(const std::string& funcName);
class Customer{
public:
//...
Customer(const Customer& rhs);
Customer& operator=(const Customer& rhs);
//..
private:
std::string name;
};
Customer::Customer(const Customer& rhs):name(rhs.name){
logCall("Customrer copy constructor");
}
Customrer::Customrer::operator=(const Customrer& rhs){
logCall("Customer copy assignment operator");
name = rhs.name;
return *this;
}
当另一个新成员变量加入后,如果程序员没有把该新成员变量在拷贝赋值和拷贝构造中处理,大多数编译器不会警告。发生继承时,可能会造成此一主题最暗中肆虐的一个潜在危机:
class PriorityCustomer: public Customer{
public:
//...
PriorityCustomer(const PriorityCustomer& rhs);
PriorityCustomer& operator=(const PriorityCustomer& rhs);
//...
private:
int priority;
};
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs):priority(rhs.priority){
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer& operator=(const PriorityCustomer& rhs){
logCall("PriorityCustomer copy assignment operator");
priority = rhs.priority;
return *this;
}
以上两个实现复制了PriorityCustomer声明的成员变量,但每个PriorityCustomer还内含它所继承的Customer成员变量复件,而那些成员变量却未被复制。所以任何时候只要你承担起为派生类撰写拷贝函数的重大责任,必须很小心的也复制其基类成分。那些基类成分往往是private,所以无法直接访问,应该让派生类的拷贝函数调用积累的拷贝函数:
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs):Customer(rhs),priority(rhs.priority){
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer& operator=(const PriorityCustomer& rhs){
logCall("PriorityCustomer copy assignment operator");
Customer::operator=(rhs);
priority = rhs.priority;
return *this;
}
请记住:
1.拷贝函数应该确保复制对象内的所有成员变量及所有基类成分。
2.不要尝试某个拷贝函数实现另一个拷贝函数,应该将共同技能放进第三个函数中,并油两个拷贝函数共同调用。