虚函数详解(一)—— 虚函数基本原理与单继承
前言在C++的众多特性中,虚函数是实现运行时多态的基石。理解虚函数的工作原理,对于写出正确、高效的面向对象程序至关重要。本系列将从底层机制出发,深入剖析虚函数的方方面面。作为开篇,我们先聚焦于最基础也最核心的部分:虚函数的基本原理,以及它在单继承体系下的行为。一、从静态绑定到动态绑定在C++中,当我们通过对象、指针或引用来调用一个成员函数时,编译器需要决定实际执行哪个函数,这个过程称为绑定。1.1 静态绑定 对于非虚函数,绑定发生在编译期:class Base { public: void show() { std::cout "Base::show()" std::endl; } }; class Derived : public Base { public: void show() { std::cout "Derived::show()" std::endl; } }; int main() { Derived d; Base* ptr = d; ptr-show(); // 输出:Base::show() }虽然 `ptr` 实际指向一个 `Derived` 对象,但因为 `show` 不是虚函数,编译器仅根据指针的静态类型(`Base*`)来决定调用 `Base::show`。这种“指鹿为马”的行为显然不符合多态的预期,解决方案正是虚函数。1.2 动态绑定(虚函数)只需在基类函数前加上 `virtual` 关键字:class Base { public: virtual void show() { std::cout "Base::show()" std::endl; } }; class Derived : public Base { public: void show() override { std::cout "Derived::show()" std::endl; } }; // ptr-show(); 将会输出:Derived::show()这样一来,调用的函数不再由指针的静态类型决定,而是由指针所指对象的**真实类型**在运行时决定,这就是动态绑定。支撑这一机制的秘密武器,便是**虚函数表(vtable)** 与**虚函数指针(vptr)。二、虚函数表和虚函数指针的内存布局2.1 什么是虚函数表编译器为每个包含虚函数的类(或继承自包含虚函数的类)生成一张表,表中存放了该类所有虚函数的实际入口地址。这张表就是虚函数表(vtable)。注意,虚函数表是**类级别**的,同一个类的所有实例共享同一张虚函数表。2.2 虚函数指针每个包含虚函数的类的对象,内部会隐式地多出一个指针成员,指向它所属类的虚函数表。这个指针就是虚函数指针(vptr),通常位于对象内存布局的最前端(具体位置依赖编译器实现,但绝大多数编译器如此)。vptr 是**对象级别**的,每个对象拥有自己的 vptr,虽然它们指向的可能是同一张 vtable。我们用一段代码来验证(以典型的 64 位编译器为例):#include iostream class Base { int data; public: virtual void func1() { std::cout "Base::func1()\n"; } virtual void func2() { std::cout "Base::func2()\n"; } }; int main() { Base b; std::cout "sizeof(Base): " sizeof(b) std::endl; }输出可能是:`sizeof(Base): 16`(其中 8 字节是 vptr,4 字节为 `int data`,还有 4 字节因对齐而填充)。也就是说,一个空壳的 `Base` 对象内部有一个无形的指针,指向基类的虚函数表。2.3 虚函数表的结构