
本文共 1913 字,大约阅读时间需要 6 分钟。
我们知道,在C++中,当我们创建一个含有虚函数的类或从含有虚函数的基类派生一个子类时,编译器会为这个类生成一个虚表(VTABLE)。这个虚表存储了所有已声明为虚函数的函数地址。这些函数地址在类中或它的基类中定义。虚表的创建和维护是动态捆绑(late binding)的基础,解决了基类和派生类之间的类型转换问题。
假设我们定义一个Instrument类,并在其中声明一个虚函数void play(note)
:
class Instrument {public: virtual void play(note) const { cout << "Instrument::play" << endl; }};
当我们创建一个Wind类并继承自Instrument时,Wind类会继承 Instrument 中的play
函数,并可以重写它。编译器会为Wind类生成一个新的虚表。类似地,任何棋器类型都会有自己的虚表。虚表的每个位置都指向其对应函数的实现,而这个实现是根据对象的实际类型决定的。
在实际运行时,虚函数调用的机制如下。当我们调用一个带有Verb::play()
的对象时,需要通过以下步骤完成:
这种机制称为动态捆绑,它意味着在运行时根据对象的类型选择合适的函数实现。这种方法避免了硬编码不同类型的函数调用,提高了系统的灵活性和可维护性。
举个例子,考虑两个类:
class Instrument {public: virtual void play(note) const { cout << "Instrument::play" << endl; }};class Wind : public Instrument {public: void play(note) const override { cout << "Wind::play" << endl; }};
当你创建一个Wind对象并传递给tune()
函数时,编译器为Wind生成一个虚表,其中的play
函数地址指向Wind::play的实现。同样,传递给一个 Drums 对象(假设 Drums 也继承于 Instrument),会生成一个新的虚表,其中 Drums::play 函数地址被调用。这样就能正确输出对象的实际类名,而不是基类。
需要注意的是,虚函数的具体实现依赖于编译器的支持,但关键是如何初始化虚表。当创建一个派生类对象时,编译器会初始化vptr pointer,指向该对象所属类型的虚表。在构造子类对象时,编译器首先初始化父类对象的vptr,之后才会为子类对象设置vptr,确保子类得到正确处理。
从编译器的角度来看,接包是否有虚函数可以通过sizeof(NoVirtual)
、sizeof(OneVirtual)
和sizeof(TwoVirtual)
来区分。例如:
#includeusing namespace std;class NoVirtual { int a;public: void x() const {} int i() const { return 1; }};class OneVirtual { int a;public: virtual void x() const {} int i() const { return 1; }};class TwoVirtual {public: virtual void x() const {} virtual int i() const { return 1; }};int main() { cout << "int : " << 4 << endl; cout << "NoVirtual : " << 4 << endl; cout << "OneVirtual : " << 8 << endl; cout << "TwoVirtual : " << 4 << endl; return 0;}
运行时会发现,只有含有虚函数的类需要额外的空间来存储虚表指针和虚表中的函数地址。因此,每个含有虚函数的类都会因为这种机制而占用额外的内存。
发表评论
最新留言
关于作者
