Effective C++读书笔记二(构造 / 析构 / 赋值运算)

18 篇文章 0 订阅
4 篇文章 0 订阅

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

class Empty
{};

上面这个类是一个 空类,但是经过C++处理之后,它就不再是一个空类了。如果你自己没有生命,编译器就会为它声明一个构造函数、拷贝构造函数、赋值运算符重载和析构函数,所有这些函数都是public而且inline的。其实上面的类就会变成下面这个样子

class Empty
{
public:
    Empty(){...}
    Empty(const Empty& e){...}
    Empty& operator=(const Empty& e){...}
    ~Empty(){...}
};

惟有当这些函数被需要也就是被调用的时候,它们才会被编译器创建出来。
编译器为类生成的赋值运算符只有当生出的代码合法而且有适当机会证明它有意义,才会真的生成。万一两个条件有一个不符合编译器会拒绝为class生出operator=。
假设你让一个引用改指向不同对象(这是不被允许的),也就是为一个引用赋值那么C++的响应是拒绝编译那一行赋值动作。如果你打算在一个含有引用成员的类中支持赋值操作,那就需要自己定义赋值运算符。类中如果有const成员也是一样的。

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

通常如果你不希望类支持某一特定机能只要不声明对应函数就是了,但这个办法对拷贝构造和赋值运算符却不起作用。
但是所有编译器产生的函数都是public的。为了阻止这些函数被创建出来,你需要自行声明它们,并且声明为private的。(类似于boost库中scoped_ptr实现的那样)
一般而言这样的方法并不安全,因为成员函数和友元函数还是可以骑调用这些私有的函数。所以除了声明为私有的,也不要去定义它们。

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

函数返回的指针指向一个派生类对象,而这个对象却经由一个基类指针被删除,而目前基类的析构函数是非虚的。C++指出,当派生类对象经由一个基类指针被删除,而该基类带着一个非虚的析构函数,实际执行时通常发生的是对象的派生类部分没有被销毁。
消除这个问题的做法很简单,将基类的析构函数声明为虚函数。此后删除派生类对象就会销毁整个对象,不会出现局部销毁的现象。
任何类只要带有虚函数都几乎确定应该也有一个虚析构函数。
如果类不含虚函数,通常表示它并不想作为一个基类,当一个类不为基类时,就没有必要将析构函数声明为虚函数了
如果要实现虚函数,那对象必须携带某些信息,主要用来在运行期间决定哪一个虚函数该被调用。这份信息通常是由虚表指针指出。虚表指向一个由函数指针构成的数组,称为虚表。每一个带有虚函数的类都有相应的虚表,当对象调用某一个虚函数,实际被调用的函数取决于该对象的虚表指针所指的那个虚表。
只有当类内含至少一个虚函数才将该类的析构函数声明为虚函数。
为你希望它成为抽象的那个类声明一个纯虚的析构函数。例:

class AWOV
{
publicvirtual ~AWOV() = 0;
};

这个类有一个纯虚函数,所以它是个抽象类,但你必须为这个纯虚析构函数提供一份定义。
析构函数的运行方式是,最深层派生的那个类的析构函数最先被调用,然后是其每一个基类的析构函数被调用编译器会在AWOV的派生类的析构函数中创建一个对~AWOV的调用动作,所以你必须为这个函数提供一份定义。

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

C++并不禁止析构函数吐出异常,但它不鼓励你这样做。
在两个异常同时存在的情况下,程序若不是结束执行就是导致不明确行为。C++不喜欢析构函数吐出异常。但如果你的析构函数必须执行一个动作,而该动作可能会在失败时抛出异常该怎么办?

  • 如果在析构函数中调用的函数抛出异常那么就结束程序,通常通过调用abort来完成
    • 如果程序遭遇一个“于析构期间发生的错误”后无法继续执行,强迫结束程序是个合理的选项,这样可以阻止异常从析构函数传播出去
  • 或者吞下因调用该函数而引发的异常(也就是忽略)
    • 一般而言,忽略异常是个不好的注意,但有时这样要比草率的终止程序要好。程序必须能够继续可靠执行

如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数,即析构函数绝对不要吐出异常,如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后忽略或结束程序
如果用户需要对某个函数运行期间抛出的异常做出反应,那么类应该提高一个普通函数执行该操作

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

不应该在构造函数和析构函数期间调用virtual函数,因为这样的调用不会带来你预想的结果
例:

class A
{
public:
    A()
    {
        ...
        fun();
    }
    virtual void fun()
    {
        //dosomething...
    }
};
class B:public A
{
public:
    virtual void fun()
    {...}
};

此时我们声明一个派生类对象:B b;
毫无疑问会有一个B类的构造函数被调用,但在这之前会先调用基类A的构造函数。在A类构造函数的最后一行调用了一个虚函数fun,那么这个时候调用虚函数的版本是基类的还是派生类的呢?可以准确的说调用的是基类的版本而不是派生类的版本,即使你现在建立的对象是派生类的。在基类构造期间虚函数绝对不会下降到派生类的阶层,相当于在基类构造期间,虚函数和普通函数无异。
由于基类的构造函数在派生类之前执行,当基类构造函数执行时,派生类的成员变量尚未初始化,在此期间若调用的虚函数为派生类的虚函数那么必定会取用派生类的成员变量,而这些成员变量还未初始化。
在派生类对象的基类构造期间,对象的类型是基类而不是派生类,不只是虚函数会被编译器解析至基类,若使用运行期间类型信息也会把对象视为基类类型。本例中,当基类构造函数正执行起来起来打算初始化派生类 对象内的基类成分时,该对象的类型是基类类型。

相同的道理也适用于析构函数,一旦派生类的析构函数开始执行,对象内的派生类成员变量便呈现未定义值,进入基类析构函数后对象就成为一个基类对象,假设我们在基类的析构函数中调用了虚函数,当我们调用基类析构函数的时候,说明派生类的析构函数已经被掉过了,这时候基类内部调用了虚函数,如果去调用派生类中重写的虚函数就没有什么意义了,所以不建议。但实际上,调用的还是基类自己的,相当于还是没有实现多态,此时的虚函数就相当于一个普通函数了

条款10:令operator=返回一个指向this指针的引用

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

class Widget
{
public:
    ...
    Widget& operator=(const Widget& wg)
    {
        ...
        return *this;
    }
    ...
};

这个协议不仅适用于以上的标准赋值形式,也适用于所有赋值相关运算,例如:+=、-=、*=等等。
注意:这只是一个协议,并无强制性。如果不遵循它,代码一样可以通过编译。

条款11:在operator=中处理自我赋值

“自我赋值”发生在对象被赋值给自己的时候,这想起来有些愚蠢,但它合法。而且有的自我赋值不一定可以被一眼看出:
a[i] = a[j]; //如果i和j相等,这就是一个自我赋值
*px = *py; //如果px和py恰好指向同一个东西,这也是自我赋值
这些现象是“别名”带来的后果。一般而言如果某段代码操作指针或引用而它们被用来指向多个相同类型的对象,就需要考虑这些对象是否为同一个。实际上两个对象只要来自同一个继承体系就有可能造成“别名”,因为一个基类的引用或指针可以指向一个派生类对象:

class Base {...};
class Derived:public Base{...};
void doSomething(const Base& rb, Derived* pd);

看看doSomething函数的两个参数,rb和*pb有可能其实是一个对象
如果你运用对象来管理资源,而且你可以确定所谓“资源管理对象”在拷贝发生的时候由正确的举措,这个时候赋值运算符的自我赋值有可能是安全的。但是。如果你尝试自行管理资源,可能会掉进陷阱—-在停止使用资源之前意外释放了它。
例:

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

Widget& Widget::operator=(const Widget& rhs)
{
    delete pb;                //停止使用当前的bitmap
    pb=new Bitmap(*rhs.pb);   //利用rhs的bitmap创建一个
    return *this;
} 

看这个例子,operator=函数中的*this和rhs有可能是一个对象。那么delete销毁的就不只是当前对象的bitmap了,同时也销毁了rhs的bitmap。那么返回的指针就指向了一个已经被删除的对象。
为了处理这种情况的发生,加一句代码即可:

Widget& Widget::operator=(const Widget& rhs)
{
    if(*this==rhs)
        return *this;
    delete pb;               
    pb=new Bitmap(*rhs.pb);   
    return *this;
} 

operator=具有“异常安全性”,往往自动获得“自我赋值安全”的回报。只要注意导出异常安全即可(也就是在实现过程中避免出现异常的情况),例如刚刚,只要注意复制pb所指的东西之前不要删除pb

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

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

之前的条款5说到,编译器会在必要的时候为我们的类创建拷贝函数(拷贝构造函数、赋值运算符重载),并说明这些由编译器生成的版本会将被拷对象的所有成员变量都做一份拷贝。
如果你自己声明这两个拷贝函数,那么当你的实现代码几乎必然出错时却不告诉你。
如果你为类添加一个成员变量,你必须同时修改拷贝函数。同时你也要修改类的所有构造函数以及任何非标准形式的operator=。

class Customer
{
public:
    ...
    Customer(const Customer& c)
        :name(c.name)
    {
        ...;
    }
    Customer& operator=(const Customer& c)
    {
        name=c.name;
        return *this;
    }
private:
    string name;
};
class PriorityCustomer:public Customer
{
public:
    ...
    PriorityCustomer(const PriorityCustomer& pc)
        :priority(pc.priority)
    {
        ...
    }
    PriorityCustomer& operator=(const PriorityCustomer& pc)
    {
        priority=pc.priority;
        return *this;
    }
private:
    int priority;
}

PriorityCustomer类继承了Customer类,看起来PriorityCustomer类的拷贝函数好像将所有的成员变量都复制了,请仔细看,它们确实复制了PriorityCustomer类的成员变量,但是别忘了还有它所继承的Customer类成员变量,而这些成员变量却没有被复制。PriorityCustomer的拷贝构造函数没有指定实参传给其基类构造函数,因此PriorityCustomer对象的Customer成分会被默认构造函数初始化。
任何时候,只要你想自己实现拷贝函数必须很小心地复制它的基类成分,那些成分往往会是private的,所以你无法直接访问,你应该让派生类的拷贝函数调用相应的基类函数
本条款说的复制每一个成分也就是当你编写一个拷贝函数请确保:

  • 复制所有本类的成员变量
  • 调用所以基类内适当的拷贝函数

不要尝试以某个拷贝函数实现另一个拷贝函数(在拷贝构造函数中调用赋值运算符重载,或者在赋值运算符重载中调用拷贝构造函数),应该将共同机能放在第三个函数里,并由两个拷贝函数共同调用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值