什么是虚函数
在我看来,虚函数是在基类中声明并由派生类重新定义的成员函数(如果不是纯虚函数也可以不覆盖)。当用基类的指针或者引用
访问虚函数时会发生「动态绑定」找到真正的函数地址。
一个基本的例子就是这样:
#include <iostream>
using namespace std;
class base {
public:
virtual void print()
{
cout << "print base class" << endl;
}
void show()
{
cout << "show base class" << endl;
}
};
class derived : public base {
public:
void print()
{
cout << "print derived class" << endl;
}
void show()
{
cout << "show derived class" << endl;
}
};
int main()
{
base* bptr;
derived d;
bptr = &d;
// virtual function, binded at runtime
bptr->print();
// Non-virtual function, binded at compile time
bptr->show();
}
注意:
如果我们在基类中定义了虚函数,函数在派生类中自动被视为虚函数。比如我们看这个代码:
#include <iostream>
using namespace std;
class base {
private:
public:
virtual void print() const {
cout << "base" << '\n';
}
};
class A : public base {
public:
void print() const {
cout << "A" << '\n';
}
};
class C : public A {
public:
void print() const {
cout << "C" << '\n';
}
};
int main() {
base *b;
b = new A();
b->print();
b = new C();
b->print();
return 0;
}
结果:
A
C
A 中没定义 print 是虚函数,但是还是调用到了 C 的 print。证明 C 这个类中有一个「虚函数表」,也就证明了 print 还是一个虚函数。
虚函数的实现
之前提到了虚函数表,什么是虚函数表(VTABLE)?以及虚函数怎么实现的?
如果一个类包含一个虚函数,那么编译器本身会做两件事:
- 如果创建了该类的对象,则将虚拟指针(VPTR)作为该类的数据成员指向该类的 VTABLE。对于创建的每一个新对象都会插入一个新的虚拟指针作为该类的数据成员。
- 无论对象是否创建,一个类的 VTABLE 都会存在,本质上就是一个静态的函数指针数组,数组的每个单元包含该类中的每个
虚函数
的地址。
什么时候编译器会选择使用虚表
我觉得就两个条件:
- 该类的某一个父类的指针或者引用
- 调用虚函数
我理解的虚函数为什么会让代码变慢的原因
以一个经典的 ARM 五级流水线为例子,一条指令的执行有五个过程:1. Fetch 2. Decode 3. Excute 4. Memory 5. Write
在编译器确定函数的情况下,在执行跳转的时候,函数的指令就已经可以放在流水线里面了。但是虚函数必须得等到从虚表中拿到函数地址才能继续执行,不能提前运行函数的指令。所以就需要空出几个 CPU 周期,所以会变慢。
但是真正的瓶颈还是 IO 这一点消耗应该也不算什么。
虚析构函数有什么作用
我们来看这个例子:
#include<iostream>
using namespace std;
class base {
public:
base(){
cout<<"Constructing base \n";
}
~base() {
cout<<"Destructing base \n";
}
};
class derived: public base {
public:
derived() {
cout<<"Constructing derived \n";
}
~derived() {
cout<<"Destructing derived \n";
}
};
int main() {
derived *d = new derived();
base *b = d;
delete b;
return 0;
}
结果是:
Constructing base
Constructing derived
Destructing base
没有正确释放派生类的对象。
如果给基类的西沟函数加上 virtual 就会正确释放 派生类的对象。所以为了避免内存泄漏,任何时候在类中有虚函数时,都应该立即添加虚析构函数(即使它什么都不做)。
#include<iostream>
using namespace std;
class base {
public:
base(){
cout<<"Constructing base \n";
}
virtual ~base() {
cout<<"Destructing base \n";
}
};
class derived: public base {
public:
derived() {
cout<<"Constructing derived \n";
}
~derived() {
cout<<"Destructing derived \n";
}
};
int main() {
derived *d = new derived();
base *b = d;
delete b;
return 0;
}