文章目录
二、构造/析构/赋值运算
条款05:了解C++默默编写并调用哪些函数
对一个空类,C++会为它声明四个函数(public且inline):
- 一个copy构造函数
- 一个copy assignment操作符
- 一个析构函数
- 如果没声明构造函数,则会声明一个default构造函数。
近似于如下类,当有相关需求时,这些函数才会被编译器创造出来
class Empty{
public:
Empty(){...}//Empty e1;
Empty(const Empty& rhs){...}//Empty e2(e1);
~Empty(){...}//自动生成的虚构函数非虚函数
Empty& operator=(const Empty& rhs){...}//e2=e1;
};
在特殊情况下,编译器拒绝为class生成operator=
如:
-
在C++中禁止reference改指向不同对象,如果出现相关赋值操作,C++将拒绝编译赋值操作。
-
面对含const成员,出现赋值操作,拒绝编译。
-
base class将copy assignment声明为private,编译器拒绝为derived class生成copy assignment操作符。(派生类一定要能处理基类啊)
条款06:若不想使用编译器自动生成的函数,就该明确拒绝
将copy构造函数和copy assignment操作符声明为private。但member函数和friend函数还可调用private函数,一般这样调用连接器会发出一个连接错误(linkage error)。
class HomeForSale{
public:
...;
private:
HomeForSale(const HomeForSale&);//没必要写参数名
HomeForSale& operator=(const HomeForSale&);
...;
};
程序通过预处理、编译、汇编及链接完成,链接阶段的错误可能会移植编译阶段(好事儿)
解决方案:使用Uncopyable类来实现也是一种方法。
class Uncopyable{
protected:
Uncopyable(){}
~Uncopyable(){}//这里的析构函数不一定virtual(条款7)
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};
class HomeForSale:private Uncopyable{...};//不一定以public继承(条款32和39)
//Boost(条款55)中有class noncopyable,也可以使用。
通过member或friend函数调用,编译器会试着生成copy构造函数和copy assignment操作符,会调用base class对应函数,由于Uncopyable对应函数是private,调用被拒绝。
条款07:为多态基类声明virtual析构函数
条款13:依赖用户执行delete操作,基本上便带有某种错误倾向。
条款18中有提到factory函数如何修改,避免/预防客户错误。
纵使都做对了,可能还会出错。
出错原因:当base class指针指向derived class对象,且基类虚构函数non-virtual,通过base class指针delete将有derived成员未销毁导致内存泄漏。
class TimeKeeper{
public:
TimeKeeper();
~TimeKeeper();//不加virtual将会导致问题
...;
};
class WristWatch:public TimeKeeper{...};
TimeKeeper* ptk = getTimeKeeper();
...;
delete ptk;//导致派生类中的成员未被销毁
解决方案:为多态基类声明virtual析构函数。
任何class只要带virtual函数都几乎确定应该也有一个virtual析构函数(含多态性质);
如果class不含virtual函数,通常表示它并不意图被用作一个base class。如果有类强行将其作为base class,C++没有像Java类似的final class禁止派生机制;
如果随意将虚构函数设为virtual,会多出一个虚函数表指针(virtual table pointer)指向虚函数表(virtual table)且虚函数表指针在其他语言中没有对应物,不利于移植;
为抽象类设置纯虚类:
class AMOV{
public:
virtual ~AMOV()=0;
};
AMOV::AMOV(){}
构造时,从base class到derived class;
析构时,从derived class到base class。
条款08:别让异常逃离虚构函数
当异常在析构时抛出,将会导致结束执行或不明确行为。
class DBConnection{
public:
...;
static DBConnection create();
void close();
};
class DBConn{
public:
...;
~DBConn(){db.close();}
private:DBConnection db;
};
{
DBConn dbc(DBConnection::create());
...;
}//区块结束时,DBConn被销毁且自动调用DBConnection的close()
//如在析构函数抛出异常,该异常将会离开该析构函数且难以驾驭。
解决方案:
-
close()抛出异常即结束程序
DBConn::~DBConn(){ try{db.close();} catch(...){ 记录运转过程,记下close调用失败; std::abort(); } }
-
close()抛出异常就吞下异常
DBConn::~DBConn(){ try{db.close();} catch(...){ 记录运转过程,记下close调用失败; //后序程序需保证在有错误的前提下还能可靠运行 } }
-
重新设计接口
class DBConn{ public: ...; void close(){db.close();closed=true;} ~DBConn(){ if(!closed){ try{db.close();} catch(...){//如果失败,回到上两种方法 记录运转过程,记下close调用失败; ...; } } } private: DBConnection db; bool closed; };
这种设计给程序员一个处理错误的机会;反之,无机会响应。
条款09:绝不在构造和析构过程中调用virtual函数
原因:
使用构造函数,当base class构造期间virtual函数绝不会下降到derived class阶层;相反,会直接使用base class的版本。
如果调用derived class版本,必然会使用local成员变量,但是他们都还没完成初始化,这会造成不明确的行为。
而且,这样调用编译器将其视为base class且运行期类型信息(runtime type information,如dynamic_cast及typeid)将其视为base class。
使用析构函数同理,derived class析构后,base class析构函数内的对象,将视为base class。
例:如下函数虽然在构造函数中没有调用virtual函数且能通过编译器与连接器,但是调用的non-virtual函数调用了virtual函数。
class Transform{
public:
Transform(){init();}//non-virtual
virtual void logTransform() const=0;...;
private:
void int(){...;logTransform();//virtual}
};
解决方案:无法用virtual函数从base class向下调用,在base class使用non-virtual版成员函数替代virtual函数,将derived class构造函数必要信息向上传递给base class构造函数。
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(para): Transaction(createLogString(para)){...;}//log信息传给base class构造函数
...;
private:
static std::string createLogString(para);//避免调用未初始化的derived class对象成员变量
}
条款10:令operator=返回一个reference to *this
这能让自定义类型也能像内置类型一样能进行连续赋值
int x,y,z;
x=y=z=10;
//满足右结合律,等效于如下:
(x=(y=(z=10)));
为了实现连续赋值,必须返回一个reference指向操作符的左侧实参。
class Widget{
public:
...;
Widget& operator=(const Widget& rhs){...;return *this;}
//同理,适用于所有赋值运算
Widget& operator+=(const Widget& rhs){...;return *this;}
};
条款11:在operator=中处理“自我赋值”
会有不注意的自我赋值,当有reference或pointer指向同一对象时,这就是需要处理的。来自同一继承体系,甚至不需要同类型也会造成自我赋值。
如果能运用对象来管理资源(条款13和14)且copy时有正确举措,自我赋值不用额外操心。自行管理资源,要避免停止使用前释放掉。
class Bitmap{...;};
class Widget{...;
private:Bitmap* pb;
};
Widget& Widget::operator=(const Widget& rhs){
delete pb;//如pb和this指向同一对象,会导致错误删除对象
pb=new Bitmap(*rhs.pb);//如果new时抛出异常,指针pb会指向一个被删除的Bitmap(不具备“异常安全性”)
return *this;
}
通常,operator=具备“异常安全性”也会具有“自我赋值安全性”,因此精心编排语句保证“异常安全性”即可。
异常安全性:遇到异常时1.不泄漏任何资源;2.不允许破坏数据。
Widget& Widget::operator=(const Widget& rhs){
if(this==&rhs) return *this;//验同测试,加入会导致效率降低,且相同的概率不高可删除
Bitmap* pOrig=pb;
pb=new Bitmap(*rhs.pb);
delete pOrig;//先复制,后删除
return *this;
}
也可使用**copy and swap技术(条款29)**与“异常安全性”紧密相关。
void Widget::swap(Widget& rhs){...;}
Widget& Widget::operator=(const Widget& rhs){
Widget tmp(rhs);//复制
swap(tmp);//交换
return *this;
}//如果该函数rhs为non-const,代码可更加简化,但根据之前的条款不太可取。
条款12:复制对象时勿忘其每一个成分
面向对象系统只保留两个函数赋值复制(copy构造函数和copy assignment操作符),通常编译器会帮你定义。如果你自己写了且忘了复制某一成分,编译器是不会提示你的。
如果添加成员变量,必须同步修改copying函数、构造函数(条款4和45)及非标准operator=函数。
任何时候,只要写了derived class的copying函数,必须复制其base class成分。往往private成员无法直接访问,需先访问相应的base class函数:
class Data{...;};
class Customer{
public:
Customer(const Customer& rhs);
Customer& operator=(const Customer& rhs);
...;
private:
std::string name;
Date lastTransaction;
}
class PriorityCustomer:public Customer{
public:
PriorityCustomer(const PriorityCustomer& rhs);
PriorityCustomer& operator=(const PriorityCustomer& rhs);
...;
private:int priority;
};
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;
}
-
不该令copy assignment操作符调用copy构造函数,这就像构造一个已经存在的对象;
-
不该令copy构造函数调用copy assignment操作符,assignment操作符只用于已初始化的对象上;
-
如发现copy构造函数和copy assignment操作符代码相近,可使用新成员函数供两者调用,常为private且命名为init()。