构造/析构/赋值运算
Constructors,Destructors,and Assignment Operators
条款05:了解C++默默编写并调用哪些函数
Know what functions C++ silently writes and calls.
当我们写下一个empty class时,不包含任何我们声明的constructors,destructors,以及copy assignment操作符时,就像这样:
class Empty {};
看起来什么都没做,其实编译器已经帮我们体贴地做了不少。就像这样:
class Empty
{
public:
Empty() {...} //default构造函数
Empty(const Empty& rhs) {...} //copy构造函数
~Empty() {...} //析构函数
Empty& operator=(const Empty& rhs) {...} //copy assignment 操作符
};
default构造函数和析构函数主要是给编译器一个地方用来放置“藏身幕后”的代码,像是调用base classes和non-static成员变量的构造函数和析构函数。而且,编译器默认生成的destructors是个non-virtual,除非这个class的基类(base class)自身声明有virtual destructors。
当然了,如果我们主动地去声明了constructors,destructors,以及copy assignment操作符,那么编译器就会带领它的default构造函数、copy构造函数、析构函数和copy assignment操作符退居二线,也就没必要再去自动创建,来遮蔽我们主动声明的版本了。
☆编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。
条款06:若不想使用编译器自动生成的函数,就该明确拒绝
Explicit disallow the use of compiler-generated functions you do not want.
书上有个例子:地产商中介卖的是房子,一个中介软件系统自然而然想有个class来描述待售房屋。而房屋是独一无二的。就像这样~
class HomeForSale {...}
为这房屋去创建一个副本显然不合适。也就是说,各种蓄谋对此房屋进行copy的行为,都应该以失败收场,理想情况应该是这样:
HomeForSale h1;
HomeForSale h2;
HomeForSale h3(h1); //企图复制h1----不该通过编译
h2=h1; //还是企图复制h1----也不该通过编译
然而。理想很丰满,现实很骨感。依据上边所说的条款5,就算我们不去声明,只要有人想去调用它们,编译器还是会去帮这个忙。再说清楚些,如果不声明copy构造函数或copy assignment操作符,编译器会帮忙生成一份默认的;如果声明了,所写的class还是支持copying。而在这个例子的目标,是去阻止copying!
解决之道很简单:将copy构造函数和copy assignment操作符声明在private中并且故意不实现。也就是“将成员函数声明为private而且故意不实现它们”的伎俩。就算member函数和friend函数还是能调用private函数,也会发生连接错误(linkage error)。就像这样:
class HomeForSale
{
public:
...
private:
...
HomeForSale(const HomeForSale&); //只有声明
HomeForSale& operator=(const HomeForSale&);
};
不过有一种更好的方法,就是把连接期错误移到编译期,越早侦测到错误越好。只要任何人——甚至member函数或friend函数去尝试复制HomeForSale对象,编译器就会试着生成一个copy构造函数和一个copy assignment操作符,而编译器生成版又会去尝试调用其base class的对应兄弟,最后只会被拒绝。因为base class的copy函数不是public或protected,而是private。
class Uncopyable
{
protected: //允许derived对象构造或析构
Uncopyable() {}
~Uncopyable() {}
private:
Uncopyable(const Uncopyable&); //但阻止copying
Uncopyable& operator=(const Uncopyable&);
};
...
class HomeForSale:private Uncopyable
{
...
//class不再声明copy构造函数或copy assignment操作符
};
☆为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class也是一种做法。
条款07:为多态基类声明virtual析构函数
Declare destructors virtual in polymorphic base classes.
当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,实际执行时通常发生的是对象的derived成分没被销毁。形如这样的classes:
class TimeKeeper
{
public:
TimeKeeper();
~TimeKeeper();
...
};
class AtomicClock: public TimeKeeper {...};
class WaterClock: public TimeKeeper {...};
class WristWatch: public TimeKeeper {...};
消除这个问题的做法很简单:给base class一个virtual析构函数,之后删除derived class对象就会是我们期望的。对,它会销毁整个对象,包括所有derived class成分,就像这样:
class TimeKeeper
{
public:
TimeKeeper();
virtual ~TimeKeeper();
...
};
TimeKeeper* ptk=getTimeKeeper();
...
delete ptk;
析构函数的运作方式是:最深层派生(most derived)的那个class其析构函数最先被调用,然后是其每一个base class的析构函数被调用。
声明virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。欲实现出virtual函数,对象必须携带某些信息,主要用来在运行期决定哪一个virtual函数该被调用。主要是由vptr(virtual table point)指针指出。如果滥用virtual函数,就会相应增加vptr的使用,也就会引起空间的浪费;并且class不含virtual函数时,通常表示它并不意图被用作一个base class。当class不企图被当做base class时,令其析构函数为virtual往往是个馊主意。比如std::string和STL容器就不被设计作为base classes使用,其中内部的是个non-virtual函数,如果将其当成base class,会因为derived成分没被销毁而导致行为不明确。
无端地将所有classes的析构函数都声明为virtual,就像从未声明它们为virtual一样,都是错误的。
☆polymorphic(带多态性质的)base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
☆Classes的设计目的如果不是作为base classes使用,或不是为了具备多态行(polymorphically),就不该声明为virtual函数。
条款08:别让异常逃离析构函数
Prevent exceptions from leaving destructors.
C++并不禁止析构函数吐出异常,但它不鼓励这样做。在析构函数内吐出异常,也就是说,让异常逃离了析构函数,就很容易引起不明确行为。
有两种方法可以避免这种问题。第一种是让析构函数记录内部函数的调用失败,然后不传播或者强行结束程序。
DBConn::~DBConn()
{
try {db.close(); }
catch (...)
{
制作运转记录,记下对close的调用失败;
}
}
DBConn::~DBConn()
{
try {db.close(); }
catch (...)
{
制作运转记录,记下对close的调用失败;
std::abort();
}
}
但如果析构函数内部的close()抛出异常呢?
第二种方法是重新设计DBConn接口,使其客户有机会对可能出现的问题进行反应。
class DBConn
{
public:
...
void close()
{
db.close();
closed=true;
}
~DBConn
{
if (!closed)
{
try
{
db.close();
}
catch (...)
{
制作运转记录,记下对close的调用失败;
...
}
}
}
private:
DBConnection db;
bool closed;
};
如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以为的某个函数。由客户自己调用close并不会对他们带来负担,而是给他们一个处理错误的机会,否则他们没机会相应。
☆析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
☆如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
参考文献:
《Effective C++》3rd Scott Meyers著,侯捷译