Effective C++ 笔记①

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

了解C++默默编写并调用哪些函数

在C++处理过一个空类之后,它就不是个空类了。如果你没有生硬,编译器就会自动声明一个复制构造函数,一个复制分配操作符和一个析构函数;并且,如果没有构造函数,编译器也会自动声明一个默认构造函数。所有的这些函数都是public且inline。

如果写下了

class Empty{};

编译器自动帮你:

class Empty{
public:
    Empty(){...}   
    Empty(const Empty& rhs){...}
    ~Empty(){...}

    Empty& operator=(const Empty& rhs){...}
};

同时,在没有定义赋值运算符的类中,改变引用对象的值是错误的。

template<class T>
class NamedObject{
public:
    //以下构造函数如今不再接受一个const名称,因为nameValue
    //如今是个reference-to-non-const string
    Namedobject(std::string& name,const T& value);
    ...
private:
    std::string& nameValue;
    const T objectValue;
}

std::string newDog("Persephone");
std::string oldDog("Satch");
NamedObject<int> p(newDog,2);
NamedObject<int> s(oldDog.36);
p = s;

上述的对象赋值操作是错误的;因为类中的string类型是引用类型,而“p = s”操作试图改变一个引用类型的值,这在C++中是不允许的,因此C++的响应是拒绝编译那一行赋值动作;如果打算在一个拥有引用成员的类内支持赋值操作,必须自行定义赋值操作符。

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

房地产中介会说,每一笔资产都是独一无二的,没有两笔完全相像。我们也认为怎么能够复制独一无二的东西呢?我们很乐意看到HomeForSale的对象拷贝动作以失败收场。

class HomeForSale{...};

HomeForSale h1;
HomeForSale h2;
HomeForSale h3(h1);

h1 = h2;

但实际上,上述操作是可以实现的。尽管在类中没有定义赋值操作符和拷贝构造函数,在编译时,编译器也会自动生成这类的函数,但在我们实际应用中是禁止该类操作进行的。

其中的一个解决办法就是将赋值构造函数和赋值操作符在private中声明,即:

class HomeForSale{
public:
    ...
private:
    ...
    HomeForSale(const HomeForSale&);
    HomeForSale& operator=(const HomeForSale&);
}

【注】此时成员函数和友元函数能够访问private内的两个函数,我们应该尽量避免,实在避免不了就轮到连接器发出抱怨了。

将连接期错误移至编译期是可能的,只要将拷贝构造函数和赋值操作符声明为private就可以了,但是是在一个专门为了阻止拷贝动作而设计的基类中。

class Uncopyable{
protected:
    Uncopyable(){}
    ~Uncopyable(){}
private:
    Uncopyable(const Uncopyable&);
    Uncopyable& operator=(const Uncopyable&);
}

class HomeForSale:private Uncopyable{
    ...
};

 

为多态基类声明virtual析构函数

基类中若不声明析构函数为virtual,则在派生类程序执行完毕后,无法执行派生类的析构函数。

欲实现virtual函数,对象必须携带某种信息,主要用来在运行期决定哪一个virtual函数该被调用。这份信息通常由vptr(虚函数表指针)指出。vptr指向一个由函数指针指向的数组,称为vtbl(虚函数表):每一个带有virtual函数的class函数都有一个相应的vtbl。当对象调用某一virtual函数,实际被调用函数取决于该对象的vptr所指的那个vtbl。

【注】不能一股脑儿的将所有析构函数定义为virtual,也不能完全不声明为virtual。

别让异常逃离析构函数

class Widget{
public:
    ...
    ~Widget(){...}           //假设这个可能吐出一个异常
};

void dosomething(){
    std::vector<Widget> v;
    ...
}                            //这里v会被自动销毁

当vector被销毁时,他应该销毁存储其中的所有Widget。假设总共有十个Widget,在销毁第一个时抛出了一个异常,在销毁第二个时也抛出了异常,此时对于C++来说异常过于多,则程序不是结束执行就是导致不明确行为;故C++不喜欢析构函数吐出异常。

【注】析构函数绝对不要吐出异常,如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或者结束程序。

绝不在构造和析构过程中调用virtual函数

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

class Traansaction{    //所有交易的基类
public:
    Transaction();

    //做出一份因类型不同而不同的日志记录
    virtual void logTransaction() const = 0;    
}

Transaction::Transaction(){
    ...
    logTransaction();
}

class BuyTransaction:public Transaction{
public:
    virtual void logTransaction() const;
    ...
}

class SellTransaction:public Transaction{
public:
    virtual void logTransaction() const;
    ...
}

现在执行:

BuyTransaction b;

此时会有BuyTransaction构造函数被调用,同时基类构造函数先于它被调用;并且,基类中的logTransaction被调用,此时的logTransaction版本属于基类而不属于BuyTransaction。这就说明,在基类构造期间,virtual函数不是virtual函数。

对此,有以下解决办法:

  1. 在class Transaction内将logTransaction函数改为non-virtual,然后要求派生类构造函数传递必要信息为基类构造函数,而后那个派生类构造函数就可安全调用non-virtual logTransaction.
    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:
        //将log信息传给基类构造函数
        BuyTransaction(parameters):Transaction(createLogString(parameters)){
        
            ...
        }
        ...
    
    private:
        static std::string createLogString(parameters);
    }

令operator=返回一个reference to *this

即重载赋值操作符时候,返回一个reference指向操作符的左侧实参。

在operator=中处理“自我赋值”

猜你喜欢

转载自blog.csdn.net/weixin_37160123/article/details/89308126