effective C++ 笔记:条款09 决不在构造和析构过程中调用virtual函数

考虑以下代码

class base {
public:
    base() :num(0){
        log();
    }
    virtual void log() {
        num++;
        numsta++;
    }
    int num;
    static  int numsta;
};

int base::numsta = 0;

class derived1:public base {
public:
    derived1() : num(0) {
    }
    virtual void log() {
        num++;
    }
    void fun() {
        cout << num<<endl;
    }
    int num;
};

class derived2:public base {
public:
    derived2() : num(0) {
    }
    virtual void log() {
        num++;
    }
    void fun() {
        cout << num<<endl;
    }
    int num;
};

int main(){
    derived1* d = new derived1;
    derived2* e = new derived2;
    base* p = dynamic_cast<base*>(d);
    base* q = dynamic_cast<base*>(e);
    d->fun();
    e->fun();
    cout << p->num<<endl;
    cout << q->num << endl;
    cout << base::numsta;
    return 0;
}

最终的输出是


image.png

将log()函数放入构造函数,本意是要记录对应的子类对象的个数。
但是我们可以看到前两个输出,也就是说两个子类中的num值并没有被修改。
再看numsta的值被修改为2,也就是说构造函数内调用的log()是基类的版本。
实际上,基类构造期间虚函数绝不会下降到子类阶层,有种说法比较传神:在基类构造期间,虚函数并不是虚函数。
也可以换一个角度理解:构造子类对象时,首先会调用基类构造函数,如果此时调用的虚函数下降到子类阶层,并且子类虚函数调用了子类的成员变量,然而这些变量其实并还没有初始化!,所以C++不会让我们这样做。
实际上如果在基类构造期间,使用dynamic_cast,你会发现这时候这个对象是基类类型的。
同样,在析构过程中,一旦执行到了基类的析构函数,那么这个对象退化成了基类对象。
回头看第三行和第四行的输出,这两个子类对象中,它们的基类部分里num的值都被修改为1,这是符合以上分析的。

但是有时候可能会不小心忽略了以上警告

base(){
    init();
}

void init(){
    log();
}

这样的代码可能会让你不小心进入以上陷阱,所以在这一点上务必要小心。

有一种比较好的解决方法
就是将log函数定义为非虚函数,并且子类构造函数的初始化列表中使用基类名的形式,如

derived(): base(logString){}

然后让基类的构造函数接收这个logString,然后在基类构造函数内根据子类构造函数传来的参数来执行存储日志的操作。
也就是用这种方法,要记录各个子类对象的个数,存储的变量就不能放在子类中了(如果是普通变量),可以声明为static变量,或者使用全局变量。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容