如果有虚函数,那么析构函数必须要设置为 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,可以改成:
默认参数要小心
有默认参数时,编译器会直接根据类型填值。也就是说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;
};