C++ 类的继承与多态机制,从语法到内存布局(含虚函数详解)
文章目录继承一、单继承基础结构成员调用1.基础成员访问规则2.隐藏问题继承本质注意事项二、多继承基础结构同名成员二义性问题菱形继承三、虚继承虚基表Virtual Base Table解决菱形继承总结多态虚函数/虚函数表虚函数虚函数表小结运行时多态如果不使用虚函数会怎样虚析构问题前言本文是基于GCC编译器进行讲解的不同编译器如MSVC在部分地方实现方式上可能有所不同继承C继承是面向对象编程的核心特性之一允许一个类派生类基于另一个类基类来构建实现代码复用和层次化设计。通过继承派生类自动获得基类的成员变量和成员函数并可添加新功能或重写基类方法。一、单继承基础结构#includeiostream#includefstreamusingnamespacestd;classBase{public:Base(){cout调用Base构造函数endl;}inta1;intb2;~Base(){cout调用Base析构函数endl;}};classDerived:publicBase{public:Derived(){cout调用Derived构造函数endl;}intc3;~Derived(){cout调用Dervice析构函数endl;}};intmain(){Derived d;return0;}输出结果在创建对象时先调用父类的构造函数再调用自身的构造函数而销毁时则是先调用自身的析构函数再调用父类析构函数。操作顺序原因构造父类 → 子类子类依赖父类父类要先准备好析构子类 → 父类子类先结束避免访问已销毁的父类资源成员调用1.基础成员访问规则classBase{public:intpublic_var10;protected:intprotected_var20;private:intprivate_var30;};classDerived:publicBase{public:voidaccess_check(){public_var100;// ✅ 可访问public继承保留public权限protected_var200;// ✅ 可访问protected成员对派生类可见// private_var 300; // ❌ 编译错误基类private成员不可见}};intmain(){Derived d;d.public_var1000;// ✅ 外部可访问// d.protected_var 2000; // ❌ 编译错误protected成员外部不可见}2.隐藏问题classBase{public:voidfunc(intx){coutBase::funcendl;}};classDerived:publicBase{public:// 隐藏基类同名函数参数不同也会隐藏voidfunc(){coutDerived::funcendl;}};intmain(){Derived d;d.func();// ✅ 调用Derived::func()// d.func(100); // ❌ 编译错误基类func(int)被隐藏d.Base::func(100);// ✅ 显式指定基类版本}继承本质内存结构Derived 对象: |- Base 子对象 |- a |- b |- Derived 新增成员 |- c继承本质为将父类内容拷贝一份到自身类当中,子类对象包含父类对象的所有成员变量不管是 public / protected / private。很多人认为子类对象不继承父类的私有成员其实这是不对的#includeiostreamusingnamespacestd;classBase{private:intsecret42;public:intgetSecret(){returnsecret;}};classDerived:publicBase{public:voidshowSecret(){// cout secret endl; // 编译错误不能直接访问 privatecoutAccess via Bases public method: getSecret()endl;}};intmain(){Derived d;d.showSecret();// 输出: 42return0;}输出结果注意事项注在继承当中子类对象创建时会隐式调用父类的无参构造因此如果你在父类中写了带参的构造函数要手动实现一个无参构造(当手动写了带参构造编辑器就不会自动生成默认无参构造函数了)或者显示调用自己写的带参构造如下classBase{public:Base(intx){coutBase构造函数, x xendl;}};classDerived:publicBase{public:Derived(inty):Base(y){// 通过初始化列表显式调用父类带参构造coutDerived构造函数, y yendl;}};二、多继承基础结构classBase1{public:inta;};classBase2{public:intb;};classDerived:publicBase1,publicBase2{public:intc;};内存结构Derived 对象: |- Base1 子对象 |- a |- Base2 子对象 |- b |- Derived 新增成员 |- c多继承和上文提到的单继承基本一致但有些问题需要注意见下文同名成员二义性问题在 C 中使用多继承multiple inheritance时如果多个基类中包含同名的成员变量或函数派生类就会出现二义性ambiguity即编译器无法确定你指的是哪个基类的成员这就是同名成员的二义性问题。#includeiostreamusingnamespacestd;classA{public:intx10;voidprint(){coutA::print()endl;}};classB{public:intx20;voidprint(){coutB::print()endl;}};classC:publicA,publicB{};上述代码中A与B有同名成员当直接在C中访问x或print()的时候会编译报错因为编译器不知道该调用哪个类的成员intmain(){C obj;// cout obj.x; // ❌ 错误x 不明确// obj.print(); // ❌ 错误print() 不明确// 正确写法coutobj.A::xendl;// ✅ 10coutobj.B::xendl;// ✅ 20obj.A::print();// ✅ 调用 A 的 printobj.B::print();// ✅ 调用 B 的 print}菱形继承菱形继承问题的本质在于多个派生类从同一个基类继承时最终派生类会重复继承该基类导致基类成员在内存中存在多份从而引发二义性、数据冗余以及资源管理混乱的问题。classA{public:intdata;};classB:publicA{public:voidfb(){coutdata;}};classC:publicA{public:voidfc(){coutdata;}};classD:publicB,publicC{};// 菱形继承intmain(){D d;// d.data 10; // 编译错误ambiguous accessd.B::data10;// 修改 B 路径的 A::datad.C::data20;// 修改 C 路径的 A::datacoutd.B::data;// 输出 10coutd.C::data;// 输出 20return0;}所谓“菱形”指的是继承结构像个菱形A / \ B C \ / D类 B 和 C 都继承自 A,类 D同时继承自 B 和 C于是 D 间接继承了两次 A内存布局D 对象: |- B 部分 |- A 子对象 |- data |- C 部分 |- A 子对象 |- data该问题可通过虚继承解决三、虚继承虚继承virtual inheritance是 C 中为了解决菱形继承钻石继承问题而引入的一种机制在讲虚继承之前先讲讲虚基表因为虚继承实际上是基于虚基表进行的虚基表Virtual Base Table虚基表是C实现虚继承机制的关键数据结构每个​​包含虚基类的类​​会生成自己的虚基表用于存储虚基类在派生类对象中的位置信息。虚基表vbtable主要用于解决以下问题​​共享虚基类实例​​确保在菱形继承中虚基类只有一份拷贝动态定位​​在运行时确定虚基类子对象的位置​​指针调整​​正确进行派生类指针到虚基类指针的转换先简单提提一下概念性内容下面我将基于前文讲讲虚继承是怎么利用虚基表解决菱形继承问题的解决菱形继承虚继承代码结构class A{public:intdata;};class B:virtual public A{public:voidfb(){coutdata;}};class C:virtual public A{public:voidfc(){coutdata;}};class D:public B,public C{};intmain(){D d;d.data10;// ✅ 不再歧义coutd.data;// ✅ 输出 10return0;}内存结构变化普通继承 D 对象: |- B 部分 |- A 子对象 |- data |- C 部分 |- A 子对象 |- data 虚继承后 D对象: |- B子对象 |- vbptr指向vbtable→ A子对象偏移量 |- C子对象 |- vbptr指向vbtable→ A子对象偏移量 |- 共享的 A子对象唯一的一份编译器为 B 和 C 增加了一个vbptr虚基表指针vbptr指向一个vbtablevbtable告诉编译器A 的子对象在 D 对象内的偏移量所以无论通过 B 还是 C最终都会访问同一个 A从而解决了菱形继承会重复继承祖父类的情况总结普通继承不加 virtual虚继承加 virtualA子对象存在两份A子对象只存在一份成员访问有歧义成员访问统一无歧义需要显式指定路径可直接访问内存占用更大占用更少多态多态Polymorphism是面向对象编程的三大特性之一字面意思是“多种形态”。在 C 中多态允许我们通过相同的接口调用不同的行为。这让程序结构更灵活、更具扩展性。C 支持两类多态编译时多态静态多态如函数重载、模板运行时多态动态多态依赖继承 虚函数本节我们重点介绍运行时多态。虚函数/虚函数表介绍多态前要介绍一个很重要的概念——虚函数虚函数定义在 C 中如果一个类的成员函数前加上virtual关键字那么这个函数就是虚函数。classBase{public:virtualvoidshow(){std::coutBase::show()std::endl;}};虚函数的意义在于支持运行时多态也叫动态绑定、动态分派即通过基类指针或引用调用派生类的重写方法。此外还有纯虚函数、抽象类 这里不过多赘述以一张表的形式总结类型定义是否支持实例化虚函数virtual普通虚函数有实现✅纯虚函数 0没有实现子类必须重写❌抽象类至少包含一个纯虚函数的类❌虚函数表定义为了实现运行时的动态分派编译器会为每个含有虚函数的类生成一张虚函数表vtable。每一个含有虚函数的类都有一个vtable每一个对象中会有一个虚指针vptr它指向所属类的vtablevtable中存储的是该类中虚函数的地址调用虚函数时实际是通过vptr找到vtable再找到正确的函数地址去调用。假设有以下代码结构classBase{public:virtualvoidf1(){std::coutBase::f1()\n;}virtualvoidf2(){std::coutBase::f2()\n;}};classDerived:publicBase{public:voidf1()override{std::coutDerived::f1()\n;}};对于Basevtable_Base: ----------- | Base::f1 | | Base::f2 | -----------对于Derivedvtable_Derived: -------------- | Derived::f1 | | Base:: f2 | 没有重写函数继承来自父类的函数 --------------虚函数表初始化时先存储自己的函数多态的时候通过虚指针(vptr)动态调整指向注虚函数表只会存储带virtual的函数因此带virtual的函数才会实现运行时多态小结虚函数表vtable每个含有虚函数的类包括直接声明或继承得到虚函数在编译时会拥有一张由编译器生成的虚函数表用于支持多态。虚函数指针vptr每个类中含有虚函数的对象实例在其内部都会包含一个vptr虚函数指针它指向该类的虚函数表。vptr 的存在是为每个对象提供运行时多态能力。运行时多态要实现运行时多态C 要求满足以下三个条件基类中定义虚函数使用virtual关键字派生类对该虚函数进行重写override示例代码#includeiostreamusingnamespacestd;classAnimal{public:virtualvoidspeak(){// 虚函数coutAnimal speaksendl;}};classDog:publicAnimal{public:voidspeak()override{coutDog barksendl;}};voidmakeSound(Animala){a.speak();// 多态调用}intmain(){Dog d;makeSound(d);// 输出Dog barks}运行结果结构分析对象d (构造中): ------------------- | vptr | ──── 暂时指向 Animal 的虚函数表 ------------------- │ 重新调整指向 │ ↓ Dog的vtable: ------------------- | type_info | ------------------- | Dog::speak() | → 输出Dog barks (覆盖Animal的实现) -------------------对象d的虚函数指针(vptr)从原来指向Animal的虚函数表到指向Dog的虚函数表,因此调用的是Dog中的函数如果不使用虚函数会怎样示例代码#includeiostreamusingnamespacestd;classAnimal{public:voidspeak(){coutAnimal speaksendl;}};classDog:publicAnimal{public:voidspeak(){coutDog barksendl;}};voidmakeSound(Animala){a.speak();// 调用}intmain(){Dog d;makeSound(d);// 输出Animal speaks}结果在这个例子中speak() 方法​​没有​​被声明为virtual因此不会触发多态行为Animal::speak()和Dog::speak()都是普通成员函数调用在编译时静态绑定因此虽然传递的是 Dog 对象但使用 Animal 会发生对象切片导致派生类特有的部分被切掉的情况。虚析构问题在 C 中如果你打算通过“基类指针”来释放“派生类对象”基类的析构函数必须是虚的否则会导致派生类资源不会被释放造成内存泄漏或程序行为异常。场景复现#includeiostreamusingnamespacestd;classBase{public:~Base(){coutBase::~Base()\n;}};classDerived:publicBase{public:~Derived(){coutDerived::~Derived()\n;}};intmain(){Base*pnewDerived();// 用基类指针指向派生类对象deletep;// 只会调用 Base::~Base()}正确做法#includeiostreamusingnamespacestd;classBase{public://进行虚析构virtual~Base(){coutBase::~Base()\n;}};classDerived:publicBase{public:~Derived(){coutDerived::~Derived()\n;}};intmain(){Base*pnewDerived();deletep;}输出Derived::~Derived() Base::~Base()