【5】了解C++默默编写并调用哪些函数 (Know what functions C++ silently writes and calls.)
如果你写下
class Empty { };
这样就好像你写下这样的代码:
class Empty {
public:
Empty () {...}
Empty (const Empty& rhs) {...}
~Empty () {...}
Empty& operator= (const Empty& rhs) {...}
};
但是假如NameObject的定义如下,其中nameValue是个reference to string, objectValue是个const T:
template<class T>
class NameObject {
public:
NamedObject(string& name, const T& value);
... //如前,假设并未声明operator=
private:
string &nameValue; //这如今是个reference
const T objectValue; //这如今是个const
};
考虑下面会发生什么事:
string newDog("Persephone");
string oldDog("Satch");
NamedBoject<int> p(newDog, 2);
NamedObject<int> s(oldDog, 36);
p = s; //现在p的成员变量该发生什么事?
面对这个难题,C++的响应式拒绝编译那一行赋值动作。如果你打算在一个“内含reference成员”的class支持赋值操作(assignment),你必须自己定义copy assignment操作符。面对“内含const成员”(如本例之objectValue)的classes,编译器的反应也一样,更改const成员是不合法的,所以编译器不知道如何在它自己生成的赋值函数内面对它们。最后还有一种情况如果某个base classes将将copy assignment操作符声明为private,编译器将拒绝为其derived classes生成一个copy classes操作符。毕竟编译器为derived classes所生的copy assignment错左幅想象中可以处理base class成分,但它们当然无法调用derived class无权调用的成员函数。
编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。
【6】若不想使用编译器自动生成的函数,就该明确拒绝 (Explicitly disallow the use of compiler-generated functions you do not want.)
class HomeForSale{
public:
...
private:
...
HomeForSlae(const HomeForSale&); //只有声明
HomeForSale& operator=(const HomeForSales&);
};
当member函数和friend函数调用你的private函数的时候,因为你只有声明而没有定义,如果某些人不慎调用任何一个会获得一个连接错误(linkage error)。
下面有一个更好的办法:
class Uncopyable {
protected:
Uncopyable() { } //允许derived对象构造和析构
~Uncopyable(){ }
private:
Uncopyable(const Uncopyable&); //但阻止copying
Uncopyable& operator=(const Uncopyable&);
};
为求阻止HomeForSale对象被拷贝,我们唯一需要做的就是继承Uncopyable:
class HomeForSlae : private Uncopyable {
...
};
这下所有的调用copy和copy assignment的操作都会被编译器拒绝。
为驳回编译器子佛能否(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class也是一种做法。
【7】为多态基类声明virtual析构函数 (Declare destructors virtual in polymorphic base classes.)
c++明白支出,当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义——实际执行通常发生的是对象的derived成分没有被销毁。
无端地将所有的classes的析构函数声明为virtual,就想从未声明它们为virtual一样,都是错误的。许多人的心得是:只有当class内含有至少一个virtual函数,才为它声明virtual析构函数。
polymorphic(带多态性质的)base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
Classes的设计目的如果不是作为base classes使用,或者不是为了具备多态性(polymorphically),就不该声明virtual析构函数。
【8】别让异常逃离析构函数 (Prevent exceptions from leaving destructors.)
析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数勇敢捕捉任何异常,然后吞下它们(不传播)或者结束程序。
如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
【9】绝不要在构造和析构过程中调用virtual函数 (Never call virtual functions during construction or destruction.)
class Transaction {
public:
Transaction( ); //所有交易的base class
virtual void logTransaction( ) const = 0; //做出一份因类型不同而不同的日志记录(log entry)
...
};
Transaction::Transaction( )
{
...
logTransaction( );
}
class BuyTransaction: public Transaction {
public:
virtual void logTransaction( ) const;
...
};
class SellTransaction: public Transaction {
public:
virtual void logTransaction() const;
...
};
现在当一下这行被执行,会发生什么事:
BuyTransaction b;
相同的道义也适用于析构函数。一旦derived class析构函数开始执行,对象内的derived class成员变量便呈现未定义值,所以C++视它们仿佛不存在。进入base class析构函数候对象就成为一个bass class对象,而c++的任何部分包括virtual函数、dynamic_casts等等也就那么看待它。
在上述示例中,Transaction构造函数直接调用一个virtual函数,这很明显而且容易看出违反本条款。由于它很容易被看出,所以编译器会为此发出一个警告信息。
但是侦测“构造函数或析构函数运行期间是否调用virtual函数”,并不总是这般轻松。请看下面的例子:
class Transaction {
public:
Transaction( )
{ init( ); } //调用non-virtual
vitural void logTransaction() const = 0;
...
private:
void init()
{
...
logTransaction(); //这里调用virtual
}
};
但你如何确保每次一有Transaction继承体系上的对象被创建,就会有适当版本的logTransaction被调用呢?很显然,在Transaction构造函数(s)内对着对象调用virtual函数是一种错误做法。
一种做法可以解决这个问题。在class Transaction内将logTransaction函数改为non-virtual,然后要求derived class构造函数传递必要信息给Transaction构造函数,而后那个构造函数便可安全地调用non-virtual logTransaction。像这样:
class Transaction{
public:
explicit Transaction(const string& logInfo);
void logTransaction(const stirng & logInfo) const;
...
};
Transaction::Transaction(const string& logInfo)
{
...
logTransaction(logInfo);
}
class BuyTransaction: public Transaction{
public:
BuyTransaction(parameters):Transaction(creatLogString(parameters))
{ ... }
...
private:
static string createLogString(parameters);
};
换句话说,由于你无法使用virtual函数从base classes向下调用,在构造期间,你可以借由“令derived classes将必要的构造信息向上传递至base class构造函数”替换之而加以弥补。
在构造和析构期间不要调用virtual函数。因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)。
【10】令operator=返回一个reference to *this (Have assignment operators return a reference to *this.)
Class Widget{
public:
…
Widget& operator=(const Widget&rhs)
{
…
return *this;
}
…
};
【面试题】return *this和return this有什么区别?
别跟我说return *this返回当前对象,returnthis返回当前对象的克隆,return this返回当前对象的地址(指向当前对象的指针)。
正确答案为:return *this返回当前对象的克隆,returnthis返回当前对象的地址(指向当前对象的指针)。
令赋值(assignment)操作符返回一个referenceto *this。
【11】在operator=中处理“自我赋值” (Handle assignment to selfin operator=.)
下面是operator=实现代码,表面看起来合理,但自我赋值出现时并不安全。
Widget&Widget::operator = (const Widget& rhs) //一份不安全的operator=实现版本
{
delete pb; //停止使用当前的bitmap
pb = new Bitmap(*rhs.pb); //使用rhs’s bitmap的副本
return *this; //见【10】
}
这里的自我赋值问题是,operator=函数内的*this(赋值的目的端)和rhs有可能是同一个对象。果真如此delete就不只是销毁当前对象的bitmap,它也销毁rhs的bitmap。在函数末尾,Widget——它原本不该被自我赋值动作改变的——发现自己持有一个指针指向一个已经被删除的对象!
想要制止这种错误,传统的方法是借由operator=最前面的一个“证同测试”达到“自我赋值”的检验目的:
Widget&Widget::operator = (const Widget& rhs) {
if (this = &rhs) return *this; //identity test
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
这样行的通,但是韩式存在异常方面的麻烦。更明确的说,如果 new Bitmap导致异常(无论是分配时内存不足或是因为Bitmap的copy构造函数抛出异常),Widget最终会持有一个指针指向一块被删除的Bitmap。这样的指针有害。你无法安全地删除它们,甚至无法安全地读取它们。唯一能做的安全的事情是付出许多调试能力找出错误的起源。
以下的办法货物不是处理“自我赋值”最高效的办法,但它行得通。
Widget&Widget::operator = (const Widget& rhs) {
Bitmap* pOrig = pb; //记住原先的pb
pb = new Bitmap(*rhs.pb); //令pb指向*pb的一个复件
delete pOrig; //删除原先的pb
return *this;
}
确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
【12】复制对象时勿忘其每一个成分 (Copy all parts of an object.)
void logCall(const string& funcName);
class Customer {
public:
…
Customer (constCustomer& rhs);
Customer&operator=(const Customer& rhs);
…
private:
string name;
};
Customer::Customer (const Customrt& rhs) : name(rhs.name)
{
logCall(“Customer copyconstructor”);
}
Customer& Customer::operator=(const Customer& rhs)
{
logCall(“Customer copyassignment operator”);
name = rhs.name;
return *this;
}
这里的每一件事情看起来都很美好,而实际上每件事情也的确都好,直到另一个成员变量加入战局:
class Date { … };
class Customer {
public:
…
Customer (constCustomer& rhs);
Customer&operator=(const Customer& rhs);
…
private:
string name;
Date lastTransaction;
};
一旦发生继承,可能就造成此一主题最暗中肆虐的一个潜藏危机。请考虑:
class PriorityCustomer : public Customer { //一个derived class
public:
…
PriorityCustomer(constPriorityCustomer& rhs);
PriorityCustomer&operator= (const PriorityCustomer& rhs);
…
private:
int priority;
};
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: priority(rhs.priority)
{
logCall(“PriorityCustomercopy constructor”);
}
PriorityCustomer& PriorityCustomer::operator= (constPriorityCustomer& rhs)
{
logCall(“PriorityCustomercopy assignment operator”);
priority = rhs.priority;
return *this;
}
它们虽然复制了PriorityCustomer声明的成员变量,但是每个PriorityCustomer还内涵它所继承的Customer成员变量原件(副本),而那些成员变量未被复制。因此PriorityCustomer对象的Customer成分会被不带实参的Customer构造函数(即default构造函数)初始化。
你应该让derived class的copying函数调用相应的base class函数:
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs); //调用base class的copy构造函数
priority(rhs.priority)
{
logCall(“PriorityCustomercopy constructor”);
}
PriorityCustomer& PriorityCustomer::operator= (constPriorityCustomer& rhs)
{
logCall(“PriorityCustomercopy assignment operator”);
Customer::operator=(rhs); //对base class成分进行赋值操作
priority = rhs.priority;
return *this;
}
Copying函数应该确保复制“对象内的所有成员变量”以及“所有bass class成分”。
不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并且由两个copying函数共同调用。