第二章:构造/析构/赋值运算
条款05:了解c++默默编写并调用哪些函数(Know what functions c++ silently writes and calls)
我们写一个空class
class Empty{}; //空类
经过c++处理之后
class Empty{
public:
Empty() {...} //default构造函数
Empty(const Empty& rhs) {...} //copy构造函数
~Empty() {...} //析构函数
Empty& operator=(const Empty& rhs) {...} //copy assignment操作符
};
编译器会自动为class创建出上述函数(当这些函数被调用时),所以我们写出的这两个class是等效的。
Empty e1; //default构造函数
//析构函数
Empty e2(e1); //copy构造函数
e2=e1; //copy assignment操作符
但当我们声明了一个构造函数后,编译器就不再为class创建default构造函数
template<typename T>
class NameObject{
public:
NameObject(const char* name,const T& value);
NameObject(const std::string& name,const T& value);
//由于没有声明copy构造函数和copy assignment操作符,编译器会创建这些函数
...
private:
std::string nameValue;
T objectValue;
};
NameObject<int> no1("Smallest Prime Number",2);
NameObject<int> no2(no1); //调用copy构造函数->编译器生成的
nameValue的类型是string,而标准string有个copy构造函数,no2.nameValue就调用copy构造函数以no1.nameValue为实参,另一个NameObject::objectValue类型为int(内置类型),所以no2.objectValue会“拷贝no1.objectValue内的每一个bits”完成初始化。
编译器所生成的copy assignment操作符,其行为基本和copy构造函数类似
当base class把copy assignment操作符声明为private,编译器会拒绝为derived classes生成copy assignment操作符
请记住:
-编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符、以及析构函数
条款06:若不想使用编译器自动生成的函数,就该明确拒绝(Explicitly disallow the use of compiler-generated functions you do not want)
编译器产出的函数是public,当我们不需要copy构造函数、copy assignment操作符时,我们可以将它们声明为private,从而使得编译器不在声明这些函数,并且阻止别人调用它们。但是这对成员函数和友元函数而言却不还是安全!!
(c++ Prime Plus: 类的友元函数是非成员函数,其访问权限和成员函数相同(P392),访问私有类成员的唯一方法是使用类方法,c++使用友元函数来避开这种限制。(P421))
class HomeForSale{
public:
...
private:
...
HomeForSale(const HomeForSale&); //只有声明
HomeForSale& operator=(const HomeForSale&);
};
当member函数或friend函数调用private函数时,连接器会发出错误
class Uncopyable{
protected:
Uncopyable(){} //允许derived对象构造和析构
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&); //但阻止coping
Uncopyable& operator=(const Uncopyable&);
};
class HomeForSale:private Uncopyable{
...
};
当member函数或friend函数拷贝HomeForSale对象,编译器会拒绝,因为调用的是base class 的拷贝函数为private。
请记住:
-为驳回编译器自动提供的机能,可将对应的成员函数声明为private并且不与实现。或者使用Uncopyable这样的base class做法
条款07:为多基态类声明virtual析构函数(Declare destructors virtual in polymorphic base classes)
class TimeKeeper{
public:
TimeKeeper();
~TimeKeeper(); //析构函数
};
class AtomicClock:public TimeKeeper{...}; //derived class
class WaterClock: public TimeKeeper{...};
class WristWatch: public TimeKeeper{...};
TimeKeeper* ptk=getTimeKeeper();
...
delete ptk;
getTimeKeeper返回的指针指向一个derived class对象,而删除时却是被一个base class指针删除,当base class是一个non-virtual析构函数时,对象的derived成分没有被删除,且derived的析构函数也没有执行,但是其base class成分却被删除了,造成资源泄露!!!
class TimeKeeper{
public:
TimeKeeper();
virtual ~TimeKeeper(); //virtual 析构函数
};
base class带有virtual析构函数后,先调用derived对象的析构函数销毁derived对象,然后在调用base对象的析构函数。如果class不含virtual函数,最好不要把它用作一个base class,同时class不想成为base class,析构函数也最好不要为virtual。
当class内含至少一个virtual函数,才为它声明virtual析构函数
现在我们使用标准string
class SpeciaString:public std::string{ //继承标准string
...
};
SpeciaString* pss=new SpeciaString("Impending Doom");
std::string* ps;
...
ps=pss;
...
delete ps; //*ps的SpeciaString资源会泄漏,SpeciaString析构函数没有被调用!
因为std::string是个non-virtual析构函数,同样所以STL容器(vector、list、set、trl::unordered_map等等)都是不带virtual析构函数的class,所以最好不要继承标准容器或者其他带non-virtaul析构函数的class。
现在来看一看pure virtual析构函数:pure virtual函数会导致class成为一个abstract class也就是不能被实体化的class(c++ Prime Plus: 当类声明中包含纯虚函数时,则不能创建该类的对象,包含纯虚函数的类只用作基类(P509)),但是我们有时需要拥有abstract class,这时可以采用pure virtual析构函数。
class AWOV{
public:
virtual ~AWOV = 0; //声明pure virtual析构函数
};
AWOV::~AWOV() {} //pure virtual析构函数的定义
必须为pure virtual析构函数提供一份定义!!当AWOV的derived class的析构函数被调用时,会创建一个对~AWOV的调用。
请记住:
-polymorphic(带多态性质的)base class 应该声明一个virtual 析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
-classes的设计目的如果不是作为base class使用,或不是为具备多态性,就不要声明virtual析构函数
条款08:别让异常逃离析构函数(Prevent exceptions from leaving destructors)
析构函数可能会发生异常
现在我们假设用一个class连接数据库
class DBConnection{
public:
...
static DBConnection creat();
void close(); //关闭联机
};
//管理DBConnection
class DBConn{
public:
...
~DBConn(){
db.close();
}
private:
DBConnection db;
};
现在只要我们建立DBConnection对象交给DBConn管理,DBConn对象被销毁时自动调用close(),但是如果调用失败则会吐出异常,我们应该避免这个问题发生,这里有两个方法来避免。
1.如果close抛出异常就结束程序:
DBConn::~DBConn(){
try { db.close(); }
catch(...) {
... //制作运转记录,记下对close的调用失败
std::abort();
}
}
2.吞下调用close而发生的异常:
DBConn::~DBConn(){
try { db.close(); }
catch(...) {
... //制作运转记录,记下对close的调用失败
}
}
这些方法都无法对"导致close抛出异常"的情况做出反应。我们可以改进DBConn接口,让客户对问题做出反应
class DBConn{
public:
...
void close(){ //供客户使用的函数
db.close();
closed = true;
}
~DBConn(){
if(!closed){
try { db.close(); }
catch(...) {
... //制作运转记录,记下对close的调用失败
}
}
}
private:
DBConnection db;
bool closed;
};
把调用close的责任转移到客户手上,如果客户没有调用close(),当发生异常时客户就没有立场抱怨了,因为他们放弃第一时间处理问题。
请记住:
-析构函数绝对不要吐出异常。如果一个析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们或结束程序
-如果客户需要对某个操作函数运行期间抛出的异常作出反应,那么class应该提出供一个普通函数(不在析构函数中)执行该操作
条款09:绝不在构造和析构过程中调用virtual函数(Never call virtual functions during construction or destruction)
我们看一下这个代码
class Transaction{
public:
Transaction();
virtual void logTransaction() const =0; //纯虚函数,用作基类(条款07)
...
};
Transaction::Transaction(){ //base class构造函数定义
...
logTransaction(); //调用virtual函数
}
class BuyTransaction: public Transaction{ //derived class
public:
virtual void logTransaction() cosnt;
...
};
现在执行这条语句
BuyTransaction b;
BuyTransaction构造函数被调用,但是base class Transaction构造函数先被调用,而Transaction构造函数内却调用了virtual函数,此时调用logTransaction是在Transaction内,即使目前建立的对象是BuyTransaction即**derived class对象在base class构造期间,对象的类型是base class而不是derived class**
同理也适用于对于析构函数
对于解决方法有如下:
第一个解决方法是错误的,这里看看背后的原因:
class Transaction{
public:
Transaction() { init(); } //调用non-virtual函数
virtual void logTransaction() const =0; //纯虚函数,用作基类(条款07)
...
private:
void init(){
...
logTransaction(); //调用virtual函数
}
};
这个方法不会引发编译器和连接器的抱怨,但会建立一个derived class对象时调用错误版本的logTransaction。。因此,唯一的做法是构造函数和析构函数都没有调用virtual函数(对象被创建和销毁期间),而且它们调用的所有函数也都服从同一约束
另一种方法是将virtual改为non-virtual
(parameters->形参)
class Transaction{
public:
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo) const; //non-virtual函数
...
};
Transaction::Transaction(){
...
logTransaction(logInfo); //调用non-virtual函数
}
class BuyTransaction: public Transaction{ //derived class
public:
BuyTransaction(parameters)
:Transaction(creatLogString (parameters)){...} //将信息传给base class构造函数
...
private:
static std::string creatLogString (parameters);
};
我们无法使用virtual函数从base classes向下调用,但在构造期间,我们可以令derived classes将构造信息向上传递至base class构造函数
请记住:
在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)
条款10:令operator=返回一个 reference to *this(Have assignment operators return a reference to *this)
这篇比较短简单看下
有如下赋值:
int x,y,z;
x=y=z=15;
赋值采用右结合
x=(y=(z=15)); //z=15,然后y=z,然后x=y
连锁赋值中赋值操作符必须返回一个reference指向操作符左侧实参
class Widget{
public:
...
Widget& operator=(const Widget& rhs){ //返回类型是一个reference
...
return *this; //返回左侧对象
}
...
};
同样,这也适用于+=、-=、*=等等(最好遵守,这份协议被内置类型和标准程序库共同遵守)
请记住:
-令赋值操作符返回一个reference to *this
条款11:在operator=中处理“自我赋值”(Handle assignment to self in operator=)
防止指向同一份资源的两个对象在被删除时掉入"在停止使用资源之前意外释放了它"
class Bitmap {...};
class Widget{
public:
...
private:
Bitmap* pb; //指向一个从heap分配而得到的对象
};
第一个写法:*this和rhs可能指向同一个对象
Widget& Widget::operator=(const Widget& rhs){
delete pb;
pb=new Bitmap(*rhs.pb);
return *this;
}
第二个写法:"new Bitmap"可能导致异常,Widget会持有一个指针指向一块被删除的Bitmap
Widget& Widget::operator=(const Widget& rhs){
if(this=&rhs) //证同测试
return *this;
delete pb;
pb=new Bitmap(*rhs.pb); //"new Bitmap"可能导致异常
return *this;
}
第三个写法:通过 “异常安全性” 获得 "自我赋值安全"
Widget& Widget::operator=(const Widget& rhs){
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);
...
};
Widget& Widget::operator=(const Widget& rhs){ //pass by reference
Widget temp(rhs); //将rhs数据制作一份副本
swap(temp); //将*this数据和上述副本数据交换
return *this;
}
/*
Widget& Widget::operator=(Widget rhs){ //rhs是被传对象的一个副本(pass by value)
swap(temp); //将*this数据和上述副本数据交换
return *this;
}
*/
请记住:
-确保当对象自我赋值时operator=有良好行为。其中技术包括“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap
-确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确
条款12:复制对象时勿忘其每一个成分(Copy all parts of an object)
copy函数:copy构造函数和copy assignment操作符
void logCall(const std::string funcName);
class Customer{
public:
...
Customer(const Customer& rhs);
Customer& operator=(const Customer& rhs);
...
private:
std::string name;
};
Customer::Customer(const Customer& rhs):name(rhs.name){
logCall("Customer copy constructor");
}
Customer& Customer::operator=(const Customer& rhs){
logCall("Customer copy assignment operator");
name=rhs.name;
return *this;
}
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;
}
PriorityCustomer的copying函数只复制了PriorityCustomer声明的成员变量,而没有复制所继承的Customer成员变量
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs):Customer(rhs),priority(rhs.priority){ //调用base class的copy构造函数
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;
}
让derived class的copying函数调用相应的base class函数
copy assignment操作符调用copy构造函数不合理,且copy构造函数调用copy assignment操作符也没有意义
可以创建一个名为init函数在private中给copying函数调用
请记住:
-Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”
-不要尝试以某一个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用