尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

【C++ 面试真题】聊聊 C++ 的虚函数和多态

【C++ 面试真题】聊聊 C++ 的虚函数和多态 【C 面试真题】聊聊 C 的虚函数和多态虚函数是 C 面试的分水岭——背得出virtual 实现多态只是及格真考你的是虚函数表长什么样、对象内存里存了什么、构造析构里调虚函数会发生什么。一道题能从语法一路问到内存布局。本文把虚函数表、动态多态、纯虚函数串成一条线一次讲透。一、先说结论虚函数 运行期才决定的调用❓ 什么是虚函数它和多态什么关系✅ 一句话虚函数是一种运行期才决定调用哪个版本的成员函数它是 C 实现动态多态的机制。普通成员函数的调用在编译期就定死虚函数的调用要到运行期根据对象实际类型来决定——这就是多态。structAnimal{virtualvoidspeak(){}virtual~Animal()default;};structDog:Animal{voidspeak()override{std::cout汪;}};Animal*anewDog;// 运行期才知道 a 指向 Doga-speak();// 打印汪// 这就是多态a的静态类型是Animal*但实际指向Dog对象。没有 virtuala-speak()会调Animal::speak有了 virtual它会调Dog::speak。这就是动态多态。二、虚函数表多态是怎么实现的❓ 虚函数到底是怎么实现运行期决定调用的✅ 靠虚函数表vtable和虚表指针vptr。这是这题的硬核必须讲清。机制分三层每个有虚函数的类编译器给它生成一张虚函数表——一个存放虚函数地址的数组每个该类的对象内存里多一个虚表指针 vptr指向自己所属类的虚表虚函数调用时通过对象的 vptr 找到虚表再查表调到正确版本。Dog 对象的内存布局 ---------- | vptr ------- Dog 的虚函数表 ---------- ------------ | 成员... | | Dog::speak| ---------- ------------Animal*anewDog;// a-speak() 实际做的事// 1. 通过 a 找到对象里的 vptr// 2. vptr 指向 Dog 的虚表// 3. 查表得到 Dog::speak 的地址// 4. 调用它加分点谁建表、谁设 vptr虚表是编译期就生成好的每个类一张静态数据vptr 是构造函数里设置/更新的——对象构造时vptr 指向自己正在构造的那个类。这就引出后面构造函数里调虚函数的坑。三、静态多态 vs 动态多态❓ 多态只有虚函数这一种吗✅ 不是。C 有两种多态类型时机机制例子静态多态编译期函数重载、模板max(1,2)vsmax(1.0,2.0)动态多态运行期虚函数a-speak()// 静态多态编译期定死templatetypenameTTadd(T a,T b){returnab;}add(1,2);// 调 int 版本add(1.0,2.0);// 调 double 版本// 动态多态运行期才定Animal*anewDog;a-speak();// 运行期查虚表区别静态多态零开销编译期就定没查表但灵活性低动态多态有开销要查虚表、间接调用但能在运行期根据实际类型决定行为。面试常问用模板还是虚函数本质就是这两者的权衡。四、构造/析构里调虚函数经典大坑❓ 在构造或析构函数里调用虚函数会怎样✅不会发生多态——调到的是当前类的版本不是子类的。这是 C 最经典的坑之一。原因对象的 vptr 在构造过程中逐层设置。构造基类时vptr 指向基类进入子类构造后vptr 才更新指向子类。析构则反过来。structBase{Base(){// 此时 vptr 指向 Baseinit();// 调 Base::init// 不是 Der::init}virtualvoidinit(){}virtual~Base()default;};structDer:Base{voidinit()override{}};Der d;// 构造时 Base() 里调的是 Base::init⚠️为什么这么设计构造基类时子类部分还没构造此时调子类的虚函数会用到未初始化的成员——很危险。所以语言规定构造/析构期间的虚函数调用退化为当前类的普通调用。理解了这层保护就不会再踩这个坑。五、纯虚函数与抽象类❓ 什么是纯虚函数抽象类有什么用✅纯虚函数是只有声明、没有定义的虚函数 0。含有纯虚函数的类叫抽象类不能实例化只能作为接口被继承。// 抽象类接口structShape{// 纯虚函数必须由子类实现virtualdoublearea()const0;virtual~Shape()default;};// 子类必须实现 area否则仍是抽象类structCircle:Shape{doubler;doublearea()constoverride{return3.14*r*r;}};// Shape s; // ❌ 抽象类不能实例化Circle c{2.0};Shape*pc;std::coutp-area();// 12.56加分点纯虚函数也可以有定义在类外写Shape::area() {...}子类可显式调用——用于提供默认实现但仍强制子类 override抽象类是 C 实现接口编程的标准手段基类只定契约子类提供实现。六、虚函数的代价❓ 用虚函数有什么开销✅ 三方面代价运行期开销每次虚函数调用要查虚表 间接跳转比普通调用慢且阻碍内联空间开销每个对象多一个vptr通常 8 字节每个类多一张虚表缓存影响间接跳转破坏指令缓存局部性。实践建议虚函数的开销在绝大多数业务代码里可以忽略——一次查表就几纳秒。只有性能敏感的热路径如每帧百万次调用才需要计较那时考虑用模板/CRTP 做静态多态替代。别为了怕慢就不用虚函数那是过早优化。七、核心规则速查表维度规则多态实现虚函数 虚表 vptr虚表生成编译期每个类一张vptr 设置构造函数里逐层更新构造/析构里调虚函数退化为当前类版本不多态纯虚函数 0使类变抽象不可实例化开销查表间接调用每对象多一 vptr八、面试高频追问❓ Q1基类析构函数为什么要是虚函数✅ 当你用基类指针 delete 一个子类对象时若析构不是虚函数只会调基类析构子类析构不执行——子类资源泄漏。这是未定义行为的高发区。所以有虚函数的类析构几乎一定要虚。❓ Q2虚函数能内联吗✅ 语法上能加 inline但通常不会被内联——虚函数要查虚表编译期定不下来。例外编译期能确定实际类型时直接通过对象而非指针/引用调用编译器可能内联掉。❓ Q3虚函数表存在哪✅ 每个类一张是编译期生成的静态只读数据通常在 .rodata 段。所有该类对象共享这一张虚表只是各自的对象里有一个 vptr 指向它。❓ Q4构造函数能是虚函数吗✅不能。虚函数依赖 vptr而 vptr 要在构造函数里设置——构造还没开始vptr 还没就位逻辑上矛盾。所以语言直接禁止虚构造函数。❓ Q5override 和 virtual 写哪个✅ 派生类重写虚函数优先写 override让编译器检查签名。virtual 性质会沿继承链自动传递派生类不写 virtual 也是虚函数但 override 能挡住签名写错导致没重写的 bug见关键字篇 final/override。九、总结速查表概念一句话虚函数运行期决定的成员函数虚表 vtable每类一张存虚函数地址vptr每对象一个指向所属类虚表抽象类含纯虚函数不可实例化多态实现通过 vptr 查 vtable一句话回顾虚函数的本质是用虚表和 vptr 实现运行期决定调用。记住每类一张虚表、每对象一个 vptr、构造析构里调虚函数会退化、基类析构要虚这题就稳了。如果您觉得本篇内容对你有帮助欢迎点赞 、收藏 ⭐、转发 。下期我们聊聊运算符重载敬请关注
返回列表