二.构造/析构/赋值运算
条款05:了解C++默默编写并调用哪些函数
1)
C++编译器会声明编译器版本的copy构造函数,一个copy assignment操作符和一个析构函数,此外若没有声明一个构造函数,则还会声明一个默认构造函数。这些函数都是public且inline的。
2)
copy构造函数和copy assignment操作符,编译器版本只是单纯地将来源对象的每一个non-static成员变量拷贝到目标对象。
3)
如果打算对一个内含reference和const的类支持赋值操作,那需要自己定义copy assignment操作符。此外,在多态场景下,base class将copy assignment操作符声明为private,那么编译器会拒绝为derived class提供一个copy assignment,因为derived class中的copy assignment函数是默认需要对base class部分的成员进行处理的。
条款06:若不想使用编译器提供自动生成的函数,就该明确拒绝
1)
可以将copy构造函数和copy assignment操作符声明为private,避免产生无意义的复制行为:
class HomeForSale{
public:
...
private:
...
HomeForSale(const HomeForSale&); //只有声明
HomeForSale& operator=(const HomeForSale);
}
当有了上述定义后,若在member函数或者friend函数中使用copy或copy assignment,通常也不能编译通过,需要定义一个专门为了阻止copying动作而设计的base class内:
class Uncopyable{
protected: //允许derived对象构造和析构
Uncopyable() {}
~Uncopyable() {}
private:
Uncopyable(const Uncopyable &);
Uncopyable& operator=(const Uncopyable&);
};
//此时继承Uncopyable即可,无需再次声明
class HomeForSale: private Uncopyable{
...
}
2)
为驳回编译器自动提供的函数,可以将相应的函数声明为private并且不予实现,或者使用类似于Uncopyable这样的base class。
条款07:为多态基类声明virtual析构函数
1)
C++指出,当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,会造成未定义结果。实际上通常发生的是对象的derived成分没被销毁,然而其base class则会诡异的被销毁,会造成资源泄漏,难以调试。
解决办法如下:
class TimeKeeper{
public:
TimeKeeper();
virtual ~Timekeeper();
...
};
TimeKeeper* ptk = getTimeKeeper();
...
delete ptk;
2)
通常base class除开virtual析构函数,还会有多个virtual函数,因为virtual函数的目的是允许derived class的实现得以客制化,即在不同的需求下有不同的实现。
3)
若不是base class,但却携带有virtual函数,会增加对象的体积。64位计算机中可能会占用64-128bits,指针占64bits.因此增加有virtual函数的对象,其大小会增加50-100%。这会导致无法将该对象传递到其他函数所使用的函数中,失去移植性。。
4)
析构函数的运作方式是,最深层派生的class其析构函数首先被调用,之后回溯调用每一个base class的析构函数。
多态性质的base class应该声明一个virtual析构函数,若一个函数带有任何virtual函数,那么他应该拥有一个virtual析构函数。
若一个class的设计目的不是为了实现多态或者作为base class,那么不应该声明virtual析构函数。
条款08:别让异常逃离析构函数
1)
对于异常,往往较佳的策略是重新设计函数接口,使其客户有机会对可能出现的问题作出反应。见DBConn Class:
class DBConn{
public:
...
void close() // 供客户使用的函数
{
db.close();
closed = true;
}
~DBConn()
{
if(!close){
try{
db.close();
}
catch (...) {
//制作运转记录,记录close调用失败
...
}
}
}
private:
DBConnection db;
bool closed;
};
本例中提供了一个供用户对异常做出反应的普通函数,若用户放弃对异常的处理,则再依赖析构函数来吞下异常或结束程序。
总而言之,析构函数最好不要吐出异常。
条款09:绝不在构造和析构过程中调用virtual函数
1)
base class构造期间virtual函数绝不会下降到derived classes阶层。不正式的说法为:在base class构造期间,virtual函数不是virtual函数。这是由于base class的执行早于derived class构造函数,当base class构造函数执行时derived class函数的成员变量尚未初始化,若此时base class的virtual函数下至derived class阶层,则会导致未定义的行为。这是derived class必然使用成员local成员变量导致的。
2)
上述更根本的原因是:在derived class对象的base class构造期间,对象的类型是base class而不是derived class,不只virtual函数会被编译器解析至base class,若使用运行期间的类型信息(dynamic_cast和typeid),也会把对象视为base class类型。因为当base class正在允许构造函数初始化时,derived class的成员变量还未初始化,所以最安全的办法就是视这些变量不存在,因此便不会下放到derived class。
3)
确保每一次构造函数和析构函数都没有调用vitual函数的用法,参见Transaction类:
class Transaction{
public:
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo) const //是non-virtual函数;
};
Transaction::Transaction(const std::string& Info)
{
...
logTransaction(logInfo); // non-virtual 调用
}
class BuyTransaction:public Transaction{
public:
BuyTransaction(type parameters): Transaction(createLogString(type parameters))
{...}
private:
static std::string createLogString(type parameters); //与成员初值列相比,使用带static关键字创建的辅助函数传递信息给构造函数可读性更高,也更方便。
}
条款10:令operator=返回一个reference to *this
1)
可以更好的连续调用“=”.
条款11:在operator=中处理自我赋值
1)
第一份operator=实现代码:
class Bitmap {...};
class Widget{
...
private:
Bitmap* pb; //指针,指向一个从heap分配得到的资源
};
Widget& Widget::operator=(const Widget& rhs)
{
delete pb; //未考虑rhs和*this是同一对象的情况,最后会造成一个指针指向一个被删除的对象
pb = new Bitmap(*rhs.pb);
return *this;
}
第二份operator=实现代码:
Widget& Widget::operator=(const Widget& rhs)
{
if(this == &rhs) return *this;
delete pb;
pb = new Bitmap(*rhs.pb); //未考虑new构造过程中出现的异常情况,造成指针指向一块被删除的Bitmap内存
return *this;
}
第三份operator=实现代码
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap* pOrig = pb; //记住原先的pb;
pb = new Bitmap(*rhs.pb); //令pb指向*pb的一个复件
delete pOrig; //删除原先的pb;
return *this;
}
本例中处理了new构造过程中可能导致的异常,也可以应付自我赋值的问题。但还可以通过copy and swap继续优化:
class Widget{
...
void swap(Widget& rhs); //交换rhs和*this的数据
...
};
Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs);
swap(temp);
return *this;
}
本段代码运用了以下事实:
i) class的copy assignment操作符可能被声明为“以by value方式接受实参”;
ii) 以by value方式传递东西会造成一份附件;
请记住:
1.确保对象自我赋值时有良好行为,其中技术包括“比较来源”和“目标对象”的地址,以及精心设计的语句顺序和copy-and-swap技术;
2.确保任何函数操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
条款12:复制对象时勿忘其每一个成分
1)
如果你发现你的copy构造函数和copy assignment操作符有相应的代码,消除重复的代码正确做法应是:建立一个新的成员函数给两者调用,这样的函数往往是private而且常被命名为init.
请记住:
1.copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。
2.不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中由两个函数共同调用。

浙公网安备 33010602011771号