Effective C++ 学习笔记(二)构造/析构/赋值运算
参考书籍《Effective C++:改善程序与设计的55个具体做法(第三版)》
5. 了解C++默默编写并调用哪些函数
-
为什么要?
-
示例
- C++自动为类声明的函数:默认构造函数、析构函数、copy构造函数和copy assignment函数
// 当写下这句时 class Empty {}; // 实则是以下内容(当然如果没有用到的函数,编译器就不会编译出来): class Empty{ public: Empty() {...} // 默认构造函数(仅当这个类无任何构造函数时) Empty(const Empty& rhs) {...} // copy构造函数 ~Empty() {...} // 析构函数 Empty()& operator=(cosnt Empty* rhs) {...} // copy assignment操作符 };
- 默认的构造函数和析构函数主要是给编译器一个地方用来放置“藏身幕后”的代码,像是调用base classes和non-static成员变量的构造函数和析构函数。
- copy构造函数和copy assignment操作符,编译器创建的版本只是单纯地将来源对象的每一个non-static 成员变量拷贝到目标对象。
NameObject{ private: std::string nameValue; T objectValue; ... }; NameObject<int> no1("Smallest Prime Number", 12); NameObject<int> no2(no1); //调用生成的copy构造函数 // 将使用“string的copy构造函数”为类域中nameValue初始化 // 而内置类型,如这里的int,将拷贝objectValue每一个bits来完成初始化
- 当类域中有引用和const对象时,你必须自己定义copy assignment操作符
class NamedObject{ private: std::string& nameValue; const T objectValue; ... }; NamedObject<int> p(std::string("Persephone"), 2); NamedObject<int> s(std::string("Satch", 36)); p = s; // 出错!
■ 编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment 操作符,以及析构函数。
- C++自动为类声明的函数:默认构造函数、析构函数、copy构造函数和copy assignment函数
6. 若不想使用编译器自动生成的函数,就该明确拒绝
-
为什么要?
- 有些情况下,你并不允许copy构造函数和copy assinment操作符的出现
-
示例
- 自己自定义声明private并且什么事都没干的copy构造函数和copy assinment操作符
- 用一个基类,将其copy构造函数和copy assinment操作符声明为private,然后真正不想被拷贝的类继承于这个类。这样做的好处是,真正不想被拷贝的类的成员函数和友元类尝试拷贝时,错误发生在编译期,而上一种做法则是在连接期(毕竟愈早侦测出错误愈好)
class Uncopyable{ private: Uncopyable(const Uncopyable&); Uncopyable& operator=(const Uncopyable&); // 不用去实现,参数名称便也不用指定 ... }; class HomeForSale:Uncopyable{ ... };
■ 为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class也是一种做法。
7. 为多态基类声明virtual析构函数
-
为什么要?
- 派生类的析构函数应该交给它自己来做,如果由基类来做,派生类内专属于自己的成员变量就没不会被销毁,这会造成资源泄露问题!
-
示例
-
让基类的析构函数变成virtual的
class TimeKeeper{ public: TimeKeeper(); virtual ~TimeKeeper(); ... }
-
virtual函数的实现原理:每一个带有virtual函数的class都有一个相应的vtbl。当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr(virtual table pointer)所指的那个vtbl(virtual table)——编译器在其中寻找适当的函数指针。
-
所有 STL 容器都不带 virtual 析构函数,所以绝对不能继承他们!
■ polymorphic(带多态性质的)base classes 应该声明一个 virtual 析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
■ Classes 的设计目的如果不是作为 base classes 使用,或不是为了具备多态性(polymorphically),就不该声明virtual析构函数。
-
8. 别让异常逃离析构函数
-
为什么要?
- 析构函数吐出异常是危险的,总会带来“过早结束程序”或“发生不明确行为”的风险。看下面例子,当 vector v 被销毁,它有责任销毁其内含的所有 Widget,当在析构第一个元素期间,有个异常被抛出,而第二个Widget析构函数又抛出异常。C++在两个异常同时存在的情况下,程序若不是结束执行就是导致不明确行为。
class Widget{ public: ~Widget() {...} // 假设这里可能吐出一个异常 ... }; void doSomething() { std::vector<Widget> v; ... } // v在函数退出时自动销毁
- 析构函数吐出异常是危险的,总会带来“过早结束程序”或“发生不明确行为”的风险。看下面例子,当 vector v 被销毁,它有责任销毁其内含的所有 Widget,当在析构第一个元素期间,有个异常被抛出,而第二个Widget析构函数又抛出异常。C++在两个异常同时存在的情况下,程序若不是结束执行就是导致不明确行为。
-
示例
- 如果希望抛出异常就结束程序,通常通过调用abort完成:
class DBConn{ public: ~DBConn(){ try { db.close(); // 可能throw出异常 } catch (...) { std::abort(); } } private: DBConnection db; ... }
- 吞下异常
... ~DBConn(){ try { db.close(); // 可能throw出异常 } catch (...) { // 做记录工作 } } ...
- 如果想让客户有处理异常的机会,可以自己提供一个close函数:
class DBConn{ public: void close() { db.close(); closed = true; } ~DBConn(){ if(!closed) { try { db.close(); } catch (...) { // 做记录工作 } } } private: DBCOnnection db; bool closed; }; DBConn dbc; try { dbc.close(); } catch (...) { // 客户可以在此处理异常 }
■ 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
■ 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通函数(而非在析构函数中)执行该操作。
- 如果希望抛出异常就结束程序,通常通过调用abort完成:
9. 绝不在构造和析构过程中调用virtual函数
-
为什么要?
- 在基类构造期间,virtual函数不是virtual函数。当创建一个派生类对象时,会先调用基类的构造函数,而当这个基类的构造函数里调用一个虚函数时 ,这个虚函数会是基类的版本。
class Transaction{ public: Transaction(); virtual void logTransaction() const = 0; }; Transactoin::Transcation() { logTransaction(); ... } class BuyTransaction: public Transaction { public: virtual void logTransaction() const; ... };
- 相同道理也适用于析构函数。一旦派生类析构函数开始执行,对象内的派生类成员变量便呈现未定义值,所以C++视它们仿佛不再存在。进入基类析构函数后对象就成为一个基类对象,而C++的任何部分包括virtual函数、dynamic_casts等等也就那么看待它。
- 在基类构造期间,virtual函数不是virtual函数。当创建一个派生类对象时,会先调用基类的构造函数,而当这个基类的构造函数里调用一个虚函数时 ,这个虚函数会是基类的版本。
-
示例
- 在基类里将构造函数里需要调用的virtual函数改为non-virtual,派生类通过参数形式传入必要信息
class Transaction{ public: Transaction(const std::string& logInfo); void logTransaction(const std::string& logInfo) const; //non-virtual }; Transactoin::Transcation(const std::string& logInfo) { logTransaction(logInfo); ... } class BuyTransaction: public Transaction { public: BuyTransaction( [parameters] ) : Transaction(createLogString( [parameters] )) { ... } private: static std::string createLogString( [parameters] ); // 必须是static ... };
■ 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class (比起当前执行构造函数和析构函数的那层)。
- 在基类里将构造函数里需要调用的virtual函数改为non-virtual,派生类通过参数形式传入必要信息
10. 令operator=返回一个 reference to*this
-
为什么要?
- 实现连锁赋值
x = y = z; // x=(y=z)
- 实现连锁赋值
-
示例
-
class Widget{ public: Widget& operator= (const Widget& rhs) { return *this; } };
■ 令赋值(assignment)操作符返回一个reference to*this。
-
11. 在operator=中处理“自我赋值”
-
为什么要?
- 在自我赋值情况下,可能发生错误的operator版本:
class Widget { public: Widget& operator=(const Widget& rhs); private: Bitmap pb; ... };
Widget& Widget::operator=(const Widget& rhs) { delete pb; pb = new Bitmap(*rhs.pb); // 如果&rhs==this,那么rhs.pb就已经被delete了! return *this; }
Widget& Widget::operator=(const Widget& rhs) { if(this == &rhs) return *this; delete pb; pb = new Bitmap(*rhs.pb); // 一旦new出异常,pb已被删除,违背异常安全性原则! return *this; }
- 在自我赋值情况下,可能发生错误的operator版本:
-
示例
-
比较好的做法:
Widget& Widget::operator=(const Widget& rhs) { Bitmap* pOrig = pb; pb = new Bitmap(*rhs.pb); // 即使&rhs==this,delete前也能new一份拷贝 delete pOrig; return *this; }
Widget& Widget::operator=(const Widget& rhs) { Widget temp(rhs); // 函数退出销毁 swap(temp); // 完成this和temp数据的交换 return *this; }
■ 确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
■ 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
-
12. 复制对象时勿忘其每一个成分
-
为什么要?
-
示例
- 当你编写一个copying函数,请确保(1) 复制所有 local 成员变量,(2) 调用所有 base classes 内的适当的copying函数。
- 如果你发现你的copy构造函数和copy assignment操作符有相近的代码,消除重复代码的做法是,建立一个新的成员函数给两者调用。
■ Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。
■ 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个coping函数共同调用。