/ NOTES

《Effective C++》读书笔记(三)

《Effective C++》读书笔记(三)

《 Effective C++ 改善程序与设计的 55 个具体做法》条款 08 ~ 条款 12

在构造和析构期间不要调用 virtual 函数

假如你在基类构造函数中调用了一个虚函数,然后当你在建立一个派生类对象时,会发现它调用的那个虚函数并不是你想要的那个版本

因为在派生类的构造函数调用之前,基类的构造函数一定会被更早的调用,然后在基类构造期间,派生类的成员变量并未初始化,你所建立的对象类型暂时是基类的而不是派生类的,如果使用运行期类型信息,也会把对象视为基类类型,所以调用的那个虚函数也就不会下降到派生类阶层,对象在派生类构造函数执行前不会成为一个派生类对象。析构函数也是同理

所以要想解决这个问题,可以令派生类将必要的构造信息向上传递至基类构造函数,如:

class Transaction
{
public:
    explicit Transaction(const std::string &logInfo);
    void logTransaction(const std::string &logInfo) const;
    // ...
};
Transaction::Transaction(const std::string &logInfo)
{
    // ...
    logTransaction(logInfo); // 非虚函数的调用
}
class BuyTransaction : public Transaction
{
public:
    BuyTransaction(parameters)
        : Transaction(createLogString(parameters)) // 将log信息传给基类构造函数
    {
        // ...
    }

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

其中 createLogString 函数是静态的,也就不可能意外地指向尚未初始化的成员变量

std::abort()

中止当前进程,产生程序终止的异常。

没有返回值,而且程序终止时不会破坏任何对象,不会导致数据竞争。

如果一个程序遇到”于析构期间发生的错误”后无法继续执行,就可以制作运转记录,记下这个失败,然后调用 std::abort() 结束程序,这样可以阻止异常从析构函数传播出去。因为从析构函数中抛出异常是危险的,如果抛出发生在堆栈展开操作期间(如另一个异常已经在传播),程序通常会崩溃;如果子类析构函数抛出异常,则不会调用父析构函数,会使对象处于部分销毁状态。所以如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们结束程序

确保 operator= 能正确处理自我赋值

下面是自我赋值时并不安全的实现代码:

class Bitmap {};
class Widget
{
private:
    Bitmap *pb;
};

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

假如 *this 和 rhs 是同一个对象时, delete 销毁操作之后,*rhs.pb 所指向的就是一个已被删除的对象

所以如果想解决这个问题,可以先进行”证同测试 (identity test) “,如果是自我赋值则不做任何事情。或者可以精心安排一下语句顺序,记住原先的 pb ,然后先复制再删除

可以使用 copy and swap 技术:

class Widget
{
    // ...
    void swap(Widget &rhs); // 交换*this和rhs的数据
};

Widget &Widget::operator=(const Widget &rhs)
{
    Widget temp(rhs);
    swap(temp);
    return *this;
}