原创文章,转载请注明出处:http://blog.csdn.net/sfh366958228/article/details/38845319
今天给自己订的任务是将《Effective C++》第二章看完,一口气看下来发现量并不大,这一章剩下的内容都较为简短,来看看今天的条款吧。
如同条款的字面意思,不要让析构函数中抛出异常,这样会使程序出现不明确行为。
举个例子:有一个Widget的自定义类的vector。
vector<Widget> v;
当它在呗销毁的时候,它需要销毁掉里面含有的所有Widget,如果里面有10个Widget,在析构第一个元素期间,有个异常抛出,其它九个依旧得销毁,而不是跳过。
用书上的话说就是C++不喜欢析构函数吐出异常。
书上还举了一个例子,假设你使用一个class负责数据库连接:
class DBConnection{ public: ... static DBConnection create(); void close(); };为了防止客户使用完DBConnection以后忘记调用close(),我们可以创建一个新类DBConn来管理这个对象,我们可以在这个新类的析构函数中调用该DBConnection对象的close()
class DBConn{ public: ~DBConn() { db.close(); } ... private: DBConnection& db; };我们就可以这样来用它:
{ DBConn connection(*(DBConnection.create())); ... }这里出现个问题就是如果db.close(),要是吐出异常怎么办?通常我们用以下解决方案:
DBConn::~DBConn(){ try { db.close(); } catch(...) { .... std::abort();//结束程序,可以强制"不明确行为"死掉 } }二是吞掉异常:
DBConn::~DBConn(){ try { db.close(); }catch(...) { ... } }一般而言吞掉异常不是很好的处理发式,但总比为"粗暴的结束程序"或"出现不明确行为"担负代价和风险要好,这样即使程序遭遇了一个错误或者异常情况下都可以继续运行,在另一方面提高了软件的健状性。而这些解决方案都存在一个问题:客户不能对"close失败的异常情况"做出反应,为了解决这个问题,这里我们可以将独立出来一个新的close接口:
class DBConn{ public: void close() { db.close(); isClosed = true; } ~DBConn(){ if(!isClosed){//使用以上两种解决方案之一来进行解决. } } ... private: DBConnection db; bool isClosed; };这样我们来重新审理这段代码,一个新的接口close(),提供给客户调用,如果出现异常客户可以第一时间来进行处理,如果可以确定这里不会出现异常的话,也可以不处理,客户还可以选择不调用close(),放弃对可能出现的异常处理,选择让析构函数自动调用。
总结:
1)析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
2)如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
class A { public: A(); virtual void func() const = 0; ... } A::A(){ ... func(); } class B : public A { public: virtual void fund() const; ... }如果我们要创建一个B对象,那么肯定会调用B的构造函数,但是在调用B()之前,A()一定会被优先调用,是的,派生类对象的基类部分会在派生类自身成分被构造之前先构造妥当。
总结:
在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)。
经常可以看到连锁赋值,形如:
int x, y, z; x = y = z = 15;连锁赋值其实采用的是右结合律,所以上述连锁赋值可以解析为:
x = (y = (z = 15));为了让自定义类也实现如此“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参。
Widget operator=(const Widget &rhs) { ... return *this; }
这个协议不仅适用于标准赋值形式,也包括+=,==,*=等所有赋值相关运算。
注意:这只是一个协议,没有强制性,但是所有内置类型和标准程序库提供的类型或者即将提供的类型都共同遵守。所以除非你有个标新立异的好理由,不然还是乖乖随众吧。
总结:
令赋值操作符返回一个reference to *this。
“自我赋值”发生在对象被赋值给自己时,比如:
Widget w; w = w;这看上去有点愚蠢,但它是合法的,所以一定不要认为客户不会这么做。其实还有其他形式,让人不易一眼识别出来。
a[i] = a[j]; // i = j时,潜在的自我赋值 *px = *py; // px与py恰好指向同一东西时,潜在的自我赋值我们来看一个因为“自我赋值”而导致的不安全示例吧:
Widget & Widget::operator=(const Widget &rhs) { delete pb; pb = new Bitmap(*rhs.pb); return *this; }如果this = &rhs,那么这份代码必定十有问题的,所以我们可以这么改一下:
Widget & Widget::operator=(const Widget &rhs) { if (this == &rhs) return *this; delete pb; pb = new Bitmap(*rhs.pb); return *this; }我们叫这个方法为“证同测试”,这样做是能避免“自我赋值”导致的不安全行为的,但是如果直接让operator=拥有“异常安全性”,它同时也会有“自我赋值安全性”,一石二鸟。
Widget & Widget::operator=(const Widget &rhs) { Bitmap *pOrig = pb; pb = new Bitmap(*rhs.pb); delete pb; return *this; }
当然,除了这个方法之外,还有一个确保代码不但“异常安全”而且“自我赋值安全”的方法:
Widget &Widget::operator=(const Widget &rhs) { Widget temp(rhs); swap(temp); return *this; }当然,这个代码还可以简化成下面这样:
Widget &Widget::operator=(Widget rhs) { swap(rhs); return *this; }
总结:
1)确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
2)确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
一个良好的面向对象系统会将对象的内部封装起来,只留两个函数负责对象的拷贝,对的,他们就是copy构造函数和copy assignment操作符,统称为copying函数。
编译器会为我们默认生成,你也可以自定义一个版本,但一定要细致谨慎。
一定要给所有的成员变量在copying函数中进行复制。不要可能会照成不明确行为。
如果这个类同时作为基类,切记一定要在copying函数中调用对应的基类copying构造函数。
简单点说:1)复制所有local成员变量,2)调用所有base classes内的适当copying函数。
不要试图让copy构造函数调用copy assignment,也不要试图让copy assignment调用copy构造函数。
如果两者之间有相似的代码,应该将他们提出来,放到一个新的函数里,任copy assignment和copy构造函数调用。
总结:
1)Copying函数应该确保复制“对象内的所有成员变量”及“所有base class 成分”。
2)不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。
看了下目录,第三章也是五个条款,总页数大概17页,明天争取看完五个条款。
原文地址:http://blog.csdn.net/sfh366958228/article/details/38845319