news 2026/8/30 6:13:03

virtual 关键字【C++ Language】

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
virtual 关键字【C++ Language】

目录

  • 1. 介绍
  • 2. 修饰非静态的成员函数
    • 2.1 虚函数与虚调用
    • 2.2 动态类型与多态对象
    • 2.3 override 说明符与 final 说明符
    • 2.4 重写(override)
      • 2.4.1 普通的非静态的成员函数
      • 2.4.2 协变的(covarient)类型
      • 2.4.3 析构函数
      • 2.4.4 构造函数
    • 2.5 关于虚函数的访问权限和可见性对构成重写关系的影响
      • 2.5.1 普通的非静态的成员函数
      • 2.5.2 析构函数
  • 3. 修饰类的继承方式
    • 3.1 菱形继承所带来的二义性问题
    • 3.2 构造函数的执行顺序
  • 4. 总结

1. 介绍

virtual关键字可以用来修饰非静态的成员函数,也可以用于声明一个类继承另一个类的方式
注意:C++ 中 class/struct/union 修饰的类型都被称为类,但 union 修饰的类型不能被其他类继承,也不能继承其它类,因此但凡提到一个类继承另一个类,前后者都是指 class/struct

2. 修饰非静态的成员函数

2.1 虚函数与虚调用

  1. virtual修饰的非静态的成员函数被称为虚函数,虚函数可以被派生类继承和重写(override)
  2. 通过基类类型的指针或引用来调用虚函数的行为被称为虚函数调用或虚调用
  3. 如果一个函数重写了另一个函数,那么这两个函数都是虚函数
#include<iostream>structA{virtualfunc(){std::cout<<"A::func"<<std::endl;}};structB:A{voidfunc(){std::cout<<"B::func"<<std::endl;}};intmain(){A*b=newB();// B::funcb->func();return0;}

2.2 动态类型与多态对象

  1. 如果一个类至少声明了一个或至少继承了一个虚函数,那么该类的对象就是多态对象。
  2. 编译器会为每个多态对象存储一个额外的信息(除非被优化了),在现有的所有编译器中,这个额外的信息就是一个指针,详情参考本文的构造函数一节
  3. 在运行时,虚调用和RTTI特性会利用这个额外的信息来决定多态对象的类型,这种类型也被称为动态类型。
  4. 相应的,对于非多态对象而言,它的类型是在编译时由它所处的表达式所决定的,这种类型也被称为静态类型
#include<iostream>structA{virtualvoidfunc(){std::cout<<"A::func"<<std::endl;}};structB:A{voidfunc(){std::cout<<"B::func"<<std::endl;}};intmain(){// 指针 b 是非多态对象, 其类型是在编译时由身处的表达式所决定, 即静态类型 A*A*b=newB();// B::func// 指针 b 所指向的对象是多态对象,其类型是在运行时由虚调用所决定, 即动态类型 Bb->func();return0;}

2.3 override 说明符与 final 说明符

  1. override 用于显式声明一个函数与另一个虚函数构成重写关系
  2. final 用于声明一个虚函数不能被重写,或者一个类不能被继承
  3. override 和 final 都不是关键字,因此可以用来做函数名或对象名
#include<iostream>structA{voidfunc1(){std::cout<<"A::func1"<<std::endl;}virtualvoidfunc2()final{std::cout<<"A::func2"<<std::endl;}virtualvoidfunc3(){std::cout<<"A::func3"<<std::endl;}voidoverride(){std::cout<<"A::override"<<std::endl;}voidfinal(){std::cout<<"A::final"<<std::endl;}};structB:A{// error: A::func1 is not a virtual function// void func1() override {// std::cout<<"B::func1"<<std::endl;// }// error: A::func2 can not be overridden// void func2() override {// std::cout<<"B::func2"<<std::endl;// }// The order of override and final can be arbitrary.virtualvoidfunc3()overridefinal{std::cout<<"B::func3"<<std::endl;}};intmain(){A*b=newB();// A::overrrideb->override();// A::finalb->final();}

2.4 重写(override)

2.4.1 普通的非静态的成员函数

设 B 是 A 的派生类,则 B 中的某个函数与 A 中的某个虚函数构成重写关系的充分条件如下(需要全部满足):

  1. 函数名一致
  2. 形参列表一致
  3. cv 限定一致
  4. ref 限定一致
  5. 返回值类型一致或前者的返回值类型相对于后者的返回值类型而言是协变的(covarient)
#include<iostream>structA{virtualvoidfunc1(){std::cout<<"A::func1"<<std::endl;}virtualvoidfunc2()const{std::cout<<"A::func2"<<std::endl;}virtualvoidfunc3()&{std::cout<<"A::func3"<<std::endl;}virtualvoidfunc4()&&{std::cout<<"A::func4"<<std::endl;}};structB:A{// 形参列表不一致virtualvoidfunc1(intx=0){std::cout<<"B::func1"<<std::endl;}// cv 限定不一致virtualvoidfunc2(){std::cout<<"B::func2"<<std::endl;}// ref 限定不一致virtualvoidfunc3(){std::cout<<"B::func3"<<std::endl;}// ref 限定不一致virtualvoidfunc4(){std::cout<<"B::func4"<<std::endl;}};intmain(){A*b=newB();// 都是 A 的版本b->func1();b->func2();b->func3();std::move(*b).func4();}

2.4.2 协变的(covarient)类型

B 相对于 A 而言是协变的的充分条件如下(需要全部满足):

  1. 两者都是指向类类型的指针类型或引用类型,且不是多级指针
  2. A 所指向的类 Base,是 B 所指向的类 Derived 的基类,且 Base 对于 Derived 而言是可访问的(即后者是公有继承)、没有二义性的
  3. 对 B 的 cv 限定程度,不高于对 A 的 cv 限定程度
#include<iostream>// Part 1: public inheritancestructA{A(){std::cout<<"A's constructor"<<std::endl;}};structB:A{B(){std::cout<<"B's constructor"<<std::endl;}};// Part 2: unambiguous base classstructC{C(){std::cout<<"C's constructor"<<std::endl;}};structD:virtualC{D(){std::cout<<"D's constructor"<<std::endl;}};structE:virtualC{E(){std::cout<<"E's constructor"<<std::endl;}};structF:D,E{F(){std::cout<<"F's constructor"<<std::endl;}};// Part 3: cv-qualified return typestructG{virtualA*func1(){returnnewA();}virtualC*func2(){returnnewC();}virtualconstA*func3(){returnnewA();}};structH:G{B*func1(){returnnewB();}F*func2(){returnnewF();}virtualB*func3(){returnnewB();}};intmain(){G*h=newH();// B 公有继承 Ah->func1();std::cout<<std::endl;// C 对于 F 而言没有二义性h->func2();std::cout<<std::endl;// 对 B* 的 cv 限定不高于对 A* 的 cv 限定h->func3();}

2.4.3 析构函数

  1. 如果把一个类的析构函数声明为虚函数,那么它的所有派生类的析构函数都与之构成重写关系,此时就可以通过delete基类指针来达到正确释放内存的目的。
  2. 反之,如果不把基类的析构函数定义为虚函数,那么派生类的析构函数就无法和它构成重写关系,此时通过delete基类指针来释放内存的行为就是未定义行为(undefined behavior)
#include<iostream>// Part 1: non-virtual destructorstructA{~A(){std::cout<<"A's destructor"<<std::endl;}};structB:A{~B(){std::cout<<"B's destructor"<<std::endl;}};// Part 2: virtual destructorstructC{virtual~C(){std::cout<<"C's destructor"<<std::endl;}};structD:C{~D(){std::cout<<"D's destructor"<<std::endl;}};intmain(){// undefined behaviorA*b=newB();deleteb;// D's destructor// C's destructorC*d=newD();deleted;}

2.4.4 构造函数

C++ 标准中并未提到过虚构造函数的概念,而且也没有明确说明不能把构造函数声明为虚函数,因此依赖于具体的编译器实现。鄙人已知的 C++ 编译器GCC、MSVC、LLVM/Clang中,虚调用的实现都是通过一个被称为虚函数表的结构来完成的,具体详情如下:

  1. 在编译时,编译器会为每个包含虚函数的类创建一个虚函数表,每个表项都是一个指向在该类中所定义的函数的指针。如果一个类没有重写基类的任何虚函数,也没有定义新的虚函数,那么它共享基类的虚函数表(如果有的话)
  2. 在运行时,编译器会为每个对象创建一个虚函数表指针(如果没有被优化的话),它的初始化发生在创建该对象的构造函数内。编译器会找到这个构造函数所属的类,然后拿到这个类所对应的虚函数表,最后把它的地址赋值给该对象的虚函数表指针
  3. 虚调用的过程,就是通过对象所拥有的虚函数表指针找到虚函数的地址,然后再去调用这个虚函数

创建一个类的对象就得先找到它的构造函数,假设这个构造函数是虚函数,那么要调用它就首先要通过虚函数表指针拿到包含它的地址的虚函数表,而虚函数表指针初始化又是发生在构造函数中的,因此无法找到这个类的构造函数,假设不成立。也就是说对于具体的编译器实现而言,是不允许把构造函数声明为虚函数的,而实际上编译器也会报错

2.5 关于虚函数的访问权限和可见性对构成重写关系的影响

2.5.1 普通的非静态的成员函数

  1. 对于访问权限而言,即便在某个类中以私有方式(private定义了一个虚函数,在它的派生类中依旧能够定义一个函数与之构成重写关系,只要满足构成重写的充分条件即可
  2. 对于可见性而言,假设存在一个继承关系链F==>D==>C,且 D 中的某个函数与 C 中的某个虚函数构成隐藏(hide)关系(即函数名一致但形参列表不同),那么在 F 中仍能够定义一个函数与 C 中的这个虚函数构成重写关系。
#include<iostream>// Part 1: private memberstructA{voidhelper(){func();}private:virtualvoidfunc(){std::cout<<"A::func"<<std::endl;}};structB:A{voidfunc(){std::cout<<"B::func"<<std::endl;}};// Part 2: hidestructC{virtualvoidfunc(){std::cout<<"C::func"<<std::endl;}};structD:C{// D::func hides C::funcvoidfunc(intx=0){std::cout<<"D::func"<<std::endl;}};structE:D{// E::func overrides C::funcvoidfunc(){std::cout<<"E::func"<<std::endl;}};intmain(){// 情形 1:私有虚函数// B::funcA*b=newB();b->helper();std::cout<<std::endl;// 情形2:隐藏// E::funcC*e1=newE();e1->func();std::cout<<std::endl;// D::funcD*e2=newE();e2->D::func();// error: The unqualified name lookup, which happens at compile time, only finds D::func in class D and thus does not examine the base class C// e2->func();}

2.5.2 析构函数

虚析构函数若要被重写,则必须对派生类而言是可访问的,即必须声明为publicprotected。在实际编程中对析构函数的声明有以下两种建议:

  1. 将基类析构函数声明为虚函数,且为public
  2. 不将基类析构函数声明为虚函数,但访问权限设为protected

读者可以自行写 demo 感受一下

3. 修饰类的继承方式

3.1 菱形继承所带来的二义性问题

  1. C++ 支持多重继承,假设类 B, C 都继承于类 A,而类 D 同时继承 B 和 C,那么类 D 的对象中,就会同时包含两份 A 的子对象,如果此时要访问这个子对象就势必会产生歧义,即二义性问题
  2. 如果一个类 B 继承另一个类 A 的同时使用了virtual来修饰它继承类 A 的方式,那么称这种关系为虚继承,即 B 虚继承于 A
  3. 假设类 B, C 都虚继承于类 A,而类 D 同时继承于 B 和 C,那么类 D 的对象中,就只会包含一份 A 的子对象,而 A 的构造函数也只会被执行一次
#include<iostream>structA{intn;};structB1:A{};structC1:A{};structD1:B1,C1{};structB2:virtualA{};structC2:virtualA{};structD2:B2,C2{};structD3:B2,C2,A{};intmain(){D1 d1;std::cout<<&(d1.B1::n)<<std::endl;std::cout<<&(d1.C1::n)<<std::endl;// error1:// object d1 has two sub-objects of type A// one that is for B1 and one that is for C1// std::cout<<&(d.n)<<std::endl;// std::cout<<&(d.A::n)<<std::endl;std::cout<<std::endl;D2 d2;// OK:// object d2 has only one sub-object of type A// so all the addresses printed below are the same.std::cout<<&(d2.B2::n)<<std::endl;std::cout<<&(d2.C2::n)<<std::endl;std::cout<<&(d2.n)<<std::endl;std::cout<<&(d2.A::n)<<std::endl;std::cout<<std::endl;D3 d3;// OK:// although object d3 has two sub-objects of type A// one of them is shared by B2 and C2std::cout<<&(d3.B2::n)<<std::endl;std::cout<<&(d3.C2::n)<<std::endl;// error2: same as error1// std::cout<<&(d3.n)<<std::endl;// std::cout<<&(d3.A::n)<<std::endl;}

3.2 构造函数的执行顺序

在包含虚继承关系的类的继承树中,如果要创建某个类的对象,那么构造函数的执行顺序如下:

  1. 虚基类的构造函数(从上往下、从左往右)
  2. 基类的构造函数(从上往下、从左往右)
  3. 非静态的成员变量(从上往下)
  4. 当前类的构造函数的函数体

其中:从上往下是相对于类的继承树或成员变量的声明顺序而言的,从左往右是相对于继承列表而言的

#include<iostream>structA1{A1(){std::cout<<"A1's constructor"<<std::endl;}};structA2{A2(){std::cout<<"A2's constructor"<<std::endl;}};structB1:virtualA1{// the constructor of virtual base class A1 is executed only once.B1():A1(){std::cout<<"B1's constructor"<<std::endl;}};structC1:virtualA1{// the constructor of virtual base class A1 is executed only once.C1():A1(){std::cout<<"C1's constructor"<<std::endl;}};structB2:virtualA2{B2(){std::cout<<"B2's constructor"<<std::endl;}};structC2:virtualA2{C2(){std::cout<<"C2's constructor"<<std::endl;}};structD1{D1(){std::cout<<"D1's constructor"<<std::endl;}};structD2{D2(){std::cout<<"D2's constructor"<<std::endl;}};structE:B1,C1{E(){std::cout<<"E's constructor"<<std::endl;}};structF:C2,B2{F(){std::cout<<"F's constructor"<<std::endl;}};structG:F,E{D1 d1;D2 d2;G(){std::cout<<"G's constructor"<<std::endl;}};intmain(){// 1. virtual base classes// A2's constructor// A1's constructor// 2. base classes// C2's constructor// B2's constructor// F's constructor// B1's constructor// C1's constructor// E'S constructor// 3. non -tatic members// D1's constructor// D2's constructor// 4. the body of this constructor// G's constructorG g;}

4. 总结

本文攘括了 C++ 标准中关于virtual的所有用法,并详细介绍了关于虚调用和多态的概念以及编译器的实现细节

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/30 6:12:07

AI取代程序员?真正危险的是任务重组,开发者需掌握AI工程化

比尔盖茨警告AI或致大规模失业&#xff0c;但真正让开发者失业的&#xff0c;可能不是AI本身过去半年&#xff0c;关于“AI会取代程序员”“大模型让初级开发岗消失”的讨论几乎没停过。最近比尔盖茨又公开警告&#xff1a;AI可能导致大规模失业&#xff0c;人类需要适应新的工…

作者头像 李华
网站建设 2026/8/30 6:09:10

从蛛网膜下腔出血到血脑屏障模型:云克隆大鼠脑膜细胞原代产品的多场景科研实战

中枢神经系统脑膜由硬脑膜、蛛网膜与软脑膜共同构成&#xff0c;是大脑重要的物理屏障、免疫边界与信号调控微环境。既往神经领域研究多聚焦神经元、小胶质细胞、星形胶质细胞&#xff0c;而脑膜细胞在脑损伤、蛛网膜下腔出血、中枢免疫调控、神经发育中的价值&#xff0c;在近…

作者头像 李华
网站建设 2026/8/30 6:07:27

RealDiff:PR阶段的运行时行为差异对比工具,弥补静态diff盲区

这次我们来看一个作用于 pull request 的工程质量工具&#xff1a;RealDiff。它不是普通的静态代码 diff&#xff0c;而是把运行时行为也纳入对比范围&#xff0c;适合在代码评审和合并前检查阶段使用。项目发布在 Show HN 上&#xff0c;主打 runtime behavior diffing&#x…

作者头像 李华
网站建设 2026/8/30 6:05:55

网页APP暗黑设计套路:从隐私泄露到强制消费,逐一破解底层逻辑

产品设计中的暗黑模式并非UI深色主题&#xff0c;而是平台通过界面、流程、文案的精细化刻意设计&#xff0c;利用人类与生俱来的认知惰性与行为弱点&#xff0c;诱导用户做出注册、付费、授权、捆绑消费等非主观意愿行为的恶意设计套路。这类模式普遍存在于网页、APP全场景&am…

作者头像 李华
网站建设 2026/8/30 6:05:45

迅雷C++校招笔试A卷深度解析:内存管理、STL容器与编程题实战

迅雷校园招聘的C在线笔试&#xff0c;是我当年刷校招时印象最深的一套题之一。倒不是说它有多难&#xff0c;而是它把C岗真正要用的东西几乎都覆盖了——指针、内存管理、STL容器、算法思维、还有工程上那些让人抓狂的细节坑。现在回头看&#xff0c;A卷这套题其实非常典型&…

作者头像 李华