第二章、构造/析构/赋值运算

条款05、了解C++默默编写并调用哪些函数

什么时候empty class(空类)不再是个empty class呢?当C++ 处理过它之后。是的,如果你自己没有声明,编译器就会为它声明(编译器版本的)一个copy析构函数、一个copy assignment操作符和一个析构函数。此外如果你没有声明任何构造函数,编译器也会为你声明一个default析构函数。所有这些函数都是public和inline的。因此,如果你写下:

class Empty{ };

这就好像你写下这样的代码:

class Empty{
public:
    Empty(){...}                    //default 构造函数
    Empty(const Empty & rhs){...}   // copy 析构函数
    ~Empty(){...}                   //析构函数,是否该是virtual 间稍后说明
    Empty& operator=(const Empty &rhs) {...}  //copy assignment 操作符
};

唯有当这些函数被需要(被调用),它们才会被编译器创建出来。程序中需要它们是很平常的事情。下面代码造成上述每一个函数被编译器产出:

Empty e1;		//default 析构函数
				//析构函数
Empty e2(e1);		//copy 构造函数
e2=e1;               //copy assignment 操作符			

至于copy 构造函数和copy assignment 操作符,编译器创建的版本只是单纯地将来源对象的每一个non-static成员变量拷贝到目标对象。

举个例子,假设NameObject定义如下,其中nameValue是个reference to string,objectValue是个const T:

template<class T>
class NameObject{
public:
    //以下构造函数如今不再接受一个const 名称,因为nameValue
    //如今是个reference-to-non-const string。先前那个char* 析构函数
    //已经过去了,因为必须有个string 可供指涉。
    NameObject(std::string* name,const T& value);
    ...         //如前,假设并未声明operator=
private:
    std::string& nameValue;//这如今是个reference
    const T objectValue;//这如今是个const    
};

现在考虑下面会发生什么事:

std::string newDog("Persephone");//
std::string oldDog("Satch");
NameObject<int> p(newDog,2);//当初撰写至此,我们的狗Persephone
                            //即将度过其第二个生日
NameObject<int> s(oldDog,36);//我小时候养的狗Satch则是36岁,--如果它还活着
p=s;        //现在p的成员变量该发生什么事

赋值之前,不论p.nameValue和s.nameValue都指向string对象(当然不是同一个)。赋值动作该如何影响p.nameValuep呢?赋值之后p.nameValue应该指向s.nameValue所指向的那个string吗?也就是说reference自身可以被改动吗?如果是,那就可以开辟新天地,因为C++并不允许“让reference改指向不同对象”。换一个想法,p.nameValue所指的那个string对象 该被修改,进而影响“持有pointers或reference而且指向该string”的其他对象吗?也就是对象不被直接牵扯到赋值操作内?编译器生成copy assignment究竟该怎么做呢?
面对这个难题,C++的响应是拒绝编译那一行赋值动作。如果你打算在一个“内含reference成员”的class内支持赋值操作(assignment),你必须自己定义copy assinment操作符。如果某个base classes将copy assignment操作符声明为private,编译器将拒绝为其derived classes (派生类)生成一个copy assignment操作符。

请记住:
1、编译器可以暗自为class创建default析构函数、copy构造函数、copy assignment(拷贝赋值)操作符,以及析构函数。

条款06、若不想使用编译器自动生成的函数,就该明确拒绝

如果有些场景需要禁止copying动作,就要禁用用户使用copy构造函数和copy assignment操作符。但是如果你不声明copy构造函数或copy assignment操作符,编译器可能为你产出一份,于是你的class支持copying。如果你声明它们,你的class还是支持copying。但这里的目标却是要阻止copying!

答案的关键是,所有编译器产出的函数都是public。为阻止这些函数被创建出来,你需要自行声明它们,但这里并没有什么需求是你必须将它们声明为public。因此你可以将copy构造函数或copy assignment操作符声明为private。藉由明确声明一个成员函数,你阻止了编译器暗自创建其专属版本:而令这些函数为private,使你得以成功阻止人们调用它。

以下是一般做法:
这种做法并不绝对安全,因为成员函数和友元函数还是可以调用你的private函数。

class HomeForSale {
public:
    ...
private:
    ...
    HomeForSale(const HomeForSale&);只有声明
    HomeForSale& operator = (const HomeForSale&)   
};

有了上述class定义,当客户企图拷贝HomeForSale对象,编译器会阻挠他。如果你不慎在成员函数或友元函数之内那么做,轮到链接器发出抱怨。

将链接期错误移至编译器是可能的,只要将copy构造函数和copy assignment操作符声明为private就可以办到,但不是在HomeForSale自身,而是在一个专门为了阻止copying动作而设计的base class内。这个base class非常简单:

class Uncopyable{
protected:
    Uncopyable(){}  //允许devived 对象构造和析构
    ~Uncopyable(){}
    
private:
    Uncopyable(const Uncopyable&);//但阻止copying
    Uncopyable & operator=(const Uncopyable&);    
};

为求阻止HomeForSale对象被拷贝,我们唯一需要做的就是继承Uncopyable:

class HomeForSale:private Uncopyable//class 不再声明copy构造函数或copy assign. 操作符
{
    ...
};

请记住:
1、为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable也是一种做法。

条款07、为多态基类声明virtual析构函数

有许多种做法可以记录时间,因此,设计一个TimeKeeper base class和一些derived classed 作为不同的计时方法,相当合情合理:

class TimeKeeper{
public:
    TimeKeeper();
    ~TimeKeeper();
    ...
    
};
class AtomicClock:public TimeKeeper{...};//原子钟
class WaterClock:public TimeKeeper{...};//水钟
class WristClock:public TimeKeeper{...};//腕表

许多客户只想在程序中使用时间,不想操心时间如何计算等细节,这时候我们可以设计factory(工厂)函数,返回指针指向一个计时对象。Factory函数会“返回一个base class指针,指向新生成之derived class 对象”:

TimeKeeper * getTimeKeeper();//返回一个指针,指向一个TimeKeeper派生类的动态分配对象。

为了遵守factory函数的规矩,被getTimeKeeper()返回的对象必须位于heap。因此为了避免泄露内存和其他资源,将factory函数返回的每一个对象适当地delete掉很重要:

TimeKeeper *ptk= getTimeKeeper();///从TimeKeeper继承体系获得一个动态分配对象。
...			//运用它...
delete ptk;//释放它,避免资源浪费

依赖客户执行delete动作,基本上便带有某种错误倾向。
问题出在getTimeKeeper返回的指针指向一个derived class 对象,而那个对象却经由一个base class指针被删除,而目前的base class有个non-virtual析构函数。 C++指出当devired class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义——实际执行时通常发生的是对象的derived成分没有被销毁。这属于“局部销毁”对象。会造成资源泄漏、败坏之数据结构、在调试器上浪费许多时间的绝佳途径。

消除这个问题的做法很简单:给base class一个简单virtual析构函数。此后删除derived class对象就会如你想要的那般。是的,它会销毁整个对象,包括所有的derived class成分:

class TimeKeeper{
public:
    TimeKeeper();
    virtual ~TimeKeeper();
    ...
    
};

TimeKeeper *ptk= getTimeKeeper();
...			//运用它...
delete ptk;//现在,行为正确

像TimeKeeper 这样的base class除了析构函数之外通常还有其他virtual函数,因为virtual函数的目的不允许derived class的实现得以客制化。例如TimeKeeper就可能拥有一个virtual gerCurrentTime,它在不同的derived classes中有不同的实现码。任何class 只要带有virtual函数都几乎确定应该也有一个virtual 析构函数。
如果class 不含virtual 函数,通常表示它并不意图被用作一个base class .。当class 不企图被当作base class,令其析构函数为virtual往往是个馊主意。

只有当class内含至少一个virtual函数,才为它声明virtual析构函数。

class SpecialString:public std::string{//馊主意
    ...
};

如果你在程序某处将一个pointer-to-SpecialString转换成std::string ,然后将转换的那个string 指针delete掉,你立刻被流放到“行为不明确”的恶地上:

SpecialString *pss=new SpecialString ("Impending Doom");
std::string *ps'
...
ps=pss;// SpecialString * => std::string
...
delete ps;//未有定义!现实中*ps的SpecialString 资源会泄漏,
                //因为SpecialString 析构函数没有被调用

相同的分析适用于任何不带virtual析构函数的class,包括所有的STL容器如vector、list、set、tr1::unordered_map。

为你希望它成为抽象的那个class声明一个pure virtual析构函数。下面是个例子:

class AWOV{
public:
    virtual    ~AWOV()=0;//声明pure virtual析构函数
};

这个class有个prue virtual函数,所以它是一个抽象class,又由于它有个virtual析构函数,所以你不需要担心析构函数的问题。然而,这里有个窍门:你必须为这个pure virtual析构函数提供一个定义:

AWOV::~AWOV(){ }//prue virtual析构函数的定义

析构函数的运作方式是,最深层派生的那个class其析构函数最先被调用,然后是其每一个base class的析构函数被调用。

请记住:
1、polymorphic(带多态性质的)base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
2、Classes 的设计目的如果不是作为base classes 使用,或不是为了具备多态性(polymorphically ),就不该声明virtual 析构函数。

条款08、别让异常逃离析构函数

这就很容易理解,但如果你的析构函数必须执行一个动作,而该动作可能会在失败时抛出异常,该怎么办?举个例子,假设你一个class负责数据库连接:

class DBConnection{
public:
    ...
    static DBConnection create();//这个函数返回DBConnection对象;
        //为求简化暂略参数。
    void close();    //关闭联机;失败则抛出异常
};

为了确保客户不忘记DBConnection 对象身上调用close(),一个合理的想法是创建一个用来管理DBConnection资源的class,并在其析构函数中调用close。这一类用于资源管理的class在第三章讨论,这里只要考虑它们的析构函数长相就够了:

class DBConn{//这个class 用来管理DBConnection对象
public:
    ...
    DBConn(){
        db.close();
    }
private: 
   DBConnection db;     
    
};

就便允许客户写出这样的代码:

{//开启一个区块(block)
    DBConn dbc(DBConnection::create());  
     //建立DBConnection对象并
    //交给DBConn对象以便管理。通过DBConn的接口使用DBConnection对象。
}//在区块结束点,DBConn对象被销毁,因而自动为DBConnection对象调用close

只要调用close成功,一切都美好。但是如果该调用导致异常,DBConn析构函数会传播该异常,也就是允许它离开这个析构函数。那就会造成问题,因为那就是抛出了难以驾驭的麻烦。

一个较佳策略是重新设计DBConn接口,使其客户有机会对可能出现的问题作出反应。例如DBConn自己可以提供一个close函数,因而赋予客户一个机会得以处理“因该操作而发生的异常”。DBConn也可以追踪其所管理之DBConnection是否已被关闭,并在答案为否的情况下由其析构函数关闭之。这可以防止遗失数据库连接。
然而如果DBConnection析构函数调用close失败,我们又将退回“强迫结束程序”或“吞下异常”的老路:

class DBConn{//这个class 用来管理DBConnection对象
public:      
    ...
    void close(){//供客户使用的新函数
        db.close();
        closed=true;
    }
    ~DBConn(){
        if(!closed){
            try {//关闭连接(如果客户不那么做的话)
                db.close();
            } catch (...) {//如果关闭动作失败
                制作运转记录,记下对close的调用失败;//记录下来并结束程序
                        ...//或吞下异常
            }
        }    
    }
private: 
    DBConnection db;     
    bool closed;    
};   

本例要说明的是,由客户自己调用close并不会对他们带来负担,而是给他们一个处理错误的机会,否则他们没有机会响应。如果他们不认为这个机会有用(或许他们坚信不会有错误发生),可以忽略它,倚赖DBConn析构函数去调用close。如果真有错误发生——如果close的确抛出异常——而且DBConn吞下该异常或者结束程序,客户没有立场抱怨,毕竟他们曾有机会第一手处理问题,而他们选择放弃。

请记住:
1、析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束异常。
2、如果客户想要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行改该操作。

条款09:绝不在构造函数和析构函数过程中调用virtual函数

假设你有一个继承体系,用来塑模股市交易如买进、卖出的订单等等。这样的交易一定要经过审计,所以每当创建一个交易对象,在审计日志中也需要创建一笔适当记录。下面是一个看起来颇为合理的做法:

class Transaction{//所有交易的base class
public:
    Transaction();
    virtual void logTransaction() const =0;//做出一份因类型不同而不同的日志记录
    ...
};

Transaction::Transaction(){//base class 构造函数之实现
    ...
    logTransaction();//最后动作是志记这笔交易
}
class BuyTransaction:public Transaction(){//derived class
    public:
    virtual void logTransaction() const;//志记(log)此型交易
   ...
};

class SelfTransaction:public Transaction(){//derived class
    public:
    virtual void logTransaction() const;//志记(log)此型交易
   ...
};

现在,当以下这行被执行,会发生什么事情:

BuyTransaction b;

无疑地会有一个BuyTransaction构造函数被调用,但首先Transaction构造函数一定会更早被调用;是的,derived class对象内的base class成分会在derived class自身成分被构造之前先构造妥当。Transaction构造函数的最后一行调用virtual 函数logTransaction,这正是引发惊奇的起点。这时候调用的logTransaction是Transaction内的版本,不是BuyTransaction内的版本——即使目前即将建立的对象类型是BuyTransaction。是的,base class 构造期间virtual函数绝不会下降到derived class阶层。取而代之的是,对象的作为就像隶属base 类型一样。非正式的说法或者比较恰当:在base class构造期间,virtual 函数不是virtual函数。

确定你的构造函数和析构函数都没有调用virtual 函数,而他们调用的所有函数也都服从同一约束。
一种做法是class Transaction内将logTransaction函数改为non-virtual ,然后要求derived class构造函数传递必要的信息给Transaction构造函数,而后那个构造函数便可安全地调用non-virtual logTransaction。像这样:

class Transaction{//所有交易的base class
public:
    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(){//derived class
    public:
    BuyTransaction(parameters):Transaction(parameters)){//将log信息传给base  class 构造函数
        ...
    }

   ...
    private:
    static std::string createLogString(parameters);
};

由于无法使用virtual函数从base classes向下调用,在构造期间,你可以藉由“令 derived classed 将必要的构造信息向上传递至base class构造函数”替换之而加以弥补

请记住:
1、在析构和构造函数期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)

条款10:令operator= 返回一个reference to *this

关于赋值,有趣的是你可以把它们写成连锁形式:

int x,y ,z;
x=y=z=15 ;//赋值连锁形式

同样有趣的是,赋值采用右结合律,所以上述连锁赋值被解析为:

x=(y= (z= 15));

这里15先被赋值给z,然后其结果再次被赋值给y,然后其结果再被赋值给x。

为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参。这是你为classes实现赋值操作符时应该遵循的协议:

class Widget{//返回类型是个reference,指向当前对象。
public:
    ...
    Widget &operator=(const Widget& rhs){
        ...
        return *this;   //返回左侧对象
    }
    ...
};

这个协议不仅适用于以上的标准赋值形式,也适用于所有赋值相关运算,例如:

class Widget{
public:
    ...
    Widget &operator+=(const Widget& rhs){//这个协议适用于+= , -=,*=,等等
        ...
        return *this;   
    }
    Widget &operator=(int rhs){//此函数也适用,即使此一操作符的参数类型不符合协定
        ...
        return *this;
    }
    ...
};

这个协议被所有内置类型和标准库提供的类型如string,vector,complex,trl::shared_ptr或即将提供的类型共同遵守。

请记住:
令赋值(assignment)操作符返回一个reference to *this。

条款11、在operator= 中处理“自我赋值”

假设你建立一个class用来保存一个指针指向一块动态分配的位图(bitmap):

class Bitmap{ ... };
class Widget{
	... 
private:
    Bitmap *pb;//指针,指向一个从heap分配而得的对象
};

下面是operator=实现代码,表面上看起来合理,但自我赋值出现时并不安全(它也不具备异常安全性,但我们稍后才讨论这个主题)。

Widget &Widget::operator=(const Widget & rhs)//一份不安全的operator=实现版本
{
    delete pb;//停止使用当前的bitmap,
    pb=new Bitmap (*rhs.pb);//使用rhs's bitmap的副本(复件)。
    return *this;
}

这里的自我赋值问题是,operator = 函数内的 *this和 rhs有可能是同一个对象。果真如此delete就只不过是销毁当前对象的bitmap,它也销毁rhs的bitmap。在函数末尾,Widget——它原本不该被自我赋值动作改变的——发现自己持有一个指针指向一个已被删除的对象。

欲阻止这种错误,传统做法是藉由operator=最前面的一个“证同测试”达到“自我赋值”的检验目的:

Widget &Widget::operator=(const Widget & rhs)//
{
    if(this == &this) return *this;//证同测试,如果是自我赋值,就不做任何事情。
    
    delete pb;//停止使用当前的bitmap,
    pb=new Bitmap (*rhs.pb);//使用rhs's bitmap的副本(复件)。
    return *this;
}

这个版本仍然存在异常方面的麻烦。更明确地说,如果“new Bitmap”导致异常(不论是因为分配时内存不足或者因为Bitmap的copy构造函数抛出异常),Widget最终会持有一个指针指向一块被删除的Bitmap。这样的指针有害。你无法安全地删除它们,甚至无法安全地读取它们。唯一能对它们做的安全事情是付出许多调试能量找出错误的起源。

许多人对“自我赋值”的处理态度是倾向于不管“异常安全性”。例如以下代码,我们只需要注意复制pb所指东西之前别删除pb:

Widget &Widget::operator=(const Widget & rhs)//
{
 
    Bitmap* pOrig =pb; //记住原先的pb
  
    pb=new Bitmap (*rhs.pb);//令pb指向 *pb的一个复件(副本)
    delete pOrig;//删除原先的pb
    return *this;
}

现在,如果“new Bitmap”抛出异常,pb(及其栖身的那个Widget)保持原状。即使没有证同测试,这段代码还是能够处理自我赋值,因为我们对原bitmap做了一份复件、删除原bitmap、然后指向新制造的那个复件。它或许不是处理“自我赋值”的最高效办法,但它行得通。

在operato=函数内手工排列语句(确保代码不但”异常安全“而且“自我赋值安全”)的一个替代方案是,使用所谓的copy and swap技术。这个技术和”异常安全性“有密切关系。然而由于它是一个常见而够好的operator=撰写办法,所以值得看看这种方法:

class Widget{
    ...
    void swap(Widget& rhs);//交换*this 和rhs的数据
    
};
Widget &Widget::operator=(const Widget & rhs)//
{
    Widget temp(rhs);//为rhs数据制作一份复件(副本)
    swap(temp);  //将*this 数据和上述复件的数据交换
    return *this;
}

这个主题的另一个变形乃利用以下事实:(1)某class的copy assignment操作符可能被声明为” 以By value方式接受实参“;(2)以by value方式传递东西会造成一份复件/副本:

Widget &Widget::operator=( Widget  rhs)//
{
    //rhs是被传对象的一份复件(副本),注意这里是pass By value
    swap(rhs);  //将*this 数据和复件(副本)的数据交换
    return *this;
}

请记住:
1、确保当对象自我赋值时operator = 有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
2、确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

条款12:复制对象时勿忘其每一个成分


void logCall(const std::string &funcName);//制造一个log entry
class Date{};//日期


class Customer{
public:
    Customer(const Customer&rhs);
    Customer& operator=(const Customer& rhs);
private:
    std::string name;
    Date lastTransaction;
};
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;
}


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)
{
    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;
}

PriorityCustomer是derived class,它的copying函数调用相应base class函数:

” Customer::operator=(rhs);//对base class 成分进行赋值动作“。
当你编写一个copying函数,请确保(1)复制所有local成员变量,(2)调用所有base classes内的适当的copying函数。

你不该令copy assignment操作符调用copy构造函数。反方向——令copy构造函数调用copy assignment操作符——同样无意义。构造函数用来初始化新对象,而assignment操作符只施行于已初始化对象身上。

如果你发现你的copy构造函数和copy assignment 操作符有相近的代码,消除重复代码的做法是,建立一个新的成员函数给两者调用。这样的函数往往是private而且常被命名为init。这个策略可以安全消除copying构造函数和copy assignment 操作符之间的代码重复。

请记住:
1、Copying 函数应该确保复制”对象内的所有成员变量“及”所有base class成分“
2、不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。

来源:Effective C++
仅供学习 ,侵删

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值