《Effective C++》笔记:IV

条款09:Never call virtual functions during construction or destruction。

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

考虑如下代码:

class BaseClass
{
public:
    BaseClass()
    {
        vfun();
    }

    ~BaseClass()
    {
    }

    virtual void vfun() const = 0;

private:

};

class DerivedClass : public BaseClass
{
public:
    DerivedClass()
    {
    }

    ~DerivedClass()
    {
    }

    virtual void vfun() const { printf("Derived vfun"); }
private:

};

int main()
{
    DerivedClass a;
    return 0;
}

实际上,这段代码无法通过编译,编译器会告诉你在BaseClass中调用一个没有定义的pure virtual函数(纯虚函数)是不合法的。

我们稍微改改

class BaseClass
{
public:
    BaseClass()
    {
        init();
    }

    ~BaseClass()
    {
    }

    void init();

    virtual void vfun() const = 0;

private:

};

class DerivedClass : public BaseClass
{
public:
    DerivedClass()
    {
    }

    ~DerivedClass()
    {
    }

    virtual void vfun() const { printf("Derived vfun"); }
private:

};

int main()
{
    DerivedClass a;
    return 0;
}

通过间接地调用虚函数,这回编译器不向你抱怨了,但程序是正确无误的吗?声明一个DerivedClass对象时,会发生什么?

事实上,在pure virtual被调用的同时,程序会被中止。如果BaseClass中的vfun非纯虚函数并带有实现代码,在构建DerivedClass对象时编译器会调用BaseClass中的vfun而非DerivedClass中的vfun,

也就是说,在构造和析构期间调用虚函数,虚函数失去了其本身的性质,这也是C++本身的初始化次序所决定的:基类的构造在派生类之前,派生类的构造函数调用之前,其成员变量都处于未定义状态

解决方法:不管基于什么理由,需要在构造和析构函数中调用虚函数,这本身就是个错误的设计。所以,从根源上避免它,保证构造函数和析构函数中不出现虚函数才是正确的做法。

但是怎么确保BaseClass继承体系上的对象被创建,就有相应的vfun被调用呢?既然没办法在BaseClass中构造函数向下调用DerivedClass中的虚函数vfun,那么把vfun声明成非虚函数,在DerviedClass的构造函数中传递构造信息给BaseClass可以替换并加以弥补。(这句话可能有点难以理解,因本例没有代入书中的场景,具体可查阅书中给出的代码)

这让我想到设计模式中的一个原则:依赖倒置原则。即高层不应该依赖于低层,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。

基类比作抽象,派生类比作细节,这样就很容易理解C++为什么要这样设计了。

在构造和析构期间不要调用virtual函数,因为这类调用无法下降至派生类。可以通过依赖倒置解决这类问题。

条款10:Have assignment operators return a reference to *this

译:令operator=返回一个reference to *this

令赋值(assignment)操作符返回一个reference to *this.

条款11:Handle assignment to self in operator=

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

class BitMap
{
…
};

class Widget
{
public:
    Widget()
    {
    }

    ~Widget()
    {
    }

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

private:
    BitMap *pb;
};

int main()
{
    Widget a;
    a = a;
    return 0;
}

这段代码试图在赋值的时候删除pb所指内容,问题是当operator=的*this和rhs是同一对象的时候。delete pb不但销毁了当前对象的Bitmap,还销毁了rhs的bitmap,因此在函数的末尾,返回的是一个指向已经删除的Bitmap的指针,这样的指针无论放在那里都是有害的。
解决办法也很简单,在operator=函数内加一个“证同测试”,

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

新版本在传入的对象与当前对象为同一对象时什么也不做,直接返回当前对象。

这当然可以解决问题,但考虑到new Bitmap这一语句,其实会执行new 申请内存及Bitmap构造函数两个动作。而在new申请内存这一阶段,如果没有足够的内存或其它情况导致分配不成功,new Bitmap就会抛出一个bad_alloc异常,其结果就是赋值后pb指向一块已经被删除的Bitmap而不是预想中新的Bitmap对象。

其实只要注意不要删除原有pb,而是复制pb的一份副本,在完成所有操作之后,再删除原bitmap就可以解决问题,代码如下

Widget& operator=(const Widget& rhs)
{

    Bitmap* pOrig = pb;
    pb = new BitMap(*rhs.pb);
    delete pOrig;
    return *this;
}

现在new Bitmap如果不成功,pb仍保留原状(其实指向复制于自己的新的副本),同样的,也可以解决自我赋值的问题。

但为了解决这个异常安全问题,我们付出了复制一份新副本的代价,这在提高性能上看起来确实是毫无意义的,同样的,证同测试也需要一个判断分支的成本,但有时候我们更多的不是只有性能上的考虑。怎么衡量安全与性能之间的平衡,我们首先要弄清楚,这份代码发生异常的可能性多大及自我赋值的频率多高?然后再依据具体情况采取具体的应对方法。

最后,书中提到了在opeartor=函数内手工排列语句(确保异常安全及自我赋值安全)的替代方案,使用所谓的copy and swap技术,这个技术在条款29有详细介绍,这里就暂时不介绍了。

其实,我理想中的解决方案,是直接操作内存、交换两个对象的内存地址或者是编译器优化直接帮助我们完成这一步,而且在C++11标准中也有转移语义这一利器,实现这一点应该不难。

确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap.

确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确

时间: 2024-11-08 08:53:10

《Effective C++》笔记:IV的相关文章

Effective C++笔记:构造/析构/赋值运算

条款05:了解C++默默编写并调用哪些函数 默认构造函数.拷贝构造函数.拷贝赋值函数.析构函数构成了一个类的脊梁,只有良好的处理这些函数的定义才能保证类的设计良好性. 当我们没有人为的定义上面的几个函数时,编译器会给我们构造默认的. 当成员变量里有const对象或引用类型时,编译器会不能合成默认的拷贝赋值函数:当一个基类把它的拷贝赋值函数定义为private时,它的派生类也不无生成默认的拷贝赋值函数,因为它无法完成基类成份的赋值. 条款06:若不想使用编译器自动生成的函数,就该明确拒绝 将拷贝构

Effective C++笔记06:继承与面向对象设计

关于OOP 博客地址:http://blog.csdn.net/cv_ronny 转载请注明出处! 1,继承可以是单一继承或多重继承,每一个继承连接可以是public.protected或private,也可以是virtual或non-virtual. 2,成员函数的各个选项:virtual或non-virtual或pure-virtual. 3,成员函数和其他语言特性的交互影响:缺省参数值与virtual函数有什么交互影响?继承如何影响C++的名称查找规则?设计选项有如些?如果class的行为

Effective c++(笔记)之继承关系与面向对象设计

1.公有继承(public inheritance) 意味着"是一种"(isa)的关系 解析:一定要深刻理解这句话的含义,不要认为这大家都知道,本来我也这样认为,当我看完这章后,就不这样认为了. 公有继承可以这样理解,如果令class D以public 的形式继承了class B ,那么可以这样认为,每一个类型为D的对象同时也可以认为是类型为B的对象,但反过来是不成立的,对象D是更特殊化更具体的的概念,而B是更一般化的概念,每一件事情只要能够施行于基类对象身上,就一定可以应用于派生类对

Effective c++(笔记) 之 类与函数的设计声明中常遇到的问题

1.当我们开始去敲代码的时候,想过这个问题么?怎么去设计一个类? 或者对于程序员来说,写代码真的就如同搬砖一样,每天都干的事情,但是我们是否曾想过,在c++的代码中怎么样去设计一个类?我觉得这个问题可比我们"搬砖"重要的多,大家说不是么? 这个答案在本博客中会细细道来,当我们设计一个类时,其实会出现很多问题,例如:我们是否应该在类中编写copy constructor 和assignment运算符(这个上篇博客中已说明),另外,我们是让编写的函数成为类的成员函数还是友元还是非成员函数,

Effective C++笔记05:实现

条款26:尽可能延后变量定义式的出现时间 博客地址:http://blog.csdn.net/cv_ronny 转载请注明出处! 有些对象,你可能过早的定义它,而在代码执行的过程中发生了导常,造成了开始定义的对象并没有被使用,而付出了构造成本来析构成本. 所以我们应该在定义对象时,尽可能的延后,甚至直到非得使用该变量前一刻为止,应该尝试延后这份定义直到能够给它初值实参为止. 这样做的好处是:不仅可以避免构造(析构)非必要对象,还可以避免无意义的default构造行为. 遇到循环怎么办?此时往往我

[Effective JavaScript 笔记]第28条:不要信赖函数对象的toString方法

js函数有一个非凡的特性,即将其源代码重现为字符串的能力. (function(x){ return x+1 }).toString();//"function (x){ return x+1}" 反射获取函数源代码的功能很强大,使用函数对象的toString方法有严重的局限性.toString方法的局限性ECMAScript标准对函数对象的toString方法的返回结果(即该字符串)并没有任何要求.这意味着不同的js引擎将产生不同的字符串,甚至产生的字符串与该函数并不相关. 如果函数

Effective c++(笔记)----类与函数之实现

上篇博客中集中说明了在设计一个类的时候常遇到的问题,当然博客中还夹杂着我随时想到的一些知识,发现自己写博客没很多人写的好,可能是自己语言不会组织,要么就是写的东西大家不愿意看,反正是有这方面的专业问题或者博客中有什么明显的错误和问题,大家提出来,我也好改进哈! 回归正题,这篇博客就大概的把Effective c++中类与函数这节看到的知识点做个笔记. 设计好一个类后,自己就要去实现这个类(实现类中的成员函数.友元.非成员函数等) 可能大家会遇到以下问题 1.在类的成员函数中,尽量避免返回内部数据

[Effective JavaScript 笔记]第27条:使用闭包而不是字符串来封装代码

函数是一种将代码作为数据结构存储的便利方式,代码之后可以被执行.这使得富有表现力的高阶函数抽象如map和forEach成为可能.它也是js异步I/O方法的核心.与此同时,也可以将代码表示为字符串的形式传递给eval函数以达到同样的功能.程序员面临一个选择:应该将代码表示为函数还是字符串?毫无疑问,应该将代码表示为函数.字符串表示代码不够灵活的一个重要原因是:它们不是闭包. 闭包回顾 看下面这个图 js的函数值包含了比调用它们时执行所需要的代码还要多的信息.而且js函数值还在内部存储它们可能会引用

java effective 读书笔记

java effective 读书笔记 [1]创建和销毁对象 1 静态工厂方法 就是“封装了底层 暴露出一个访问接口 ” 门面模式 2 多参数时 用构建器,就是用个内部类 再让内部类提供构造好的对象 3 枚举 singleton 不知道怎么操作,觉得意义不大 单例模式 4 私有化构造器不能实例化,也不可被子类继承 5 能用原生类的就尽量不用对象 [2]对于所有对象都通用的方法 reflexivity 自反性 symmetry 对称性 [3]类成员 降低可访问性 尽量把公有域 变成私有域,并提供

Effective c++(笔记) 之 杂项讨论

看到了Effective c++的最后一章,最开始的那章---内存管理还没搞清楚,准备那章搞清楚完也写篇博客,不管怎样,有好的开始就应该让它有个完美的结束,杂项讨论这章是作者将那些分到哪章都不合适的就索性放到了最后讨论,我看完后从中摘出自己认为重要的坐下笔记,如果能帮得到大家,那就更荣幸了哈! 1.当我们定义一个类时,编译器会自动给我产生哪些成员函数? 解析:我们都知道,当我们定义类时,如果我们没有定义某些成员函数的话,编译器会总会给我们自动合成,这就是编译器默默为我们完成和调用函数,这些函数主