当在C++中发生多态时,整个运行流程可以分解为以下步骤:
1. 定义基类和派生类
首先,我们定义一个基类和一个或多个派生类。基类中至少有一个虚函数,派生类会重写这个虚函数。
class Base {
public:
virtual void show() {
std::cout << "Base class show function called." << std::endl;
}
};
class Derived : public Base {
public:
void show() override {
std::cout << "Derived class show function called." << std::endl;
}
};
2. 创建派生类对象
接下来,我们创建一个派生类的对象。
Derived d;
3. 对象内存布局
当派生类对象 d
被创建时,它的内存布局包括:
- 基类的成员变量(如果有)
- 虚函数指针(vptr),指向派生类的虚函数表(vtable)
4. 虚函数表(vtable)
虚函数表是一个函数指针数组,它包含基类和派生类中所有虚函数的指针。对于派生类,它的vtable通常开始时会包含基类vtable的副本,然后是派生类新增或重写的虚函数指针。
5. 发生多态
现在,我们通过基类指针或引用来调用虚函数。
Base* bptr = &d;
bptr->show();
以下是发生多态时的详细步骤:
步骤 1: 指针或引用赋值
bptr
被赋值为派生类对象 d
的地址。
步骤 2: 调用虚函数
通过基类指针 bptr
调用 show
函数。
步骤 3: 解析虚函数调用
在运行时,C++解析器查看 bptr
指向的对象的虚函数指针(vptr)。
步骤 4: 查找虚函数表
解析器通过vptr找到派生类的虚函数表(vtable)。
步骤 5: 调用正确的函数
解析器在vtable中查找 show
函数的指针,并调用它。由于 show
在派生类中被重写,所以调用的是 Derived::show
。
步骤 6: 执行派生类的函数
Derived::show
被执行,输出相应的信息。
总结
整个多态的运行流程可以概括为:
- 创建派生类对象,该对象包含一个指向其vtable的vptr。
- 通过基类指针或引用调用虚函数。
- 在运行时,通过对象的vptr找到正确的vtable。
- 从vtable中获取正确的虚函数指针并调用函数。
- 执行派生类中重写的函数。
这个过程确保了即使是通过基类类型的指针或引用调用函数,也能够调用到派生类中重写的函数实现,从而实现了多态。
补充
只有当类被实例化为对象时,才会为该对象分配一个虚函数表指针(vptr)。虚函数表本身是类的一部分,它在程序编译时就已经确定,并且是静态存储的,但是每个对象在创建时才会被分配一个指向其类虚函数表的vptr。
这里有几个关键点:
- 类的虚函数表:每个包含虚函数的类都有一个虚函数表,这个表是类的一部分,而不是对象的一部分。它包含了类中所有虚函数的指针。
- 对象的虚函数表指针:当类的实例被创建时,对象的内存布局中会包含一个vptr,这个指针指向其类的虚函数表。
- 多态行为:由于每个对象都有一个vptr,多态行为得以实现。当通过基类指针或引用调用虚函数时,实际上是通过对象的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; }
};
int main() {
Base b; // 创建基类对象,b有一个vptr指向Base的虚函数表
Derived d; // 创建派生类对象,d有一个vptr指向Derived的虚函数表
return 0;
}
在这个例子中,b
和 d
对象都有各自的vptr,但它们指向不同的虚函数表。b
的vptr指向Base
类的虚函数表,而d
的vptr指向Derived
类的虚函数表。
如果没有创建类的实例,那么虚函数表仍然存在,但是没有具体的对象来持有指向这些表的vptr。因此,虚函数表是在类层面定义的,而vptr是在对象实例化时分配的。