一、概念
在 C++ 中,虚函数表(Virtual Function Table,简称 vtable)是实现多态机制的一个重要底层数据结构。当一个类中包含了虚函数时,编译器会为这个类创建一个虚函数表,用来存放该类的虚函数的地址。每个包含虚函数的类的对象实例中,会隐含一个指针(通常称为虚指针,vptr),它指向所属类的虚函数表。
二、作用
虚函数表的核心作用就是支持多态性,也就是让通过基类指针或引用调用虚函数时,能够根据对象的实际类型(是基类对象还是派生类对象)来决定调用哪个类中重写后的虚函数版本。
例如,有如下基类和派生类的定义:
class Base {
public:
virtual void func() {
std::cout << "Base::func()" << std::endl;
}
};
class Derived : public Base {
public:
void func() override {
std::cout << "Derived::func()" << std::endl;
}
};
当通过基类指针来操作对象时:
Base* ptr = new Derived();
ptr->func(); // 会调用Derived类中重写后的func函数,这就是多态的体现,而虚函数表在背后起到了关键作用
三、虚函数表的结构与存储
-
结构:虚函数表本质上就是一个函数指针数组,数组中的每个元素都是一个虚函数的地址。在这个数组中,虚函数的存放顺序和类中声明虚函数的顺序是一致的。例如,如果一个类先声明了虚函数
func1
,再声明虚函数func2
,那么在虚函数表中,存放func1
地址的元素就在前面,存放func2
地址的元素紧随其后。 -
存储:对于一个类来说,它的虚函数表在内存中只有一份副本,不管这个类创建了多少个对象实例,所有对象共享这同一个虚函数表(通过各自的虚指针指向它)。并且虚函数表一般是在编译阶段就确定好了其内容和布局,存放在只读数据段(因为函数地址在程序运行过程中通常是固定不变的)。
四、虚指针(vptr)
- 虚指针是类对象中的一个隐含成员,它的类型是指向虚函数表的指针。在对象创建时(构造函数调用时),编译器会自动将对象的虚指针初始化为指向其所属类的虚函数表。例如,对于前面定义的
Derived
类的对象,当创建它时:
Derived d;
// 在d对象的内存布局中,有一个隐含的vptr,它指向Derived类对应的虚函数表
- 不同类型(基类或派生类)但存在继承关系且包含虚函数的对象,其虚指针指向不同的虚函数表,从而实现了多态调用时函数的正确选择。比如基类
Base
的对象的虚指针指向Base
类的虚函数表,而Derived
类对象的虚指针指向Derived
类的虚函数表。
五、单继承下的虚函数表示例
以下通过一个简单的代码示例结合内存布局来更深入理解单继承情况下的虚函数表:
class Animal {
public:
virtual void eat() {
std::cout << "Animal is eating" << std::endl;
}
virtual void sleep() {
std::cout << "Animal is sleeping" << std::endl;
}
};
class Dog : public Animal {
public:
void eat() override {
std::cout << "Dog is eating" << std::endl;
}
void sleep() override {
std::cout << "Dog is sleeping" << std::endl;
}
};
- 内存布局上,
Animal
类对象有一个虚指针(假设对象起始地址为0x1000
),这个虚指针指向Animal
类的虚函数表(假设虚函数表地址为0x2000
),在0x2000
这个地址开始的虚函数表中,存放着Animal::eat
和Animal::sleep
这两个虚函数的地址,顺序和类中声明顺序一致。 - 对于
Dog
类对象(假设起始地址为0x3000
),它也有一个虚指针,这个虚指针指向Dog
类自己的虚函数表(假设地址为0x4000
),在0x4000
开始的虚函数表中,存放的是Dog::eat
和Dog::sleep
的地址,因为Dog
重写了Animal
的虚函数,所以这里存放的是Dog
类中重写后版本的虚函数地址。
当执行如下代码:
Animal* a = new Dog();
a->eat();
通过 a
这个基类指针(实际指向 Dog
类对象)调用 eat
函数时,会顺着对象中的虚指针找到 Dog
类的虚函数表,然后根据虚函数表中存放的函数地址调用 Dog::eat
函数,实现了多态行为。
六、多继承下的虚函数表情况(更复杂一些)
在多继承场景下,情况会复杂一些,因为一个派生类有多个基类,每个基类如果有虚函数表,那派生类对象中就会有多个虚指针(每个基类对应一个虚指针,指向各自基类的虚函数表),同时还可能存在一个额外的属于派生类自己的虚函数表(如果派生类新增了虚函数的话)。
例如:
class Base1 {
public:
virtual void func1() {
std::cout << "Base1::func1()" << std::endl;
}
};
class Base2 {
public:
virtual void func2() {
std::cout << "Base2::func2()" << std::endl;
}
};
class Derived : public Base1, public Base2 {
public:
void func1() override {
std::cout << "Derived::func1()" << std::endl;
}
void func2() override {
std::cout << "Derived::func2()" << std::endl;
}
};
在内存布局上,Derived
类对象中,首先会有一个指向 Base1
类虚函数表的虚指针(因为 Base1
是第一个基类),接着是 Base1
类相关的数据成员(如果有的话),然后是指向 Base2
类虚函数表的虚指针,再之后是 Base2
类相关的数据成员(如果有话)。如果 Derived
类自己新增了虚函数,还会有一个属于 Derived
类自己的虚函数表来存放这些新增虚函数的地址(当然这涉及到更复杂的内存布局调整和函数调用查找机制了)。
总之,虚函数表是 C++ 多态机制实现的关键底层支撑,理解它对于深入掌握 C++ 面向对象编程以及多态特性有着非常重要的意义。
标签:函数,对象,Derived,Dog,C++,基类,指针 From: https://blog.csdn.net/Visual_progress/article/details/143852322