条款05 : 了解C++默默编写并调用哪些函数
编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。
1. default构造函数和析构函数:主要是给编译器一个地方用来放置“藏身幕后”的代码,像是调用base classes和non-static成员变量的构造和析构函数;当我们显式声明了一个构造函数,编译器于是不再为我们的类创建default构造函数。
2. 如果你打算在一个“内含reference成员”的class内支持赋值操作,则必须自己定义copy assignment操作符。面对“内含const成员”的classes,编译器的反应也是一样的,面对"内含const成员"的classes,更改const成员是不合法的,所以编译器不知道如何在它自己生成的赋值函数面对它们。
3. 如果某个base classes将copy assignment操作符声明为private,编译器将拒绝为其derived classes生成一个copy assignment操作符。因为:derived classes 所生成的copy assignment操作符需要处理base class成分,但它们却无法调用base class 的copy assignment操作符。(其他的default构造函数,copy构造函数以及析构函数也一样。这也是下一个条款所描述的“若不想使用编译器自动生成的函数,就该明确拒绝”的其中一种实现方法:通过在base class中显式(编译器不再自动生成)的将构造函数等放置在private(阻止人们调用它)下,阻止编译器自动生成。)
故而:
编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。
条款06 : 若不想使用编译器自动生成的函数,就该明确拒绝
1. 所有编译器产出的函数都是public。为了阻止这些函数被创建出来,你得自己声明它们,但这里并没有什么需求使你必须将它们声明为public。因此你可以将copy构造函数或copy assignment操作符声明为private。藉由明确声明一个成员函数,你阻止了编译器自创建其专属版本;而令这些函数为private,使你得以成功阻止人们调用它。但这有时还不够,因为member函数和friend函数还是可以调用你的private函数。除非你够聪明,不去定义它们,那么如果某些人不慎调用任何一个,会获得一个连接错误。“将成员函数声明为private而且故意不实现它们”可以被很好的用来阻止copying行为(copy构造函数和copy assignment操作符)。如下:
class HomeForSale{ public: ...... private: ...... HomeForSale(const HomeForSale&); //只有声明 HomeForSale& operator=(const HomeForSale&); };
2. 将连接期错误移至编译期是可能的(而且那是好事,毕竟越早侦测出错误越好)。如下:
class Uncopyable{ protected: Uncopyable() {} //允许derived对象构造和析构 ~Uncopyable() {} private: Uncopyable(const Uncopyable&); //但阻止copying Uncopyable& operator=(const Uncopyable&); };
为了阻止HomeForSale对象被拷贝,我们唯一需要做的就是继承Uncopyable:
class HomeForSale:private Uncopyable{ ....... // class 不再声明copying函数 };
只要任何人——甚至是member函数或friend函数——尝试拷贝HomeForSale对象,编译器便试着生成copying函数,而正如条款12所说,这些函数的“编译器生成版”会尝试调用其base class的对应兄弟,那些调用会被编译器拒绝,因为其base class的拷贝函数式private。
故而:
为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class 也是一种做法。
条款07 : 为多态基类声明virtual析构函数
1. C++指出:当derived class 对象经由一个base class 指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义——实际执行时通常发生的是对象的derived成分没被销毁,而其base class成分通常会被销毁,于是造成一个诡异的“局部销毁”对象。
解决方案:给base class一个virtual析构函数。任何class只要带有virtual函数(多态)都几乎确定应该也有一个virtual析构函数。
如果class不含virtual函数,通常表示它并不意图被用做一个base class,当class不企图被当作base class,令其析构函数为virtual往往是个馊主意。原因在于:欲实现virtual函数,对象必须携带某些信息,主要用来在运行期决定哪一个virtual函数该被调用。这份信息通常是由一个所谓vptr(virtual table pointer)指针指出。vptr指向一个由函数指针构成的数组,称为vtbl(virtual table);每一个带有virtual函数的class都有一个相应的vtbl。当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr所指的那个vtbl——编译器在其中寻找适当的函数指针。这导致对象的体积增加,并且移植性变差。
2. 很多人的心得是:只有当class内含至少一个virtual函数,才为它声明virtual析构函数。然而,即使class完全不带virtual函数,被“non-virtual析构函数问题”给咬伤还是有可能。如下:
class SpecialString:public std::string { // 馊主意。std::string有个non-virtual析构函数 ...... };
请考虑下面代码:
SpecialString* pss = new SpecialString("Impending Doom"); std::string * ps; .... ps = pss; //SpecialString* =>std::string* ... delete ps; // 未有定义,现实中*ps的SpecialString资源会泄露,因为SpecialString析构函数没被调用
注意:相同的分析使用与任何不带virtual析构函数的class,包括所有的STL容器如vector,list,set,tr1::unordered_map(条款54)等等。记住:不要企图继承一个标准容器或任何其他”带有non-virtual析构函数“的class。
3. 由于抽象class(不能被实体化的class,也即不能构造出对象)总是企图被当作一个base class来用,而又由于base class应该有个virtual析构函数,并且由于pure virtual函数(纯虚函数)会导致抽象class,因此:为你希望它成为抽象的那个class 声明一个pure virtual析构函数。如下:
class AWOV { public: virtual ~AWOV () = 0; // 声明pure virtual析构函数 }; AWOV :: ~AWOV () { } // pure virtual析构函数定义
注意:必须为这个pure virtual析构函数提供一份定义。析构函数的运作方式是,最深层派生的那个class其析构函数最先被调用(与构造函数的调用顺序相反),然后是其每一个base class的析构函数被调用。编译器会在AWOV的derived classes的析构函数中创建一个对~AWOV的调用动作(详见条款05-1),所以你必须为这个函数提供一份定义。否则编译器会报错。
故而:
1. polymorphic(带多态性质的)base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
2. Classes的设计目的如果不是作为base class使用,或不是为了具备多态性(如条款06-2中的基类Uncopyable),就不该声明virtual析构函数。
条款08: 别让异常逃离析构函数
首先考虑以下代码:
class DBConnection { public: ... static DBConnection create(); // 这个函数返回DBConnection 对象 void close(); //关闭联机;失败则抛出异常。 }; class DBConn { // 这个class用来管理DBConnection 对象 public: .... ~DBConn() // 确保数据库连接总是会被关闭 { db.close(); } private: DBConnection db; };
只要调用close成功,一切都美好。但如果该调用导致异常,DBConn析构函数会传播该异常,也就是允许它离开这个析构函数。那会造成问题,因为那就是抛出了难以驾驭的麻烦。有两个方法可以避免这一问题:
1. 如果close抛出异常就结束程序。通常通过调用abort完成。
DBConn::~DBConn() { try { db.close(); } catch( ... ){ // 日志 std::abort(); } }
2. 吞下因调用close而发生的异常。
DBConn::~DBConn() { try { db.close(); } catch( ... ){ // 日志 } }
然而,上面两种方法都不尽如人意,因为它们都无法对“导致close抛出异常”的情况做出反应。一个较佳策略如下:
class DBConn { public: ..... void close() //供客户使用的新函数 { db.close(); closed = true; } ~DBConn() { if (!closed) { try { db.close(); } catch( ... ){ // 日志 .... } } } private: DBConnection db; bool closed; };
注意:这个在DBConn类中提供一个供客户使用的新函数,如果客户不调用,那么才会在析构函数中调用DBConnection的close函数关闭联机。这就给了客户一个处理相应异常的机会,并且在析构函数中做了双重保险。
如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。因为析构函数吐出异常就是危险,总会带来“过早结束程序”或“发生不明确行为”的风险。
故而:
1. 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
2. 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
条款09: 绝不在构造和析构过程中调用virtual函数
首先考虑以下代码:
class Transaction { 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;
无疑地会有一个BuyTransaction构造函数被调用,但首先Transaction构造函数一定会更早被调用;是的,derived class对象内的base class成分会在derived class 自身成分被构造之前先构造妥当。
问题在于:Transaction构造函数最后调用了logTransaction是Transaction内的版本,不是BuyTransaction内的版本——即使目前即将建立的对象类型是BuyTransaction。是的,base class构造期间virtual函数绝不会下降到derived classes阶层 --> (理由)由于base class 构造函数的执行更早于derived class构造函数,当base class构造函数执行时derived class的成员变量尚未初始化。如果此期间调用的virtual函数下降至derived class阶层,要知道derived class 的函数几乎必然取用local成员变量,而那些成员变量尚未初始化。这将是一张通往不明确行为和彻夜调试大会的车票。 --> (更根本的原因)在derived class 对象的base class构造期间,对象的类型是base class而不是derived class。不只virtual函数会被编译器解析至base class ,若使用运行期类型信息,也会把对象视为base class 类型。这样的处理是合理的:derived class的专属成分尚未被初始化,所以面对它们,最安全的做法就是视它们不存在。对象在derived class构造函数开始执行前不会成为一个derived class对象。
相同的道理也适用于析构函数。一旦derived class析构函数开始执行,对象内的derived class成员变量便呈现未定义值,所以C++视它们仿佛不再存在。进入base class析构函数后对象就成为一个base class对象,而此时,C++的任何部分包括virtual函数、dynamic_casts等等也就那么看待它。
解决方案:在base class 内将virtual函数改为non-virtual,然后要求derived class构造函数传递必要信息给base class构造函数,而后base class 构造函数就可以安全的调用non-virtual函数了。如下:
class Transaction { public: explicit Transaction (const std::string& logInfo); //单参数构造函数,最好使用explicit禁止其进行隐式类型转换 void logTransaction (const std::string& logInfo) const; //non-virtual函数 ...... }; Transaction::Transaction (const std::string& logInfo) { ...... logTransaction (logInfo); //non-virtual调用 } class BuyTransaction :public Transaction { public: BuyTransaction(parameters) :Transaction(createLogString(parameters)) // 将log信息传给base class 构造函数 { ..... } .... private: static std::string createLogString(parameters); // 函数为static };
注意:比起成员初值列内给予base class所需数据,利用辅助函数创建一个值传给base class构造函数往往比较方便(也比较可读)。
令此函数为static,也就不可能意外指向"初期未成熟之derived class对象内尚未初始化的成员变量"。可以参考Effective C++ —— 让自己习惯C++(一)条款04 和 C++类中的static数据成员,static成员函数
故而:
在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)。这就是所谓的:virtual函数在构造/析构期间的“失常表现”,也即,在此期间,virtual函数不是virtual函数。
条款10: 令operator=返回一个reference to *this.
关于赋值,你可以写下如下语句:
int x, y, z; x = y = z = 15; // 赋值连锁形式 // 赋值采用右结合律,所以上述连锁赋值被解析为: x = (y = (z = 15));
这里15先被赋值给z,然后其结果(更新后的z)再被赋值给y,然后其结果(更新后的y)再被赋值给x。
为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧。
故而:
令赋值操作符返回一个reference to *this.
条款11: 在operator=中处理“自我赋值”
“自我赋值”发生在对象被赋值给自己时。考虑以下代码:
class Bitmap { ... } class Widget{ .... private: Bitmap *pb; //指针,指向一个从heap分配而得的对象 }; --------------------Method1 不具备“自我赋值安全性”、不具备“异常安全性”---------------------------------------------------------- Widget& Widget::operator=(const Widget& rhs) //一份不安全的operator=实现版本 { delete pb; // 停止使用当前的bitmap pb = new Bitmap(*rhs.pb); // 使用rhs's bitmap的副本 return *this; } --------------------Method2 具备“自我赋值安全性”、不具备“异常安全性”---------------------------------------------------------- Widget& Widget::operator=(const Widget& rhs) // { // 赋值之前会先释放自身的内容,如果是自己,数据就丢失了
if (this == &rhs) return *this; //证同测试,自我赋值安全性 delete pb; // 停止使用当前的bitmap pb = new Bitmap(*rhs.pb); // 申请内存失败,此时pb已被删除,导致异常 return *this; } --------------------Method3 具备“自我赋值安全性”、具备“异常安全性”---------------------------------------------------------- // 让operator=具备“异常安全性”往往自动获得“自我赋值安全性”的回报,只需注意在复制pb所指东西之前别删除pb: Widget& Widget::operator=(const Widget& rhs) // { Bitmap* pOrig = pb; // 记住原先的pb pb = new Bitmap(*rhs.pb); // 若申请内存失败,原先的pb此时仍未被删除(保持原状),不导致异常;new分配失败时,会抛出异常跳过后面的代码
若成功,皆大欢喜,pb指向新的内容,后面pOrig正常删除原来数据;
若成功,但是是自我赋值,那么由于此时pb所指的内容还未被删除,pb指向自己的一个副本(新的内容),之后才删除原来的内容,不会导致自我赋值时数据丢失的可能
delete pOrig; // 删除原先的pb,在赋值之后删除
return *this; } --------------------Method4 具备“自我赋值安全性”、具备“异常安全性”---------------------------------------------------------- // copy and swap技术(条款29) class Widget{ .... void swap(Widget& rhs); //交换*this和rhs的数据(见条款29) .... }; Widget& Widget::operator=(const Widget& rhs) // { Widget temp(rhs); //为rhs数据制作一份复本 swap(temp); //将*this数据和上述复件的数据交换 return *this; } --------------------Method5 具备“自我赋值安全性”、具备“异常安全性”---------------------------------------------------------- // (1) 某class的copy assignment操作符可能被声明为“以by value方式接受实参”; // (2) 以by value方式传递东西会造成一份复件; Widget& Widget::operator=(Widget rhs) //rhs是被传对象的一份复件,注意这里是pass by value { swap(ths); //将*this数据和上述复件的数据交换 return *this; }
故而:
1. 确保当对象自我赋值时operator= 有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
2. 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
条款12: 复制对象时勿忘其每一个成分
设计良好之面向对象系统会将对象的内部封装起来,只留两个函数负责对象拷贝,那便是带着适切名称的copy构造函数和copy assignment操作符,也即所谓的copying函数。
考虑以下代码:
class Date { ... }; class Customer { public: ..... private: std::string name; Date lastTransaction; }; 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& PriorityCustomer::operator=(const PriorityCustomer& rhs) { logCall("PriorityCustomer copy assignment operator"); priority = rhs.priority; return *this; // 条款10 }
PriorityCustomer的copying函数看起来好像复制了PriorityCustomer内的每一样东西,但注意,它们复制了PriorityCustomer声明的成员变量,但每个PriorityCustomer还内含它所继承的Customer成员变量复件,而那些成员变量却未被复制。PriorityCustomer的copy构造函数并没有指定实参传给其base class构造函数(也就是说它在它的成员初值列中没有提到Customer),因此PriorityCustomer对象的Customer成分会被不带实参之Customer构造函数(即default构造函数——必定有一个否则无法通过编译)初始化。default构造函数将针对name和lastTransaction执行缺省的初始化动作。
以上事态在PriorityCustomer的copy assignment操作符身上只有轻微不同。
所以,任何时候,只要你决定自己承担起“为derived class撰写copying函数”的重责大任,就必须很小心地复制其base class成分。那些成分往往是private(条款22),所以你无法直接访问它们,你应该让derived class的copying函数调用相应的base class 函数。如下:
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs) :Customer(rhs), // 调用base class 的copy构造函数 priority(rhs.priority) { logCall("PriorityCustomer copy constructor"); } PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) { logCall("PriorityCustomer copy assignment operator"); Customer::operator=(rhs); // 对base class成分进行赋值动作 priority = rhs.priority; return *this; // 条款10 }
注意:虽然两个copying函数往往有相似的实现本体,但却不能互为调用。令copy assignment操作符调用copy构造函数是不合理的,因为这就像试图构造一个已经存在的对象。相反,令copy构造函数调用copy assignment操作符同样无意义,构造函数用来初始化新对象,而assignment操作符只施行于已初始化对象身上,对一个尚未构造好的对象赋值,就像在一个尚未初始化的对象身上做“只对已初始化对象才有意义”的事一样。无聊嘛,别尝试。
故而:
1. Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。
2. 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。