You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C++虚继承中virtual关键字的深层作用解析(附示例代码与汇编结果疑问)

C++虚继承中virtual关键字的深层作用解析(附示例代码与汇编结果疑问)

我太懂你这种困惑了——网上讲C++虚继承的文章十个有九个死抠“菱形继承问题”,翻来覆去说它能让基类只存一份拷贝,但真遇到单继承里加了virtual继承的情况,瞬间就懵了:这关键字到底在这儿干了啥?为啥我测出来运行结果、汇编代码都没啥区别?

先给你拍板:你的测试代码确实太简单了,单继承+没触发多路径继承的场景下,虚继承的核心作用根本发挥不出来,所以加不加virtual看起来没区别很正常。

一、先搞懂:虚继承的“本职工作”根本不是单继承场景

虚继承设计出来的核心目的,是解决**多路径继承(也就是大家常说的菱形继承)**的坑:比如你有Base,然后A和B都继承Base,最后C继承A和B——这时候如果不用虚继承,C的对象里会有两份Base的实例,调用Base的成员时会产生二义性,还会浪费内存。

而你写的代码是Derived直接virtual public Base,这是单继承,本来Derived里就只会有一份Base实例,所以虚继承的“去重”功能完全没地方发挥。再加你没重写f(),不管是不是虚继承,Base*指向Derived时调用f(),都会走到Base::f(),运行结果当然一模一样。

二、单继承里加虚继承,底层真的没变化吗?

表面看运行结果和大部分汇编代码都一样,但编译器其实已经偷偷做了手脚——只是这些手脚在你的测试逻辑里没被触发而已:

虚继承的底层实现,编译器会给派生类对象添加一个虚基类表指针(vbtable),用来记录当前对象到虚基类实例的偏移量。在多继承场景下,这个偏移量能让所有路径的派生类都找到同一个Base实例;但在单继承场景下,这个偏移量是固定的(比如0,因为Base就在Derived对象的开头),所以即使有vbtable,实际调用时也不会影响逻辑,汇编代码看起来就和普通继承几乎一致。

咱们拆解你贴的汇编代码:

普通继承(Without virtual)的Derived构造函数

Derived::Derived() [base object constructor]:
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call Base::Base() [base object constructor]  # 先调用基类构造
mov edx, OFFSET FLAT:vtable for Derived+16
mov rax, QWORD PTR [rbp-8]
mov QWORD PTR [rax], rdx  # 再设置自己的虚表指针
nop
leave
ret

虚继承(With virtual)的Derived构造函数核心差异

虚继承时,构造函数的逻辑本质是:虚基类的构造由最顶层的派生类负责调用(单继承里最顶层就是Derived,所以还是它调用,但编译器会通过vbtable定位基类位置)。虽然最终还是调用了Base::Base(),但对象布局里会多一个vbtable指针——只是在你这个场景下,这个指针的存在不会影响任何可见的运行结果,所以汇编看起来和普通继承几乎一样。

三、什么时候能看出虚继承的区别?给你改个例子

把代码改成经典的菱形继承,你瞬间就能看到差异:

#include <iostream>
class Base {
public:
    int x = 10;
    virtual ~Base() {}
    virtual void f() { std::cout << "Base.f()" << std::endl; }
};

class A : public virtual Base {};
class B : public virtual Base {};
class C : public A, public B {};

int main() {
    C c;
    // 如果不用虚继承,这里会报错:二义性,不知道是A::x还是B::x
    std::cout << c.x << std::endl;
    return 0;
}

这个例子里,要是A和B不用virtual继承Base,C的对象里会有两份Base实例,访问c.x直接编译报错;加了虚继承后,C里只有一份Base,就能正常访问。

四、回到你的测试代码:为什么汇编看起来没区别?

你用Godbolt看的汇编里,两种情况的差异其实藏得很深:

  • 虚继承的Derived对象,编译器会生成虚基类表(vbtable),用来存储基类的偏移量(你可以在Godbolt里开启更详细的输出,能看到这个表);
  • 普通继承的Derived则没有这个表,直接用固定的偏移访问基类。

但在单继承场景下,这个偏移量是0(基类就在对象开头),所以不管用不用vbtable,最终的内存访问逻辑完全一样,汇编代码自然看起来没区别。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 08:29:31