virtual 关键字【C++ Language】

virtual 关键字【C++ Language】 目录1. 介绍2. 修饰非静态的成员函数2.1 虚函数与虚调用2.2 动态类型与多态对象2.3 override 说明符与 final 说明符2.4 重写override2.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/struct2. 修饰非静态的成员函数2.1 虚函数与虚调用由virtual修饰的非静态的成员函数被称为虚函数虚函数可以被派生类继承和重写override。通过基类类型的指针或引用来调用虚函数的行为被称为虚函数调用或虚调用。如果一个函数重写了另一个函数那么这两个函数都是虚函数#includeiostreamstructA{virtualfunc(){std::coutA::funcstd::endl;}};structB:A{voidfunc(){std::coutB::funcstd::endl;}};intmain(){A*bnewB();// B::funcb-func();return0;}2.2 动态类型与多态对象如果一个类至少声明了一个或至少继承了一个虚函数那么该类的对象就是多态对象。编译器会为每个多态对象存储一个额外的信息除非被优化了在现有的所有编译器中这个额外的信息就是一个指针详情参考本文的构造函数一节在运行时虚调用和RTTI特性会利用这个额外的信息来决定多态对象的类型这种类型也被称为动态类型。相应的对于非多态对象而言它的类型是在编译时由它所处的表达式所决定的这种类型也被称为静态类型#includeiostreamstructA{virtualvoidfunc(){std::coutA::funcstd::endl;}};structB:A{voidfunc(){std::coutB::funcstd::endl;}};intmain(){// 指针 b 是非多态对象, 其类型是在编译时由身处的表达式所决定, 即静态类型 A*A*bnewB();// B::func// 指针 b 所指向的对象是多态对象其类型是在运行时由虚调用所决定, 即动态类型 Bb-func();return0;}2.3 override 说明符与 final 说明符override 用于显式声明一个函数与另一个虚函数构成重写关系final 用于声明一个虚函数不能被重写或者一个类不能被继承override 和 final 都不是关键字因此可以用来做函数名或对象名#includeiostreamstructA{voidfunc1(){std::coutA::func1std::endl;}virtualvoidfunc2()final{std::coutA::func2std::endl;}virtualvoidfunc3(){std::coutA::func3std::endl;}voidoverride(){std::coutA::overridestd::endl;}voidfinal(){std::coutA::finalstd::endl;}};structB:A{// error: A::func1 is not a virtual function// void func1() override {// std::coutB::func1std::endl;// }// error: A::func2 can not be overridden// void func2() override {// std::coutB::func2std::endl;// }// The order of override and final can be arbitrary.virtualvoidfunc3()overridefinal{std::coutB::func3std::endl;}};intmain(){A*bnewB();// A::overrrideb-override();// A::finalb-final();}2.4 重写override2.4.1 普通的非静态的成员函数设 B 是 A 的派生类则 B 中的某个函数与 A 中的某个虚函数构成重写关系的充分条件如下需要全部满足函数名一致形参列表一致cv 限定一致ref 限定一致返回值类型一致或前者的返回值类型相对于后者的返回值类型而言是协变的covarient#includeiostreamstructA{virtualvoidfunc1(){std::coutA::func1std::endl;}virtualvoidfunc2()const{std::coutA::func2std::endl;}virtualvoidfunc3(){std::coutA::func3std::endl;}virtualvoidfunc4(){std::coutA::func4std::endl;}};structB:A{// 形参列表不一致virtualvoidfunc1(intx0){std::coutB::func1std::endl;}// cv 限定不一致virtualvoidfunc2(){std::coutB::func2std::endl;}// ref 限定不一致virtualvoidfunc3(){std::coutB::func3std::endl;}// ref 限定不一致virtualvoidfunc4(){std::coutB::func4std::endl;}};intmain(){A*bnewB();// 都是 A 的版本b-func1();b-func2();b-func3();std::move(*b).func4();}2.4.2 协变的covarient类型B 相对于 A 而言是协变的的充分条件如下需要全部满足两者都是指向类类型的指针类型或引用类型且不是多级指针A 所指向的类 Base是 B 所指向的类 Derived 的基类且 Base 对于 Derived 而言是可访问的即后者是公有继承、没有二义性的对 B 的 cv 限定程度不高于对 A 的 cv 限定程度#includeiostream// Part 1: public inheritancestructA{A(){std::coutAs constructorstd::endl;}};structB:A{B(){std::coutBs constructorstd::endl;}};// Part 2: unambiguous base classstructC{C(){std::coutCs constructorstd::endl;}};structD:virtualC{D(){std::coutDs constructorstd::endl;}};structE:virtualC{E(){std::coutEs constructorstd::endl;}};structF:D,E{F(){std::coutFs constructorstd::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*hnewH();// B 公有继承 Ah-func1();std::coutstd::endl;// C 对于 F 而言没有二义性h-func2();std::coutstd::endl;// 对 B* 的 cv 限定不高于对 A* 的 cv 限定h-func3();}2.4.3 析构函数如果把一个类的析构函数声明为虚函数那么它的所有派生类的析构函数都与之构成重写关系此时就可以通过delete基类指针来达到正确释放内存的目的。反之如果不把基类的析构函数定义为虚函数那么派生类的析构函数就无法和它构成重写关系此时通过delete基类指针来释放内存的行为就是未定义行为undefined behavior#includeiostream// Part 1: non-virtual destructorstructA{~A(){std::coutAs destructorstd::endl;}};structB:A{~B(){std::coutBs destructorstd::endl;}};// Part 2: virtual destructorstructC{virtual~C(){std::coutCs destructorstd::endl;}};structD:C{~D(){std::coutDs destructorstd::endl;}};intmain(){// undefined behaviorA*bnewB();deleteb;// Ds destructor// Cs destructorC*dnewD();deleted;}2.4.4 构造函数C 标准中并未提到过虚构造函数的概念而且也没有明确说明不能把构造函数声明为虚函数因此依赖于具体的编译器实现。鄙人已知的 C 编译器GCC、MSVC、LLVM/Clang中虚调用的实现都是通过一个被称为虚函数表的结构来完成的具体详情如下在编译时编译器会为每个包含虚函数的类创建一个虚函数表每个表项都是一个指向在该类中所定义的函数的指针。如果一个类没有重写基类的任何虚函数也没有定义新的虚函数那么它共享基类的虚函数表如果有的话在运行时编译器会为每个对象创建一个虚函数表指针如果没有被优化的话它的初始化发生在创建该对象的构造函数内。编译器会找到这个构造函数所属的类然后拿到这个类所对应的虚函数表最后把它的地址赋值给该对象的虚函数表指针虚调用的过程就是通过对象所拥有的虚函数表指针找到虚函数的地址然后再去调用这个虚函数创建一个类的对象就得先找到它的构造函数假设这个构造函数是虚函数那么要调用它就首先要通过虚函数表指针拿到包含它的地址的虚函数表而虚函数表指针初始化又是发生在构造函数中的因此无法找到这个类的构造函数假设不成立。也就是说对于具体的编译器实现而言是不允许把构造函数声明为虚函数的而实际上编译器也会报错2.5 关于虚函数的访问权限和可见性对构成重写关系的影响2.5.1 普通的非静态的成员函数对于访问权限而言即便在某个类中以私有方式private定义了一个虚函数在它的派生类中依旧能够定义一个函数与之构成重写关系只要满足构成重写的充分条件即可对于可见性而言假设存在一个继承关系链FDC且 D 中的某个函数与 C 中的某个虚函数构成隐藏hide关系即函数名一致但形参列表不同那么在 F 中仍能够定义一个函数与 C 中的这个虚函数构成重写关系。#includeiostream// Part 1: private memberstructA{voidhelper(){func();}private:virtualvoidfunc(){std::coutA::funcstd::endl;}};structB:A{voidfunc(){std::coutB::funcstd::endl;}};// Part 2: hidestructC{virtualvoidfunc(){std::coutC::funcstd::endl;}};structD:C{// D::func hides C::funcvoidfunc(intx0){std::coutD::funcstd::endl;}};structE:D{// E::func overrides C::funcvoidfunc(){std::coutE::funcstd::endl;}};intmain(){// 情形 1私有虚函数// B::funcA*bnewB();b-helper();std::coutstd::endl;// 情形2隐藏// E::funcC*e1newE();e1-func();std::coutstd::endl;// D::funcD*e2newE();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 析构函数虚析构函数若要被重写则必须对派生类而言是可访问的即必须声明为public或protected。在实际编程中对析构函数的声明有以下两种建议将基类析构函数声明为虚函数且为public不将基类析构函数声明为虚函数但访问权限设为protected读者可以自行写 demo 感受一下3. 修饰类的继承方式3.1 菱形继承所带来的二义性问题C 支持多重继承假设类 B, C 都继承于类 A而类 D 同时继承 B 和 C那么类 D 的对象中就会同时包含两份 A 的子对象如果此时要访问这个子对象就势必会产生歧义即二义性问题如果一个类 B 继承另一个类 A 的同时使用了virtual来修饰它继承类 A 的方式那么称这种关系为虚继承即 B 虚继承于 A假设类 B, C 都虚继承于类 A而类 D 同时继承于 B 和 C那么类 D 的对象中就只会包含一份 A 的子对象而 A 的构造函数也只会被执行一次#includeiostreamstructA{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::coutstd::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::coutstd::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 构造函数的执行顺序在包含虚继承关系的类的继承树中如果要创建某个类的对象那么构造函数的执行顺序如下虚基类的构造函数从上往下、从左往右基类的构造函数从上往下、从左往右非静态的成员变量从上往下当前类的构造函数的函数体其中从上往下是相对于类的继承树或成员变量的声明顺序而言的从左往右是相对于继承列表而言的#includeiostreamstructA1{A1(){std::coutA1s constructorstd::endl;}};structA2{A2(){std::coutA2s constructorstd::endl;}};structB1:virtualA1{// the constructor of virtual base class A1 is executed only once.B1():A1(){std::coutB1s constructorstd::endl;}};structC1:virtualA1{// the constructor of virtual base class A1 is executed only once.C1():A1(){std::coutC1s constructorstd::endl;}};structB2:virtualA2{B2(){std::coutB2s constructorstd::endl;}};structC2:virtualA2{C2(){std::coutC2s constructorstd::endl;}};structD1{D1(){std::coutD1s constructorstd::endl;}};structD2{D2(){std::coutD2s constructorstd::endl;}};structE:B1,C1{E(){std::coutEs constructorstd::endl;}};structF:C2,B2{F(){std::coutFs constructorstd::endl;}};structG:F,E{D1 d1;D2 d2;G(){std::coutGs constructorstd::endl;}};intmain(){// 1. virtual base classes// A2s constructor// A1s constructor// 2. base classes// C2s constructor// B2s constructor// Fs constructor// B1s constructor// C1s constructor// ES constructor// 3. non -tatic members// D1s constructor// D2s constructor// 4. the body of this constructor// Gs constructorG g;}4. 总结本文攘括了 C 标准中关于virtual的所有用法并详细介绍了关于虚调用和多态的概念以及编译器的实现细节