构造/析构/赋值运算Constructors,Destructors,and Assignment Operators
5、了解C++默默编写并调用那些函数
Know what functions C++ silently writes and calls.
声明一个empty class空类,经过C++处理后,就会变成一个非empty class,编译器就会为它声明一个default构造函数,一个copy构造函数,一个copy assignment操作符和一个析构函数的类,所有的这些函数都是public且inline。如下:
编码版本:
class Empty { };
编译器处理后版本:
class Empty {
public:
Empty() {…} //default构造函数
Empty(const Empty& rhs) {…} //copy构造函数
~ Empty() {…} //析构函数
Empty& operator=(const Empty& rhs) {…} //copy assignment操作符
};
但是这些函数只有被需要(被调用)时,才会被编译器创建出来。
Empty e1; //default构造函数,析构函数
Empty e2(e1); //copy构造函数
e2 = e1; //copy assignment操作符
对于default构造函数,析构函数主要是给编译器一个地方放置“藏身幕后”的代码,像是调用base classes和non-static成员变量的构造函数和析构函数。copy构造函数和copy assignment操作符编译器创建的版本只是单纯地将来源对象的每一个non-static成员变量拷贝到目标对象。
如下一个NamedObject template:
template<typename T>
class NamedObject {
public:
NamedObject(const char* name, const T& value);
NamedObject(const std::string& name, const T& value);
…
private:
std::string nameValue;
T objectValue;
};
由于其声明了构造函数,编译器不再为它创建default构造函数,这就意味如果设计一个class,其构造要求实参,不用担心编译器默认添加一个无实参构造函数而覆盖自己定义版本。由于没有声明copy构造函数和copy assignment操作符,编译器会默认创建。如下用法:
NamedObject<int> no1(“Smallest”, 2);
NamedObject<int> no2(no1);
此时编译器生成的copy构造函数,对于no2.nameValue的初始化方式为调用string的copy构造函数并以no1.nameValue为实参。对于no2.objectValue由于为int是内置类型,会以拷贝no2.objectValue内的每一个bits来完成初始化。而对于copy assignment操作符的操作和copy构造函数类似。
但是如果nameValue为std::string& nameValue时,如果对于两个对象进行赋值操作,此时由于两个对象的nameValue为引用,但是C++不允许让reference改指向不同对象,此时编译器会拒绝此赋值动作。
- 编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符以及析构函数
6、若不想使用编译器自动生成的函数,就该明确拒绝
Explicitly disallow the use of compiler-generated functions you do not want.
如果不想让某个类的对象通过赋值的方式创建或者对象被赋值,此时需要阻止编译器生成默认的copy构造函数和copy assignment操作符,此时可以将copy构造函数和copy assignment操作符声明为private。如此编译器无法默认创建其专属版本,而这些函数为private,是其他人无法调用它。但是此做法并不是绝对安全,因为member函数和friend函数还是可以调用此private函数。此时可以通过只声明不实现copy构造函数和copy assignment操作符,如此如果有函数调用它们则会提示连接错误(linkage error)。如下:
class HomeForSale {
public:
…
private:
…
HomeForSale(const HomeForSale&); //只有声明
HomeForSale& operator=(const HomeForSale&);
};
另一种更好的办法是设计一个专门为阻止copying动作的base class,然后private继承它即可。如下:
class Uncopyable {
protected: //允许派生(derived)对象构造和析构
Uncopyable() {}
~Uncopyable() {}
private:
Uncopyable(const Uncopyable&); //阻止copying
Uncopyable& operator=(const Uncopyable&);
};
class HomeForSale: private Uncopyable {…};
- 为驳回编译器自动提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base
class也是一种做法。
7、为多态基类声明virtual析构函数
Declare destructors virtual in polymorphic base classes.
对于有如下例子,记录时间,基类TimeKeeper,三个具体实现类继承与TimeKeeper:
class TimeKeeper {
pubilc:
TimeKeeper();
~ TimeKeeper();
…
};
class AtomicClock:public TimeKeeper{…}; //原子钟
class WaterClock:public TimeKeeper{…}; //水钟
class WristClock:public TimeKeeper{…}; //腕表
对于正常使用的话,由于不想操作具体细节,可以利用factory(工厂)函数,返回一个基类的指针,指向新生成的派生类(derived class)对象,返回通过此函数获得一个动态分配的对象,在使用后,释放它,避免资源泄漏:
TimeKeeper *getTimeKeeper(); //返回一个指针,指向TimeKeeper派生类动态分配对象
TimeKeeper *ptk = getTimeKeeper(); //获得一个动态分配对象
… //使用它
delete ptk; //释放
由于获得的指针指向一个派生类对象,但是由一个基类指针而删除,由于基类中有一个non-virtual析构函数,其结果未有定义—实际执行时通常发生的是对象的派生成分没被销毁。这样造成了一个诡异的“局部销毁”对象。为了清除这个问题:给基类一个virtual析构函数,此后删除派生类对象就会销毁整个对象,包括派生类部分:
class TimeKeeper {
pubilc:
TimeKeeper();
virtual ~ TimeKeeper();
…
};
对于TimeKeeper这样的基类一般除了析构函数外,通常还有其他的virtual函数,因为virtual函数的目的是允许派生类的实现得以定制化。例如TimeKeeper就可能拥有一个virtual getCurrentTime,它在不同的派生类中有不同的实现。任何class只要带有virtual函数都几乎确定也有一个virtual析构函数。
- polymorphic(带多态性质的)基类应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
- Classes的设计目的如果不是作为基类使用,或不是为了具备多态性,就不该声明virtual析构函数。
8、别让异常逃离析构函数
Prevent exceptions from leaving destructors.
C++并不禁止析构函数抛出异常,但它不建议这样做。例子如下:
class Widget{
pubilc:
~Widget() {…} //假设这个可能抛出一个异常
};
Void do Something()
{
std:vector< Widget > v;
…
} //v在这里被自动销毁
当vector v被销毁时,它需要销毁其内所有的Widgets。而在析构第一个Widget期间,有个异常被抛出,但其他的还应该被销毁,因此v应该调用它们各个析构函数,如果第二个Widget析构函数又抛出异常,现在有两个同时作用的异常,对C++而言太多了,此时程序若不结束执行就是导致不明确的行为。
但是如果析构函数必须执行一个动作,而这个动作可能会在失败时抛出异常,如下例子,使用一个class负责数据库连接:
class DBConnection {
public:
static DBConnection create(); //返回DBConnection对象
void close(); //关闭连接,失败抛出异常
};
为了确保数据库连接总能被关闭,在析构函数上调用close()函数:
class DBConn { //管理DBConnection对象
public:
…
~ DBConn() { db.close(); } //确保数据库连接总是被关闭
private:
DBConnection db;
};
可以如下使用数据库连接:
{
DBConn dbc(DBConnection::create());
…
}
但是如果调用导致异常,Dbconn析构函数会传播该异常,也就是允许它离开这个析构函数。为了避免这个问题,可以使用如下两个办法:
如果close抛出异常就结束程序。通过调用abort完成:
DBConn::~ DBConn()
{
try{ db.close(); }
catch (…) { //制作运转记录,记下对close的调用失败;
std::abort();
}
}
吞下因调用close而发生的异常:
DBConn::~ DBConn()
{
try{ db.close(); }
catch (…) {
//制作运转记录,记下对close的调用失败;
}
}
但是这这种办法都无法对导致close抛出异常的情况做出反应。一个更好的办法是重新设计DBConn接口,是其客户有机会对可能出现的问题作出反应。例如DBConn自己可以提供一个close函数,因而赋予客户一个处理异常的机会。DBConn也可以追踪其所管理的DBConnection是否已被关闭,并在未关闭时由其析构函数关闭。这可以防止遗失数据库连接。但是如果DBConnection析构函数调用close失败,然后还是使用强迫程序结束或吞下异常。新类如下:
class DBConn { //管理DBConnection对象
public:
…
void close() { db.close(); closed = true;}
~ DBConn()
{
if(!closed) {
try { db.close();}
catch(…)
{ //根据运行记录,记下对close的调用失败
}
}
}
private:
DBConnection db;
bool closed;
};
- 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕获任何异常,然后吞下它们(不传播)或结束程序。
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
9、绝不在构造和析构过程中调用virtual函数
Never Call virtual functions during construction or destruction.
假设有个class继承体系,用来模拟股市交易如买入、卖出的订单等,然后每个交易在升级日志中都需要创建一笔记录:
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的构造函数一定会更早调用,派生类对象的基类成分会在派生类自身成分被构造之前先构造完成。但是Transaction构造函数的最后一行调用virtual函数logTransaction,这时候调用的logTransaction是Transaction内的版本,不是BuyTransaction内的版本,因为基类构造期间virtual函数绝对不会下降到派生类阶层。某些编译器会为此发出一个警告信息,即使没有警告,因为logTransaction函数在Transaction内是个pure virtual。除非它被定义(但基本不可能),否则程序无法连接,因为连接器找不到必要的Transaction::logTransaction的实现代码。
相同的道理也适用于析构函数。一旦派生类析构函数开始执行,对象内的派生类成员变量便呈现未定义值。
对于上述例子,在class Transaction内将logTransaction改为non-virtual,然后要求派生类构造函数传递必要的信息给Transaction构造函数,而后那个构造函数便可以安全地调用non-virtual logTransaction函数,如下:
class Transaction {
public:
explicit Transaction(const std::string& logInfo);
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信息传递给基类构造
…
private:
static std:string createLogString(parameters);
};
由于无法使用virtual函数从基类向下调用,在构造期间可以派生类将必要的构造信息向上传递给基类构造函数。需要注意private static函数createLogString的使用,比使用初值列更加方便也可读性更高。令此函数为static,也就不肯能意外指向初期未成熟的BuyTransaction对象内尚未初始化的成员变量。
在构造和析构期间不用调用virtual函数,因为这类调用从不下降至派生类(比起当前执行构造函数和析构函数的那层)。
10、令operator=返回一个reference to *this
*Have assignment operators return a reference to this.
C++中可以将赋值写成连锁形势
int x,y,z;
x = y = z = 15;
赋值采用右结合,所以被被解析为
x = (y =( z = 15));
为了实现此类操作,赋值操作符必须返回一个reference指向操作符的左侧的实参:
class Widget{
public:
…
Widget& operator=(const Widgets& rhs)
{ …
return* this;
}
};
同样上述方法也适用于所有赋值相关运算:
class Widget{
public:
…
Widget& operator+=(const Widgets& rhs) //适用于+=、-=、*=等等
{ …
return* this;
}
Widget& operator=(int rhs) //即使参数类型不同也可以
{ …
return* this;
}
};
所有内置类型和标准程序库提供的类型如string,vector,complex,tr1:shared_ptr都提供此支持。
- 令赋值(assignment)操作符返回一个reference to *this。
11、在operator=中处理“自我赋值”
Handle assignment to self in operator=.
“自我赋值”发生在对象被赋值给自己时:
class Widget { … };
Widget w;
…
w = w; //赋值给自己
看起来有点异常,但是是合法的,而且有可能会这么做,而且很多时候很难识别,例如:
a[i] = a[j]; //潜在的自我赋值,如果i=j
*px = *py //潜在的自我赋值,如果指向同一个东西
这些并不明显的自我赋值,是别名(aliasing)带来的结果。一般而言如果某段代码操作pointers或references而它们被用来“指向对个相同类型的对象”,就需要考虑这些对象是否为同一个。两个对象如果来自同一个继承体系,不需要声明为相同类型就可能操作“别名”,因为一个基类的引用或指针可以指向一个派生类对象。为了阻止这种错误,让operator=具备“异常安全性”,为了考虑效率可以把证同测试(identity test)加在起始处。如下
Widget& Widget::operator=(const Widget& rhs)
{
if(this == &rhs) return *this; //证同测试
Bitmap* pOrig = pb; //记住原先的pb
pb = new Bitmap(*rhs.pb); //令pb指向*pb的一个副本
delete pOrig; //删除原先的pb
return *this;
}
另一个方案是使用copy and swap技术,如下:
class Widget {
void swap(Widget& rhs); //交换*this和rhs数据
…
};
Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs); //为rhs数据制作一根副本
swap(temp); //将*this数据和上述副本的数据交换
return *this;
}
- 确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
- 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
12、复制对象时勿忘其每一个成分
Copy all parts of an object.
设计良好的面向对象系统(OO-system)会将对象的内部封装起来,只保留两个函数负责对象拷贝(复制),即copy构造函数和copy assignment操作符,合称为copying函数。
如果自定义了copying函数,然后编译器对于其中的错误很难捕捉到。如下一个class顾客顾客,提供自定义copying函数,使得外界对它们的调用会被记录下来:
void logCall(const std::string& funcName); //记录log函数
class Customer {
public:
…
Customer(const Customer& rhs);
Customer& operator=( const Customer& rhs);
private:
std::string name
};
Customer::Customer(const Customer& rhs):name(rhs.name) //复制rhs的数据
{
logCall(“Customer copy constructor”);
}
Customer::Customer& operator=( const Customer& rhs)
{
logCall(“Customer copy assignment constructor”);
name = rhs.name; //复制rhs的数据
return *this;
}
但是如果加入一个新的成员lastTransaction,但是目前的copying函数执行的是局部拷贝,而且此时出现的问题,编译器很难捕获到:
class Date { … };
class Customer
{
public:
…
private:
std::string name;
Date lastTransaction;
};
如果发生继承,会出现更难发现的问题,如下例子只是复制了PriorityCustomer内的成员变量,但每个PriorityCustomer内还包含所继承的Customer成员变量副本,而那些成员变量却未被复制:
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;
}
对于此情况,如果基类为自定义copying函数,则需要为派生类编写自定义copying函数,此时需要很小心的复制其基类成分,对于private的成分,无法直接访问它们,应该让派生类的copying函数调用相应的基类的函数:
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
:Customer(rhs), priority(rhs.priority) //调用基类的copy构造函数
{
logCall(“PriorityCustomer copy constructor”);
}
PriorityCustomer::PriorityCustomer& operator=(const PriorityCustomer& rhs)
{
logCall(“PriorityCustomer copy assignment operator”);
Customer::operator=(rhs); //对基类成分进行赋值动作
priority = rhs.priority;
return *this;
}
当编写一个copying函数,确保赋值所有local成员变量,调用所有基类内的适当的copying函数。如果copying构造函数和copy assignment操作符有很多相近的代码,消除重复代码的正确做法为建立一个新的成员函数给两者调用。通常是private的init函数。
- Copying函数应该确保复制“对象内的所有成员变量”及“所有基类成分”。
- 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。
上一篇: C++进阶_Effective_C++第三版(一) 让自己习惯C++ Accustoming Yourself to C++
下一篇: C++进阶_Effective_C++第三版(三) 资源管理 Resource Management