虚函数赋予了 C++ 运行时多态的能力(也就是动态绑定)
最重要的结论:
- 虚表是类的虚表;每个类都有独立的虚表
- 虚表是编译时生成的
- 如果自身或者父类有虚函数,那么这个类的对象就会多出一个字段:虚指针,指向这个类的虚表
如何查看带有虚表的内存布局:
- g++:
g++ -fdump-class-hierarchy -c main.cpp
- clang:
clang -cc1 -fdump-record-layouts main.cpp
几个例子
注:
- 不考虑内存对齐的 align;
- 64 位环境,所以指针是 8 字节
1. 有虚函数才有虚函数表
这样一个类:
struct A {
int a;
virtual void foo() {}
};
其内存布局为:
0 struct A
0 |__ (A vtable pointer)
8 |__ int a
2. 父类有虚函数,子类无虚函数
父类有虚函数,子类没有:
struct A {
int a;
virtual void foo() {}
};
struct B : public A {
int b;
};
这种情况,B 的内存布局为:
0 struct B
0 |__ struct A
0 | |__ (A vtable pointer)
8 | |__ int a
12 |__ int b
3. 父类的子类都有虚函数,且虚函数不同
struct A {
int a;
virtual void foo() {}
};
struct B : public A {
int b;
virtual void bar() {}
};
B 的内存布局为:
0 struct B
0 |__ struct A
0 | |__ (A vtable pointer)
8 | |__ int a
12 |__ int b
4. 父类的子类都有虚函数,且虚函数相同
struct A {
int a;
virtual void foo() {}
};
struct B : public A {
int b;
virtual void foo() {}
};
B 的内存布局为:
0 struct B
0 |__ struct A
0 | |__ (A vtable pointer)
8 | |__ int a
12 |__ int b
5. 结论
- 当前类存在虚函数,或者父类存在虚函数,那么对象中就会存在虚指针
- 每个类都有独立的虚表,即使类之间存在继承关系,也不会共用虚表。
- 虚表是编译时期生成的。
一个多继承的例子
注:这个例子是在 windows 的 visual studio 下得出的,32 位环境,所以指针大小是 4 字节
存在如下的类:
通过 visual studio 的 debugger,查看每个对象的内存布局:
可以看到:
- 每个类都有自己独立的虚表
- 类
Derive1
虚表的前半部分和Base1
的虚表是形式相同的;类Derive1
虚表的后半部分和Base2
的虚表是形式相同的;这让动态绑定得以实现
虚表如何实现动态绑定?
- 情形:将子类指针转换为父类指针,调用某个方法,这个方法是一个虚方法,且父类和子类都有自己的实现
- 根据子类的虚指针,找到子类的虚表,找到要调用的函数在虚表中的位置,调用
- 注:为了实现动态绑定,父类虚表和子类虚表在相同位置的函数名称是相同的,但是实现不同
虚函数的应用
1. 析构函数是虚函数
一般情况下,析构派生类时,会先调用派生类的析构函数,再调用基类的析构函数。
但如果用基类指针指向派生类,然后delete这个指针,只会调用基类的析构函数。
在这种情况下如何正确地析构呢?可以将基类的析构函数定义为虚函数。