如果有虚函数,那么析构函数必须要设置为 virtual

如果析构函数不是 virtual,那么如果用指针或引用的时候,仅会调用父类的析构,子类的不会。如:

#include <iostream>
#include <complex>
using namespace std;

class Base {
public:
    Base() {
        cout << "Base::Base()" << endl;
    }
    ~Base() {
        cout << "Base::~Base()" << endl;
    }
};

class Derived: public Base {
public:
    Derived() {
        cout << "Derived::Derived()" << endl;
    }
    ~Derived() {
        cout << "Derived::~Derived()" << endl;
    }
};

int main() {
    Base*   pb = new Derived;
    delete pb;
}

输出为:

Base::Base()
Derived::Derived()
Base::~Base()

改成 virtual 后,则是:

Base::Base()
Derived::Derived()
Derived::~Derived()
Base::~Base()

根据 Bjarne Stroustrup 的解释,这么做是因为 C++ 没有多少有子类的类,它们只用上了 OOP 的封装特性;有子类的也多数是作为 interface 使用。所以默认 virtual 有点多余。

为什么说只要有 virtual 就一定要设置析构函数为 virtual?因为 virtual 本来就是用于继承的,如果继承了某个父类而不增加或重载方法,那么这个继承有意义吗?

同名函数会覆盖

下面的代码有 3 个 f,看起来 Derived 会调用 f(double),但结果不是。

#include <iostream>
#include <complex>
using namespace std;

class Base {
public:
    virtual void f( int ) {
        cout << "Base::f(int)" << endl;
    }
    virtual void f( double ) {
        cout << "Base::f(double)" << endl;
    }
};

class Derived: public Base {
public:
    void f( complex<double> ) {
        cout << "Derived::f(complex)" << endl;
    }
};

int main() {
    Derived d;
    Base*   pb = new Derived;

    d.f(1.0);
    pb->f(1.0);
}

输出是:

Derived::f(complex)
Base::f(double)

根据 Bjarne Stroustrup 的解释,这是因为每一个 class 都有自己的作用域,所以在匹配的时候首先尝试看看 Derived 中有没有可以调用的,然后才去找 Base。

所以要在子类重载,要用 using,可以改成:

image

默认参数要小心

有默认参数时,编译器会直接根据类型填值。也就是说Base* b = Derived{}, b->f()会传父类的默认值。所以如果不给父类方法默认值的话,这种情况会编译出错,子类方法不给默认值也是一样的。

推测这样的原因是,一个父类指针可以指向不同的子类,而编译期这种事情不太好确定(比如在工厂函数里),这种时候只能知道父类的默认值。至于子类指针,必须要final才可以确定没有孙类。所以就干脆根据类型定默认值了。

感觉还是不要在继承中用默认参数好一些……

#include <iostream>
#include <complex>
using namespace std;

class Base {
public:
    Base() {
        f(); // 实际调用Base::f(10)
        cout << "Base::Base()" << endl;
    }
    virtual ~Base() {
        cout << "Base::~Base()" << endl;
    }
    virtual void f(int a = 10) {
        cout << "Base::f(" << a << ")" << endl;
    }
};

class Derived: public Base {
public:
    using Base::f;
    Derived(): Base{} {
        f(); // 实际调用Derived::f(20)
        cout << "Derived::Derived()" << endl;
    }
    virtual ~Derived() {
        f(); // 实际调用Derived::f(20)
        cout << "Derived::~Derived()" << endl;
    }
    void f(int a = 20) {
        cout << "Derived::f(" << a << ")" << endl;
    }
};

int main() {
    Derived d;
    d.f(); // 实际调用Derived::f(20)
    Base& b = d;
    b.f(); // 实际调用Derived::f(10)
}

父类函数尤其是构造函数调用虚函数

在父类构造函数调用虚函数,会调用父类自己的,而非子类的,其他情况则是调用子类的。

#include <iostream>
#include <complex>
using namespace std;

class Base {
public:
    Base() {
        f(10); // 实际调用Base::f(int)
        cout << "Base::Base()" << endl;
    }
    ~Base() {
        cout << "Base::~Base()" << endl;
    }
    virtual void f( int ) {
        cout << "Base::f(int)" << endl;
    }
    void g() {
        f(10); // 实际调用Derived::f(int)
        cout << "Base::g(int)" << endl;
    }
};

class Derived: public Base {
public:
    Derived(): Base{} {
        f(20); // 实际调用Derived::f(int)
        cout << "Derived::Derived()" << endl;
    }
    ~Derived() {
        f(10); // 实际调用Derived::f(int)
        cout << "Derived::~Derived()" << endl;
    }
    void f(int) {
        cout << "Derived::f(int)" << endl;
    }
};

int main() {
    Derived d;
    d.g();
}
Base::f(int)
Base::Base()
Derived::f(int)
Derived::Derived()
Derived::f(int)
Base::g(int)
Derived::f(int)
Derived::~Derived()
Base::~Base()

所以如果要在父类,尤其是构造函数中调虚函数,那么建议改成 std::function

根据 Bjarne StrouStrup 所说,在构造函数中,虚函数功能未开启,所以会调用父类的。不开启的原因是虚表未构造完成。

但我试了发现初始化列表之后就实际调用子类的了。上文的Derived::Derived()的f(),调用的实际上是子类的。这也好解释,因为子类肯定知道自己的f是谁。同样的,如果要在子类调用父类的同名函数,还是得要用using。

那么虚表什么时候构造完成?在初始化列表初始化完成的时候,也就是说,在下面 cout 前,v{10} 后的位置。用 gdb 也容易看出来,因为 p *this 会显示 vptr 地址的。

#include <iostream>
#include <complex>
using namespace std;

class Base {
public:
...
};

class Derived: public Base {
public:
    Derived():
    Base{},
    v{10}
    {
        cout << "Derived::Derived()" << endl;
    }
    int v;
};

image

参考文献

https://www.stroustrup.com/bs_faq2.html#virtual-dtor

http://www.gotw.ca/gotw/005.htm