Effective C++第二章笔记

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

条款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()

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值