C++ 虚函数表与多态
C++ 虚函数表与多态
虚函数表指针
虚函数引入后类的变化
1
2
3
class A {};
A a;
cout << sizeof(a) << endl; // 输出对象的大小是1
一个空类, 只要对象占用内存空间, 那么这个类的大小至少是1;
1
2
3
4
5
6
class A {
void func1() {}
void func2() {}
};
A a;
cout << sizeof(a) << endl; // 输出对象的大小仍然是1
类A的普通成员函数并不会占用类对象的内存空间, 所以类A的大小仍然是1;
1
2
3
4
5
class A {
virtual void func1() {}
};
A a;
cout << sizeof(a) << endl; // 输出对象的大小是4 (Visual Studio解决方案中的×86平台, Linux平台GCC编译的结果可能是8)
当一个或多个虚函数加入到类中后, 编译器会向类中插入一个看不见的成员变量, 这个成员变量类似下方的伪代码形式:
1
2
3
4
class A {
void* vptr; // 虚函数表指针(Virtual Table Pointer)
virtual void func1() {}
};
也就是说, 编译器给类中加入的虚函数表指针(Virtual Table Pointer), 它的大小正好是4个字节(32位系统), 这4个字节是占用类对象的内存空间的;
虚函数表
1
2
3
4
5
class A {
void func1() {}
void func2() {}
virtual void vfunc() {}
};
当类A中存在至少一个虚函数时, 编译器就会给类A生成一个虚函数表(Virtual Table), 这个虚函数表会一直伴随着类A的对象存在;
通过编译链接后生成一个可执行文件之后, 类A和伴随类A的虚函数表会被保存到可执行文件中, 在这个可执行文件执行时, 伴随类A的虚函数表也会被加载到内存中;
虚函数表指针被赋值的时机
- 虚函数表指针(vptr): 类中有虚函数的时候, 编译器会在类中插入一个虚函数表指针;
- 虚函数表(vtbl): 类中有虚函数的时候, 编译器会生成一个虚函数表;
1
2
3
class A {
virtual void vfunc() {}
};
对于有虚函数的类A, 在编译时, 编译器会向类A的构造函数中(如果没有构造函数, 编译器则会创建一个构造函数), 安插为vptr
赋值的代码, 伪代码如下:
1
2
3
4
5
6
7
class A {
void* vptr; // 虚函数表指针
virtual void vfunc() {}
A() { // 类A的构造函数
vptr = &A::vtable; // 为vptr赋值, 使得vptr指向类A的虚函数表vtbl
}
};
创建类A的对象时, 会调用类A的构造函数, 在构造函数中会为vptr
赋值, 使得vptr
指向类A的虚函数表;
类对象在内存中的布局
1
2
3
4
5
6
7
8
9
10
11
class A {
public:
void func1() {} // 普通成员函数1
void func2() {} // 普通成员函数2
virtual void vfunc1() {} // 虚函数1
virtual void vfunc2() {} // 虚函数2
virtual ~A() {} // 虚析构函数
private:
int a; // 成员变量
int b;
};
此时生成了类A对象后, 类A对象在内存中的布局如下:
函数的普通成员函数func1
和func2
不会占用类对象的内存空间;
所以, 这个类A的对象占用的内存空间大小是4(虚函数指针) + 4(int a;) + 4(int b;) = 12
字节
虚函数的工作原理及多态性的体现
多态性(动态多态)
- 静态多态: 函数重载: 同一个类中, 函数名相同, 参数列表不同, 有种说法是函数重载不算多态
- 动态多态: 虚函数: 父类指针指向子类对象, 调用虚函数时, 会调用子类的虚函数
通过父类指针new一个子类对象或通过父类引用绑定一个子类对象的时候
如果用父类指针来调用虚函数, 那么会调用子类的虚函数
代码实现上的多态
1
2
3
4
5
6
7
class A {
public:
virtual void vfunc() {}
A() {
vptr = &A::vtbl;
}
};
如果是通过vptr
来找到虚函数表指针vtbl
, 再通过查询虚函数表指针vtbl
找到虚函数表的入口地址, 并去执行虚函数vfunc
, 这就被称之为多态
例子:
1
2
3
4
5
6
7
8
9
10
11
12
class Base {
public:
virtual void vfunc() {}
};
Base* pa = new Base();
pa->vfunc(); // 是多态
Base base;
base.vfunc(); // 不是多态
Base* ybase = &base;
ybase->vfunc(); // 是多态
表现形式上的多态
- 程序中既要存在父类也要存在子类, 父类中必须要有虚函数, 子类中必须要重写父类的虚函数
- 父类指针要指向子类对象或者父类引用要绑定(指向)子类对象
- 通过父类指针或引用, 调用子类中重写的虚函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Derive : public Base {
public:
virtual void vfunc() {} // Derive类重写了Base类的虚函数vfunc
};
Derive derive;
Base* pbase = &derive; // 父类指针指向子类对象
pbase->vfunc(); // 是多态:通过父类指针调用虚函数,实际调用的是子类的vfunc
Base& pbase2 = *new Derive(); // 父类引用绑定子类对象, 注意自行delete释放内存
pbase2.vfunc(); // 是多态:通过父类引用调用虚函数,实际调用的是子类的vfunc
Derive derive2;
Base& yinbase = derive2; // 父类引用绑定子类对象
yinbase.vfunc(); // 是多态:通过父类引用调用虚函数,实际调用的是子类的vfunc
父类的析构函数
- 如果父类的析构函数不是虚函数, 那么通过父类指针释放子类对象的时候, 只会调用父类的析构函数, 不会调用子类的析构函数
- 如果父类的析构函数是虚函数, 那么通过父类指针释放子类对象的时候, 会先调用子类的析构函数, 再调用父类的析构函数
1
2
3
4
5
6
7
8
9
10
11
12
class Base {
public:
virtual ~Base() { std::cout << "Base destructor\n"; }
};
class Derived : public Base {
public:
~Derived() { std::cout << "Derived destructor\n"; }
};
Base* ptr = new Derived();
delete ptr; // 会正确调用 Derived 的析构函数,再调用 Base 的析构函数
本文由作者按照 CC BY 4.0 进行授权