条款 5:了解c++默默编写并调用了哪些函数
当你定义一个类,如果自己 没有声明,那么编译器会自动声明拷贝构造函数、复制操作符、析构函数和默认构造函数,并且都是inline和public。但是值得注意的是,所有编译都会自动生成这些函数,这些都只是c++语法的要求,然而在实际开发中,编译器会进行优化,并没有生成拷贝构造函数和默认构造函数,只有四种情况下才生成:默认构造函数和拷贝构造函数
所以必须记住:
1、编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符和析构函数
条款 6:若不想使用编译器自动生成的函数,应该明确拒绝
有时候出于对实际的考虑,可能并不需要某些函数,如拷贝构造函数和赋值操作符。但是如果不声明它们,编译器也会自动声明他们。解决问题的关键之处在于所有编译器产生的函数都是public。为了阻止他们的产生,我们必须自行声明,为了防止用户代码调用,可以将其声明为private。这样借由明确声明一个成员变量,不仅阻止了编译器暗自创建其专属版本,也阻止了人们的调用。但是一般而言这样做仍然是不够安全的,因为member和friend函数还是可以调用private函数的。解决方法就是不去定义它们,那么如果有人不慎调用任何一个,会获得一个连接错误。所以将成员函数声明为private并且故意 不去实现它们广泛地被人所接受。另外一个通过类继承的方式可以来拒绝使用某些成员函数。
class Uncopyable{
protected:
Uncopyable(){} //允许derived对象构造和析构
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&); //但是阻止copying
Uncopyable& operator=(const Uncopyable&);
};
class HomeForSale : private Uncopyable{
...
};
这样任何人甚至是member和friend函数尝试拷贝HomeForSale对象都会失败,因为编译器会为HomeForSale生成拷贝构造函数和复制操作符,这些函数会尝试调用其base class的对应的函数,因为在base class中这两个函数是private,所以会被编译器拒绝。关于private继承,其实继承标识符并不影响子类对于父类成员的访问,影响的是用户代码对父类成员的访问。所以这里即使是public继承也无妨。
所以必须记住:
1、为驳回编译器暗自提供的机能,1)可将成员函数声明为private并且不予实现;2)像Uncopyable这样基类的方法
条款 7:为多态基类声明virtual析构函数
如果基类指针指向子类对象,而base class带着一个non-virtual析构函数,实际执行时通常发生的是对象的derived成分没有被销毁。于是造成“局部销毁”对象,形成资源泄漏。解决这个问题的办法就是给base class一个virtual析构函数。因此任何class只要带有virtual函数机会可以确定应该也有一个virtual析构函数。如果class不含有virtual函数,通常表示它并不意图被用作一个base class,当一个class不企图充当base class时,令其析构函数为virtual往往是馊主意。即时class完全不带virtual函数,被“non-virtual”咬伤还是有可能的。比如说标准string不含有virtual函数,但是有时候程序员会错误地把它当作case class,如下:
class SpecialString : public std::string{ //馊主意!std::string有non-virtual析构函数
...
};
SpecialString* pss = new SpecialString("Impending Doom");
std::string* ps;
...
ps = pss;
...
delete ps;//未定义!现实中*ps的SpecialString资源泄漏,因为SpecialString析构函数没有被调用
相同的分析同样适用于任何不带virtual析构函数的class,包括STL容器如vector、list、set等。“给base class一个virtual析构函数”,这个规则只适用于polymorphic(多态性)的base class身上。并非所有的base class的设计目的是为了多态的用途,如标准的string和STL容器都不被设计作为base class,更别说多态了。因此它们不需要virtual析构函数。
所以必须记住:
1、polymorphic(多态)的base class应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数
2、classes的设计目的如果不是作为base class使用,或不是为了具备多态性,就不该声明为virtual析构函数
条款 8:别让异常逃离析构函数c++并不禁止析构函数吐出异常,但不鼓励你那样做。只要析构函数吐出异常,程序可能过早结束或出现不明确行为。参考至“不能在析构函数中抛出异常” “C++异常处理模型最大的特点和优势就是对C++中的面向对象提供了最强大的无缝支持。好的,既然如此!那么如果对象在运行期间出现了异常,C++异常处理模型有责任清除那些由于出现异常所导致的已经失效了的对象(也即对象超出了它原来的作用域),并释放对象原来所分配的资源,这就是调用这些对象的析构函数来完成释放资源的任务,所以从这个意义上说,析构函数已经变成了异常处理的一部分。不知大家是否明白了这段话所蕴含的真正内在涵义没有,那就是上面的论述C++异常处理模型它其实是有一个前提假设——析构函数中是不应该再有异常抛出的。试想!如果对象出了异常,现在异常处理模块为了维护系统对象数据的一致性,避免资源泄漏,有责任释放这个对象的资源,调用对象的析构函数,可现在假如析构过程又再出现异常,那么请问由谁来保证这个对象的资源释放呢?而且这新出现的异常又由谁来处理呢?不要忘记前面的一个异常目前都还没有处理结束,因此这就陷入了一个矛盾之中,或者说无限的递归嵌套之中。所以C++标准就做出了这种假设,当然这种假设也是完全合理的,在对象的构造过程中,或许由于系统资源有限而致使对象需要的资源无法得到满足,从而导致异常的出现,但析构函数完全是可以做得到避免异常的发生,毕竟你是在释放资源呀!”为了避免这一问题,1)可以在析构函数内部处理异常;2)在析构函数中吞下异常;3)一个较佳的策略是重新上设计接口,使其客户可以有机会对可能出现的问题做出反应。例如DBConn自己可以提供一个close函数,因而赋予客户一个机会处理“因该操作而发生的异常”。如果客户不认为这个我机会有用,可以忽略,依赖析构函数去调用close。
class DBConn{
public:
...
void close() //供客户使用的新函数
{
db.close();
closed = ture;
}
~DBConn()
{
if(!closed)
{
try{
db.close(); //如果客户没有关闭,关闭连接
}
catch(...){}
}
}
private:
DBConnection db;
bool closed;
};
所以必须记住:
1、析构函数中绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序
2、如果客户需要对某个操作函数运行期间抛出的异常进行反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作
条款 9: 绝不要在构造函数和析构函数中调用virtual函数base class在构造函数期间virtual函数绝不会下降到derived class阶层。取而代之的是,对象的作为就像隶属于base类型一样。或者可以说,在base class构造期间,virtual函数不是virtual函数。由于base class构造函数的执行更早于derived class构造函数,当base class构造函数执行时,derived class的成员变量尚未初始化。如果期间调用virtual函数下降至derived class阶层,必然导致不明确行为和彻夜调试的后果,所以c++会禁止你走这条路。其实更根本的原因是:在derived class对象的base class构造期间,对象类型是base class而不是derived class。不只virtual函数被编译器解析至base class,运行期类型信息也会将对象视为base class。对象derived class构造函数执行之前不会成为一个derived class。相同的道理同样适用于析构函数。一旦derived class析构函数开始执行,对象内的derived class成员变量便呈现未定义值,所以c++视它们仿佛不存在。进入base class析构函数后对象称为一个base class对象。
一种很好的理解方法就是,派生类部分必须在基类部分构造完全之后才会去构造,因此在虚表中尚未注册派生类的VirtualFunction(),这时只能调用基类的VirtualFunction()。对于析构函数,同样是如此,派生类部分先析构,这时基类中的虚函数将无法定位到派生类,只能调用基类自身的函数。书上指出“在base class构造期间,virtual函数不是virtual函数”。这样的结果会使读者感到困惑,与多态法则的效果不一致,所以本书作者强调“绝不在构造和析构函数中调用virtual函数”。
所以必须记住:
1、在构造函数和析构函数期间不要调用virtual函数,因为这类调用不会下降至derived class。
条款 10:令operator=返回一个reference to *this
假设1:返回void,虽然这对于普通的复制来说是可行的,但是对于符合c++语法规定的连锁赋值方式就会出现问题;假设2:返回右对象(以string为例:const string& operator=(const string& rhs)),但是对于(x=y)=z=12的情况,无法对(x=y)进行赋值,因为返回常量对象;那如果改成string& operator=(const string& rhs),代码中return rhs,则编译不会通过;那如果改成string& operator=(string& rhs),则对于str=“hello”编译通不过,因为“hello”会产生临时的常对象,而operator参数是string类型而不是const string;
假设3:返回左对象,如果返回对象而不是引用:参考
class1 A("herengnag");
class1 B;
B=A;
看似简单的赋值操作,其所有的过程如下:
1 释放对象原来的堆资源
2 重新申请堆空间
3 拷贝源的值到对象的堆空间的值
4 创建临时对象(调用临时对象拷贝构造函数),将临时对象返回
5 临时对象结束,调用临时对象析构函数,释放临时对象堆内存
但是,在这些步骤里面,如果第4步,我们没有overload 拷贝函数,也就是没有进行深拷贝。那么在进行第5步释放临时对象的heap 空间时,将释放掉的是和目标对象同一块的heap空间。这样当目标对象B作用域结束调用析构函数时,就会产生错误!!因此,如果赋值运算符返回的是类对象本身,那么一定要overload 类的拷贝函数(进行深拷贝)!
返回左对象引用:
1 释放掉原来对象所占有的堆空间
1.申请一块新的堆内存
2 将源对象的堆内存的值copy给新的堆内存
3 返回源对象的引用
4 结束。
因此,如果赋值运算符返回的是对象引用,那么其不会调用类的拷贝构造函数,这是问题的关键所在!!
以上讨论不仅适用于标准赋值形式,同样也适用于所有赋值相关运算,例如+=、*=、-=等。
所以必须记住:
1、令赋值操作符返回一个reference to *this。
条款 11:在operator=中处理“自我赋值”
Widget& Widget::operator=(const Widget& rhs) //不安全的operator=实现版本
{
delete pb; //停止使用当前bitmap
pb = new Bitmap(*rhs.pb); //使用rhs Bitmap的副本
return *this; //条款10
}
这里存在自我赋值问题,*this和rhs有可能是同一个对象。delete不只是销毁当权对象的bitmap,也销毁了rhs的bitmap。传统的做法就是在前面加一个“证同测试”:
</pre><pre name="code" class="cpp">Widget& Widget::operator=(const Widget& rhs)
{
if(*this == rhs) return *this;//如果自我赋值不做任何事
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
虽然新版本的operator=“自我赋值”安全,但是不具备异常安全性。更明确的是,如果new操作出现异常,那么pb始终会指向一块被删除了Bitmap。值得庆幸的是,往往在让operator=获得“异常安全性”的同时也具备了“自我赋值安全性”,所以越来越多的人把焦点放在“异常安全性”。例如一下代码,我们只需要注意在复制pb所指的东西之前不要删除pb:
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap pOrig = pb;
pb = new Bitmap(*rhs.pb);
delete pOrig;
return *this;
}
现在,如果new抛出异常,pb保持原状。另外,即使没有证同测试,也实现了自我赋值的安全性,只是不是最高效率的方法。
在operator=函数内手工调整代码语句顺序达到异常和自我赋值的安全性的替代方法就是使用所谓的copy and swap技术:
Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs); //为rhs数据制作一份复件
swap(temp); //将*this数据和上述复件的数据交换
return *this;
}
请记住:
1、确保当对象自我赋值时operator=有良好的行为,其中技术包括比较“来源对象”和“目的对象”的地址比较,精心周到的语句顺序,copy-and-swap;
2、确定任何一个函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
条款 12:复制对象时勿忘其每一个部分
当你编写一个copying函数(拷贝构造和赋值构造函数)时,确保(1)复制所有的local成员变量;(2)调用所有的base内适当的copying函数。注意,通常这两个copying函数往往有近似的实现本体,这可能会诱使你让某个函数调用另一个函数避免重复,但是千万不要这么做。可以建立一个新的函数供二者调用,往往是private并且命名为init。
赋值操作符调用拷贝构造函数是不合理的,就像试图构造一个已经存在的对象。反之亦然,赋值操作符只作用于已初始化对象,而构造函数来初始化新对象。
请记住:
1、Copying函数应当确保复制对象内所有成员变量以及所有base class成员;
2、不要尝试以某个copying函数实现另一个copying函数。应该将共同机制放进第三个函数中,并有二者copying函数共同调用。