2026/1/23 23:40:06
网站建设
项目流程
网站制作公司昆明,国内炫酷的网站首页,企业局域网的规划与设计,个人或主题网站建设实验体会这篇blog我写了一天一夜, 但是我初学时花了好几个月才学懂 , 我想告诉你, 我那个时候也非常痛苦, 数次想要放弃, 我花了好几天时间才搞懂虚函数原理, 我真是个很笨的人, 只能花很多时间一点一点理解这些东西.
学习C的过程很艰辛, 不要被困难打倒, 一定要坚持下去!没有任何困难是…这篇blog我写了一天一夜, 但是我初学时花了好几个月才学懂 , 我想告诉你, 我那个时候也非常痛苦, 数次想要放弃, 我花了好几天时间才搞懂虚函数原理, 我真是个很笨的人, 只能花很多时间一点一点理解这些东西.学习C的过程很艰辛, 不要被困难打倒, 一定要坚持下去!没有任何困难是克服不了的--------2025,12,17,深夜继承的概念C 继承是面向对象编程OOP的三大核心特性封装、继承、多态之一其核心目的是代码复用和层次化设计允许从已有类基类 / 父类派生出新类派生类 / 子类子类可复用父类的属性和方法同时扩展自身独有的功能。继承的设计原则里氏替换原则子类必须能替换父类public继承满足 “is-a” 关系单一继承优先多继承易引发二义性优先用单一继承 接口纯虚函数类最小权限原则继承方式选择最严格的如无需外部访问基类成员用protected继承。章节一. 继承基本语法// 基类父类 class 基类名 { // 成员属性、方法 }; // 派生类子类 class 派生类名 : 继承方式 基类名 { // 派生类新增成员 };基类被继承的已有类如Person派生类基于基类扩展的新类如Student类成员权限: 也分为public、protected、privatepublic成员: 类内外都可访问, 派生类也可以访问private成员 外部也不可访问private成员, 同时, 无论哪种继承方式派生类都无法直接访问可通过基类的public/protected成员函数间接访问protected成员派生类内部可访问外部如main函数不可访问实际开发中public继承是最常用的符合 “is-a” 关系如 “学生是一个人”protected/private继承仅在特殊场景使用。子类继承方式决定基类成员在派生类中的访问权限分为public、protected、private默认private。继承方式会约束基类成员在派生类内部和外部的可见性核心规则如下表格更清晰基类成员权限public 继承protected 继承private 继承publicpublicprotectedprivateprotectedprotectedprotectedprivateprivate不可见不可见不可见继承原则父类的私有元素无法被子类直接访问, 但是子类是继承了父类的私元素, 只是没有访问权限继承方式1. 公共继承父类的元素被继承到子类中不会改变其访问权限2. 保护继承父类的公共元素和保护元素都会被作为保护元素被继承3. 私有继承父类中的公共元素和保护元素都会被继承为私有元素四个注意事项:1. 父类中的静态成员无法被子类继承, 因为父类和子类会共用静态成员;2. 对象的模型可以在windows的cmd窗口查看;3. 友元关系不能继承基类的友元函数 / 类无法访问派生类的私有成员4. 构造 / 析构 / 赋值运算符不能继承派生类需自定义若要复用基类逻辑可在派生类中显式调用示例public 继承最常用#include iostream using namespace std; // 基类人 class Person { public: string name; // 公有成员 protected: int age; // 保护成员 private: string id; // 私有成员 public: void setId(string s) { id s; } // 间接访问私有成员 }; // 派生类学生public继承Person class Student : public Person { public: int score; // 新增成员 void show() { name 张三; // 可访问基类public成员 age 18; // 可访问基类protected成员 // id 123456; // 错误基类private成员不可直接访问 setId(123456);// 正确通过基类public函数间接访问 score 90; } }; int main() { Student s; s.name 李四; // 正确public继承后name仍为public // s.age 20; // 错误protected成员外部不可访问 // s.id 654321;// 错误基类private成员外部不可见 s.show(); return 0; }C 继承派生类的构造、拷贝构造、赋值重载、析构0. 派生类的构造与析构顺序:派生类对象的生命周期中构造和析构遵循固定顺序核心规则1. 构造顺序从父到子基类构造函数 → 派生类的成员对象构造函数若有 → 派生类构造函数2. 析构顺序从子到父派生类析构函数 → 派生类的成员对象析构函数若有 → 基类析构函数若派生类包含其他类的成员对象如class A { B b; };构造顺序为基类构造 → 成员对象构造 → 派生类构造析构顺序相反。1. 派生类的构造函数派生类实例化时需同时调用自身构造函数 基类构造函数规则如下若基类提供默认构造函数无参、带默认参数、编译器自动生成派生类构造函数无需显式调用基类构造编译器会自动调用。若基类仅提供非默认构造函数无默认参数的有参构造必须在派生类构造函数的初始化列表中显式调用基类构造且该调用需放在初始化列表的首位语法强制要求。// 基类仅非默认构造 class Person { private: string _name; public: Person(const string name) : _name(name) {} // 非默认构造 }; // 派生类 class Student : public Person { private: string _address; int _stuNum; public: // 初始化列表首位调用基类构造再初始化自身成员 Student(const string name, const string addr, int num) : Person(name) // 必须显式调用基类非默认构造首位 , _address(addr) , _stuNum(num) { // 构造函数体仅处理非初始化列表的逻辑 } };无法被继承的基类:基类构造函数被声明为private派生类无法访问基类构造因此无法实例化等价于禁止继承。基类声明时加final关键字C11class Person final {};编译器直接禁止该类被继承编译报错。2. 派生类的拷贝构造默认情况系统自带浅拷贝一般无需手动编写。手动编写的场景派生类成员包含堆区手动分配的内存new避免浅拷贝问题。实现方式初始化列表中直接将派生类对象传入基类拷贝构造利用 “父类指针 / 引用可接收子类对象” 的特性。Student(const Student s) : Person(s) // 调用基类拷贝构造 , _address(s._address) , _num(s._num) {}3. 派生类的赋值重载operator默认赋值重载编译器自动生成浅拷贝成员包含堆区内存会导致浅拷贝问题需手动重写。名字隐藏派生类的operator会隐藏基类的operator需通过基类名::显式调用基类赋值重载。自赋值判断必须先判断this ! s避免自赋值导致的堆内存提前释放。返回值要求返回*this的引用Student支持链式赋值如s1 s2 s3。Student operator(const Student s) { // 第一步避免自赋值 if (this s) { return *this; } // 第二步调用基类赋值重载拷贝基类成员 Person::operator(s); // 第三步派生类成员深拷贝释放旧堆内存→重新分配→拷贝内容 delete[] _address; // 释放当前对象的旧堆内存 _address new char[strlen(s._address) 1]; strcpy(_address, s._address); _stuNum s._stuNum; // 第四步返回自身引用支持链式赋值 return *this; }拷贝构造和赋值重载的核心差异拷贝构造是 “创建新对象”赋值重载是 “给已有对象赋值”前者无需释放旧内存后者需先释放再拷贝。名字隐藏name hiding派生类中定义的成员函数若与基类成员函数同名无论参数列表函数签名是否不同基类的该函数都会被隐藏。4. 派生类的析构函数1. 执行顺序析构顺序与构造相反派生类析构函数体执行 → 派生类成员对象析构 → 基类析构函数执行基类析构由编译器自动调用无需手动触发。2. 禁止行为严禁手动调用基类析构函数如Person::~Person();编译器会对析构函数名做统一修饰如_ZN6PersonD1Ev手动调用会导致析构函数被重复执行引发内存错误。5. 基类的虚析构当基类指针 / 引用指向派生类对象时若基类析构非虚函数delete指针仅会调用基类析构派生类析构不执行导致堆内存泄漏将基类析构声明为virtual可触发动态绑定先执行派生类析构再执行基类析构。基类析构加virtual后派生类析构无论是否加virtual都会与基类析构构成重写编译器统一修饰析构函数名为destructor满足重写的函数签名要求。若类作为基类使用建议默认将析构声明为虚析构即使无堆内存仅增加一个虚函数表指针的开销避免内存泄漏风险。class Person { public: virtual ~Person() { // 虚析构 cout Person::~Person() endl; } }; class Student : public Person { private: char* _address; public: ~Student() { // 自动继承虚属性无需加virtual delete[] _address; cout Student::~Student() endl; } }; // 测试基类指针指向派生类对象 int main() { Person* p new Student(张三, 北京, 1001); delete p; // 先执行Student::~Student()再执行Person::~Person() return 0; }继承中的同名成员处理隐藏hide规则派生类中定义的同名的成员变量 / 函数若与基类同名的成员变量 / 函数同名无论参数列表函数签名是否不同基类的该函数都会被隐藏。1. 同名成员变量需通过基类名::显式访问基类的同名变量class Base { public: int num 10; }; class Derived : public Base { public: int num 20; // 隐藏基类num void show() { cout 派生类num num endl; // 20 cout 基类num Base::num endl; // 10 } };2. 同名成员函数即使参数列表不同基类函数也会被隐藏需通过基类名::访问基类函数class Base { public: void func() { cout Base::func() endl; } void func(int x) { cout Base::func(int) endl; } }; class Derived : public Base { public: void func() { cout Derived::func() endl; } // 隐藏基类所有func void test() { func(); // 调用派生类func() // func(10); // 错误基类func(int)被隐藏 Base::func(10); // 正确显式调用基类func(int) } };继承中同名静态成员的访问规则核心原则继承中 ** 同名静态成员变量 / 函数** 的处理方式与非静态同名成员一致子类的同名静态成员会隐藏父类的同名静态成员。访问子类的同名静态成员直接访问即可。访问父类的同名静态成员必须通过 ** 作用域限定符父类名::** 指定。子类同名静态成员会隐藏父类的访问父类成员必须加父类名::作用域静态成员属于类本身更推荐通过类名 作用域的方式访问符合静态成员的特性。1. 访问同名静态成员变量1通过对象访问cout 通过对象访问 endl; Son s; // 访问子类的静态成员变量m_A cout Son 下 m_A s.m_A endl; // 访问父类的静态成员变量m_A加父类作用域 cout Base 下 m_A s.Base::m_A endl;2通过类名访问静态成员属于类更推荐此方式cout 通过类名访问 endl; // 访问子类的静态成员变量m_A类名::成员 cout Son 下 m_A Son::m_A endl; // 访问父类的静态成员变量m_A子类名::父类名::成员 cout Base 下 m_A Son::Base::m_A endl;2. 访问同名静态成员函数1通过对象访问cout 通过对象访问 endl; Son s; // 访问子类的静态成员函数func() s.func(); // 访问父类的静态成员函数func()加父类作用域 s.Base::func();2通过类名访问cout 通过类名访问 endl; // 访问子类的静态成员函数func() Son::func(); // 访问父类的静态成员函数func()子类名::父类名::成员函数 Son::Base::func();多继承一个子类继承多个父类C十分不建议使用多继承语法, 如果强烈需要复用两个父类的代码, 可以使用多继承, 但是更推荐使用组合的语法( 组合语法文章后续会详细介绍)C 支持多继承Java 不支持语法class 派生类名 : 继承方式 基类1, 继承方式 基类2, ... { // 新增成员 };1. 多继承的核心问题二义性若多个基类有同名成员派生类访问时会触发二义性需通过基类名::明确指定class A { public: void func() { cout A::func() endl; } }; class B { public: void func() { cout B::func() endl; } }; class C : public A, public B { public: void test() { // func(); // 错误二义性A和B都有func A::func(); // 正确指定A的func B::func(); // 正确指定B的func } };2. 菱形继承钻石继承多继承的典型坑场景两个子类继承同一个基类第三个子类继承这两个子类形成菱形结构。问题数据冗余基类成员会被继承两次两个子类各存一份二义性访问基类成员时无法确定来源。解决方式虚继承virtual在继承时加virtual关键字让派生类共享基类的一份实例虚基类#include iostream using namespace std; class Top { public: int _e: }; class Mid1 : virtual public Animal {}; class Mid2 : virtual public Animal {}; class Bottom : public Sheep,public Tuo {}; void test01() { Bottom bo; bo.Mid1::_e 91; bo.Mid2::_e 19; coutbo.Mid1::_ebo.Mid2::_ebo._eendl; //以上结果均为19 }虚继承原理(会用就行,看不懂没事)虚继承会为派生类生成 “虚基类表vbtable”编译器会根据Bottom的整体内存布局重新计算并改写 Mid1 的 vbtable 偏移量从而保证基类实例唯一。如果没有虚继承,Mid1和Mid2分别继承一份Top类的成员,这对Mid1和Mid2没有任何问题的影响;问题是: Bottom如果继承了Mid1和Mid2, 编译器会把两份Top类的成员全部载入Bottom, 无虚继承时直接访问 Bottom 的 x 会编译报错编译器无法分辨是 Mid1::x 还是 Mid2::x, 而且两份Top类成员, 我们大概率只用一份, 多出来的就浪费了, 然后平时调用成员时必须用基类名::明确指定, 这很麻烦..为了解决这两个问题: 我们可以让Mid1和Mid2虚继承Top1, 这样编译器会在Mid1和Mid2的内部隐式生成一个虚基类表, 编译器会在(以Mid1为例)这个表里存储「整个 Top 虚基类实例的起始地址」到 Mid1 的 vbptr 的偏移量, 平时访问Top1成员时, 编译器自动处理, 和操作普通成员函数无差别(有微小性能差距);当Bottom类继承Mid1和Mid2时, 编译器在编译Bottom类时会识别出这是 “菱形虚继承” 场景因此会Top::x 的位置迁移把 Mid1 和 Mid2 各自的 Top::x 抽离出来合并为一份共享的 Top::x放在 Bottom 对象内存的最末尾地址0x128而非跟着 Mid1/Mid2 的子区域Mid1和Mid2 的 vbptr 地址改写Mid1 作为 Bottom 的子区域其 vbptr 的地址变成了0x100Bottom 对象内的子区域起始地址而非独立时的0x200。改写vbtable 里的偏移量值:重新计算每个虚继承类Mid1/Mid2的 vbptr 到共享 Top::x 的偏移量把独立时的0x10替换为 Bottom 适配后的0x28Mid1/0x18Mid2;这样做既避免了Bottom对Mid1和Mid2同时继承导致的二义性和数据冗余, 也满足了 Mid1和Mid2对Top成员的调用.,而且Mid1/Mid2 的原有代码无需修改就能适配 Bottom 的共享 Top 实例.下面是一些更加详细的解释:独立的Mid1对象内存布局虚继承Top独立实例化Mid1 m1_obj;时内存布局如下假设起始地址为0x200内存区域起始地址结束地址占用字节核心说明Mid1 的 vbptr虚基类表指针0x2000x2078 字节指向Mid1的虚基类表vbtableMid1 的成员m1含填充0x2080x20F8 字节int m1占 4 字节填充 4 字节至 8 字节Top 的成员x唯一实例0x2100x2178 字节虚基类Top的成员放在对象末尾以 Mid1 的 vbtable 的具体结构vbtable不存储 Top 每个成员的偏移只存储「整个 Top 虚基类实例的起始地址」到 Mid1 的 vbptr 的偏移量因为 Top 是一个完整的类实例只要找到 Top 实例的起始地址就能直接访问其所有成员无需为每个成员存偏移元素索引存储值十六进制含义核心作用00x00vbptr自身相对于当前类Mid1起始地址的偏移通常为 0因为 vbptr 在 Mid1 的起始位置10x28Mid1的vbptr到虚基类Top::x的偏移量之前计算的核心偏移Bottom类的内存分布0x100: Mid1的vbptr → 指向Mid1的vbtable 0x108: Mid1的成员 0x110: Mid2的vbptr → 指向Mid2的vbtable 0x118: Mid2的成员 0x120: Bottom的成员 0x128: Top的成员_e唯一实例0x18 和 0x28 不是 “随便定的”编译器在编译Bottom类时会识别出这是 “菱形虚继承” 场景因此Top::x 的位置迁移把 Mid1 和 Mid2 各自的 Top::x 抽离出来合并为一份共享的 Top::x放在 Bottom 对象内存的最末尾地址0x128而非跟着 Mid1/Mid2 的子区域Mid1和Mid2 的 vbptr 地址改写Mid1 作为 Bottom 的子区域其 vbptr 的地址变成了0x100Bottom 对象内的子区域起始地址而非独立时的0x200。改写vbtable 里的偏移量值:重新计算每个虚继承类Mid1/Mid2的 vbptr 到共享 Top::x 的偏移量把独立时的0x10替换为 Bottom 适配后的0x28Mid1/0x18Mid2;运行时访问 x 时只需要 “当前 vbptr 的地址 预存的偏移量”就能精准定位到唯一的 x—— 这也是虚继承能解决菱形继承二义性的核心无论走哪个路径最终通过 “地址 偏移量” 都能找到同一个 x。章节二. 多态C多态是面向对象编程的核心概念之一它允许我们通过基类的指针或引用来调用派生类中重写的方法从而实现接口的复用和运行时方法的动态绑定。多态的作用:提高代码的可复用性通过多态我们可以编写出能够处理基类对象的通用代码这些代码可以不加修改地应用于所有的派生类对象。提高代码的可扩展性当需要增加新的派生类时我们不需要修改原有的基于基类的代码只需让新的派生类重写基类的虚函数即可。这符合开闭原则对扩展开放对修改封闭。实现接口的统一定义通过基类中定义的虚函数我们可以为所有派生类提供一个统一的接口。不同的派生类可以根据自己的需要重写这些虚函数提供具体的实现。实现运行时类型识别和动态绑定多态允许程序在运行时根据对象的实际类型来调用相应的方法而不是根据指针或引用的类型。这使得程序更加灵活。设计模式的基础许多设计模式如工厂模式、策略模式、观察者模式等都依赖于多态性。多态的原理:在C中多态是通过虚函数virtual function和继承来实现的。具体来说我们需要在基类中将希望被多态调用的函数声明为虚函数然后在派生类中重写这些虚函数。当我们通过基类的指针或引用调用虚函数时会根据实际对象的类型来调用相应的函数。- 必须通过**基类指针或引用**调用虚函数才能触发多态。- 直接使用对象而非指针/引用会导致**对象切片slicing**失去多态性。- 基类虚函数必须要写virtual但是派生类可以不加virtual也能构成重写虽然这样很不规范- 基类析构函数应声明为虚函数确保正确释放派生类资源。class Animal { public: virtual void speak() { std::cout Animal sound std::endl; } }; class Dog : public Animal { public: void speak() override { std::cout Woof! std::endl; } }; class Cat : public Animal { public: void speak() override { std::cout Meow! std::endl; } }; void makeSound(Animal* animal) { animal-speak(); // 多态调用根据实际对象类型调用对应的speak方法 } int main() { Dog dog; Cat cat; makeSound(dog); // 输出Woof! makeSound(cat); // 输出Meow! return 0; }其实, 目前看来, 多态还不够灵活, 虽然子类可以在父类的基础上, 重写父类的虚函数, 自定义自己的功能, 还可以统一通过父类的指针调用这些功能, 看起来非常自由灵活, 但是实际上依然是带着镣铐起舞, 因为子类自己单独的函数, 不能使用多态, 无法让一个统一的父类指针调用, 虽然可以通过类型强行转换基类指针为子类指针,然后调用子类的非虚函数,但是这样很危险, 你必须保证你强转的基类指针确实指向你以为的子类对象, 当然你可以使用各种安全类型转换函数, 但是会产生额外的性能开销. 协变可以一定程度解决上面的问题.对象切片vs多态:对象切片Object Slicing定义与场景对象切片是指将派生类对象直接赋值值拷贝给基类对象时基类对象只能保存派生类中 “属于基类的部分”派生类特有的成员属性、方法会被 “切掉”最终基类对象仅包含基类子对象丢失派生类特有信息。场景是否切片本质原因基类指针 指向子类对象基类引用 引用子类对象不切片指针 / 引用是 “间接访问”指向完整的子类对象仅访问范围被限制为基类接口子类对象直接赋值 / 值传递给基类对象切片发生值拷贝基类对象只能容纳自身成员子类特有部分被丢弃1发生切片的示例class Base { int a; }; class Derived : public Base { int b; }; // 派生类特有成员b Derived d; Base b d; // 直接赋值b仅保留Base的aDerived的b被切掉切片2不发生切片的示例Derived d; Base* ptr d; // 指针指向完整的d仅以Base视角访问 Base ref d; // 引用绑定完整的d仅以Base视角访问 // ptr/ref可通过虚函数调用派生类重写版本证明d的派生类部分仍存在多态与对象切片的关系多态动态绑定的核心是 “根据对象实际类型调用对应虚函数”而对象切片会直接破坏多态需注意以下规则(1) 多态的触发条件必须通过基类指针 / 引用调用虚函数虚函数的 “动态多态”运行时确定调用版本仅在基类指针 / 引用调用虚函数时触发若用派生类指针 / 引用调用属于 “静态调用”编译期确定不是多态的核心机制。示例多态生效class Animal { public: virtual void speak() { cout Animal sound endl; } virtual ~Animal() {} // 虚析构后续补充 }; class Dog : public Base { public: void speak() override { cout 汪汪 endl; } }; class Cat : public Base { public: void speak() override { cout 喵喵 endl; } }; int main() { Animal* animal1 new Dog(); Animal* animal2 new Cat(); animal1-speak(); // 输出“汪汪”多态调用Dog的版本 animal2-speak(); // 输出“喵喵”多态调用Cat的版本 delete animal1; delete animal2; }(2) 对象切片会导致多态失效若直接将派生类对象赋值给基类对象发生切片调用虚函数时仅会执行基类版本多态性丢失。示例多态失效class Person { public: virtual void BuyTicket() { cout 买成人票 endl; } }; class Student : public Person { public: void BuyTicket() override { cout 买学生票 endl; } }; int main() { Student s; Person p s; // 发生切片p仅保留Person的成员 p.BuyTicket(); // 输出“买成人票”仅执行基类版本多态失效 }多态的关键补充基类需声明虚析构若基类析构函数不是虚函数当用基类指针指向派生类对象并 delete时仅会调用基类析构函数派生类的析构函数不会执行导致派生类中动态分配的资源堆内存、文件句柄等无法释放造成内存泄漏。示例内存泄漏 vs 正确释放class Person { public: // 非虚析构错误写法 ~Person() { cout Person析构 endl; } }; class Student : public Person { public: ~Student() { cout Student析构 endl; } }; int main() { Person* p new Student(); delete p; // 仅调用Person析构Student析构不执行→资源泄漏 }class Person { public: virtual ~Person() { cout Person析构 endl; } // 虚析构正确写法 }; class Student : public Person { public: ~Student() { cout Student析构 endl; } }; int main() { Person* p new Student(); delete p; // 先调用Student析构再调用Person析构→资源正确释放 }虚函数virtual父类的非虚成员函数是编译期静态绑定的无论this指针指向父类还是子类对象都会固定调用父类版本功能不可变为了让函数调用能根据this指针指向的对象的实际类型父类 / 子类动态调整执行逻辑需将父类成员函数声明为virtual虚函数虚函数不会修改自身代码而是通过虚函数表机制在运行时根据this指向对象的动态类型调用对应类的虚函数版本实现多态。基类中用virtual修饰的成员函数允许派生类重写覆盖并支持 “基类指针 / 引用指向派生类对象时调用派生类的重写函数”。1. 虚函数重写函数的规则:函数名,参数,返回值,都必须相同,但是参数默认值可以不同协变允许返回值不同, 但是返回值被限制, 只能是基类的虚函数返回值的派生类的引用或者指针(协变在后面会详细讲解)2. 虚函数重写例子:class Parent { public: // 虚函数静态类型是Parent*但调用版本由this指向的对象类型决定 virtual void func() { cout Parent::func() endl; } }; class Child : public Parent { public: // 重写父类虚函数子类vtable中替换为Child::func()的地址 void func() override { cout Child::func() endl; } }; int main() { Parent* p1 new Parent(); Parent* p2 new Child(); // this的静态类型是Parent*动态类型是Child* p1-func(); // this指向Parent对象 → 调用Parent::func() p2-func(); // this指向Child对象 → 调用Child::func() delete p1; delete p2; return 0; }纯虚函数与抽象类纯虚函数virtual 返回值 函数名(参数) 0;无函数体抽象类包含纯虚函数的类无法实例化仅作为基类规则派生类必须重写纯虚函数否则派生类也是抽象类。示例// 抽象类接口 class Shape { public: // 纯虚函数计算面积 virtual double area() 0; }; // 派生类矩形必须重写area class Rect : public Shape { private: int w, h; public: Rect(int x, int y) : w(x), h(y) {} double area() override { return w * h; } }; int main() { // Shape s; // 错误抽象类不能实例化 Shape* ptr new Rect(3, 4); cout ptr-area() endl; // 12多态 delete ptr; return 0; }析构函数建议设为虚函数若基类指针指向派生类对象删除指针时基类析构非虚仅调用基类析构派生类析构不执行 → 内存泄漏基类析构为虚先调用派生类析构再调用基类析构 → 正确释放。虚析构示例class Base { public: virtual ~Base() { cout Base析构 endl; } // 虚析构 }; class Derived : public Base { public: ~Derived() { cout Derived析构 endl; } }; int main() { Base* ptr new Derived(); delete ptr; // 输出Derived析构 → Base析构正确 return 0; }虚函数原理(会用就行,看不懂也没事):虚函数表是 C 实现 “运行时多态动态绑定” 的核心机制其使用分为编译期准备、对象初始化、运行时调用三个阶段。1. 编译期生成 vtable 和插入 vptrvtable虚函数表的生成每个包含虚函数的类或继承了虚函数的类编译器会为其生成一份唯一的虚函数表存储在程序的全局只读数据段表中按声明顺序存储该类所有虚函数的函数地址。子类会继承父类的虚函数表, 但是在此之后子类的虚函数表和父类的虚函数表是独立的若子类重写了父类的虚函数子类 vtable 中对应位置的函数地址会被替换为子类的重写版本, 不会影响父类的虚函数表;若子类未重写则保留父类虚函数的地址。vptr虚函数表指针的插入编译器会在每个该类的对象中隐式插入一个 vptr通常是对象的第一个成员vptr 的值是所属类 vtable 的起始地址用于运行时找到对应的 vtable。2. 对象创建时初始化 vptr当通过new或栈实例化对象时构造函数会自动初始化 vptr让它指向当前对象所属类的 vtable父类对象的 vptr → 父类的 vtable子类对象的 vptr → 子类的 vtable若子类重写了父类虚函数vtable 中对应地址已替换为子类版本。3. 调用虚函数时动态寻址核心步骤当通过父类指针 / 引用调用虚函数时程序不会直接绑定函数而是通过以下步骤动态找到实际执行的函数获取对象的 vptr从指针 / 引用指向的对象中取出 vptr对象的第一个成员通过 vptr 找到 vtablevptr 的值是 vtable 的起始地址直接定位到整个 vtable 数组在 vtable 中找到目标函数地址vtable 中虚函数按声明顺序排列编译器已知每个虚函数的索引位置如第一个虚函数索引 0通过索引取出对应函数地址调用函数用取出的函数地址执行对应逻辑子类重写则调用子类版本未重写则调用父类版本。一个例题用来检测你对虚函数的理解:下面代码的运行结果是( )A:A-0B:B-1C:A-1D:B-0E:编译出错F:以上都不正确class A { public: virtual void func(int val 1) { std::cout A- val std::endl; } virtual void test() { func(); } }; class B : public A { public: void func(int val 0) { std::cout B- val std::endl; } }; int main(int argc, char* argv[]) { B* p new B; p-test(); return 0; }考点分析结合 vtable 机制test 函数的调用B 未重写test()因此p-test()调用的是 A 的test()函数体A 的 vtable 中test的地址未被替换。func 函数的动态绑定在 A 的test()中调用func()时func是虚函数会触发动态寻址从 B 对象中取出 vptr → 找到 B 的 vtableB 的 vtable 中func的地址已被替换为 B 的重写版本 → 调用 B 的func函数体。虚函数默认参数的绑定规则C 规定虚函数的默认参数是 “静态绑定”编译期确定即默认参数由 “调用虚函数时的静态类型” 决定而非实际对象的动态类型。在 A 的test()中调用func()时func的静态类型是A::func→ 默认参数取 A 中func的val1而非 B 中func的val0。最终输出B 的func函数体被调用参数为 A 中func的默认值 1 → 输出B-1对应选项B。虚函数表vs虚继承表:虚函数表vtable和虚继承表vbtable是 C 编译器为实现完全不同的面向对象特性设计的静态表结构二者的核心目的、触发条件、存储内容、工作逻辑均无交集仅因都带 “虚” 字且由编译器隐式生成易被混淆。vtable为 “多态” 服务是 “虚函数地址的集合”让基类指针能调用派生类的重写函数vbtable为 “虚继承” 服务是 “偏移量的集合”让菱形继承中虚基类的实例唯一消除冗余和二义性。一个有意思的冷知识: 大多数教材在介绍虚函数表指针和虚继承表指针时都说它们会被隐式存放在类的起始位置,那到底谁在最开始呢?其实C 标准未强制规定vptr虚函数表指针和 vbptr虚基类表指针的位置但主流编译器GCC/Clang、MSVC均遵循统一优先级:虚函数表指针vptr优先放在对象内存的最起始位置虚基类表指针vbptr紧随其后;这种设计的核心目的是优化多态调用效率vptr 是运行时多态的核心高频访问放开头可直接通过对象首地址获取无需额外地址计算vbptr 仅服务于虚继承的成员访问频率更低因此后置。以下是更详细的全方位的对比解析核心定位最根本区别特性虚函数表vtable虚继承表vbtable核心目的实现运行时多态动态绑定让基类指针 / 引用能调用派生类重写的虚函数解决菱形继承的两大问题数据冗余Top 成员重复存储 访问二义性无法确定调用哪份 Top 成员设计初衷适配 “重写override” 语义支持多态扩展适配 “虚继承” 语义保证虚基类实例唯一关键维度详细对比对比维度虚函数表vtable虚继承表vbtable触发条件类中包含至少一个虚函数自身声明 / 从基类继承类采用虚继承virtual public 基类且处于菱形继承场景存储内容存储类中所有虚函数的入口地址1. 基类 vtable 存自身虚函数地址2. 派生类重写虚函数时替换 vtable 中对应位置的函数地址存储偏移量地址差1. 首元素vbptr 自身的校准偏移通常为 02. 后续元素vbptr 到虚基类Top实例起始地址的偏移量对应的对象指针每个含虚函数的对象会隐式生成1 个 vptr虚函数表指针指向类的 vtable每个虚继承的类在对象中生成N 个 vbptr虚基类表指针N 虚继承的基类数指向各自的 vbtable数量规则每个 “含虚函数的类” 对应1 份 vtable静态存储区所有对象共享每个 “虚继承的类” 对应1 份 vbtable静态存储区编译器会根据最终派生类Bottom的布局调整表内偏移量访问逻辑运行时动态绑定基类指针→vptr→vtable→找到派生类重写的虚函数地址→调用编译期计算 运行时定位vbptr→vbtable→读取偏移量→计算虚基类实例地址→访问成员内存位置存储在程序只读数据段.rodata属于类级别的静态资源同 vtable存储在只读数据段属于类级别的静态资源性能影响调用虚函数时多一次 “vptr 找 vtable” 的间接跳转性能损耗极小访问虚基类成员时多一次 “vbptr 找 vbtable 计算偏移量”性能损耗比 vtable 略小纯地址计算与继承的关系无继承也可存在单个类声明虚函数就会生成 vtable仅存在于继承场景且必须是虚继承 菱形结构才会体现价值协变与工厂模式:协变Covariance是 C 虚函数重写的语法规则:工厂模式尤其是工厂方法模式是面向对象的设计模式二者的核心关联是协变为工厂模式的多态接口提供了更灵活、更安全的产品返回能力—— 让派生工厂类能返回 “更具体的派生产品类型”既保持工厂接口的多态统一性又避免手动类型转换的风险完美适配工厂模式的设计目标。协变不是工厂模式的 “必需条件”但却是工厂方法模式的 “最佳实践”没有协变工厂方法模式也能实现但需手动类型转换有了协变工厂方法模式既能保持 “基类接口统一” 的多态特性又能兼顾 “派生类型精准使用” 的灵活性同时规避类型转换风险是语法规则对设计模式的完美赋能。协变的核心规则:协变是 C 对 “虚函数重写” 的放宽规则满足以下条件即视为合法重写基类虚函数返回值基类产品的指针 / 引用如Product*派生类重写函数返回值该基类产品的派生类指针 / 引用如ConcreteProduct*其余签名参数列表、const/volatile、函数名必须完全一致仅支持指针 / 引用值类型会触发对象切片不适用协变。class Base {}; class Derived : public Base {}; class A { public: virtual Base* create() { // 基类虚函数返回Base* return new Base(); } }; class B : public A { public: virtual Derived* create() override { // 派生类虚函数返回Derived*Base的派生类 return new Derived(); } };工厂模式的核心目标以工厂方法为例:定义 “基类工厂” 的虚接口如createProduct()负责声明产品创建逻辑每个 “派生工厂” 重写该接口创建对应的 “派生产品”使用者通过基类工厂指针 / 引用调用createProduct()无需关心具体产品类型多态。无协变时工厂模式的痛点如果没有协变派生工厂的createProduct()只能返回 “基类产品指针”会导致两个核心问题1. 代码示例无协变的工厂方法#include iostream using namespace std; // 产品层级 class Fruit { // 基类产品 public: virtual void show() 0; virtual ~Fruit() default; }; class Apple : public Fruit { // 派生产品苹果 public: void show() override { cout 我是苹果 endl; } void appleOnlyFunc() { cout 苹果专属功能脆甜 endl; } // 派生产品特有方法 }; // 工厂层级 class FruitFactory { // 基类工厂 public: virtual Fruit* createFruit() 0; // 只能返回基类产品指针 virtual ~FruitFactory() default; }; class AppleFactory : public FruitFactory { // 派生工厂苹果工厂 public: Fruit* createFruit() override { // 无协变只能返回Fruit* return new Apple(); } }; // 使用者代码 int main() { FruitFactory* factory new AppleFactory(); Fruit* fruit factory-createFruit(); fruit-show(); // 多态调用输出“我是苹果” // 问题1要调用苹果专属方法必须手动强转类型安全风险 Apple* apple dynamic_castApple*(fruit); if (apple) { apple-appleOnlyFunc(); // 输出“苹果专属功能脆甜” } delete fruit; delete factory; return 0; }2. 核心痛点类型安全风险手动dynamic_cast可能失败如工厂返回错误产品导致空指针 / 未定义行为代码冗余使用者必须知道 “基类产品实际是哪个派生类”才能正确转换违背工厂模式 “隐藏具体产品” 的设计初衷接口不直观派生工厂明明只生产苹果却只能返回 “水果” 指针语义上不匹配。协变如何解决工厂模式的痛点协变允许派生工厂的createFruit()直接返回 “派生产品指针如Apple*”既保持虚函数重写的多态性又避免手动转换让工厂接口更贴合语义。1. 代码示例有协变的工厂方法#include iostream using namespace std; // 产品层级和无协变版本一致 class Fruit { public: virtual void show() 0; virtual ~Fruit() default; }; class Apple : public Fruit { public: void show() override { cout 我是苹果 endl; } void appleOnlyFunc() { cout 苹果专属功能脆甜 endl; } }; // 工厂层级核心协变返回值 class FruitFactory { public: virtual Fruit* createFruit() 0; // 基类返回Fruit* virtual ~FruitFactory() default; }; class AppleFactory : public FruitFactory { public: Apple* createFruit() override { // 协变返回Apple*Fruit的派生类指针 return new Apple(); } }; // 使用者代码 int main() { // 场景1多态使用基类指针—— 保持工厂模式的多态性 FruitFactory* factory1 new AppleFactory(); Fruit* fruit factory1-createFruit(); fruit-show(); // 输出“我是苹果” // 场景2直接使用派生类型无需转换—— 兼顾灵活性和类型安全 AppleFactory* factory2 new AppleFactory(); Apple* apple factory2-createFruit(); // 直接返回Apple*无需强转 apple-appleOnlyFunc(); // 安全调用专属方法 delete fruit; delete apple; delete factory1; delete factory2; return 0; }2. 协变带来的核心价值针对工厂模式维度无协变的工厂模式有协变的工厂模式类型安全依赖手动强转有风险直接返回派生类型无转换风险代码简洁性冗余的类型转换逻辑无需转换代码更简洁接口语义派生工厂返回基类指针语义不匹配派生工厂返回对应派生产品指针语义精准多态兼容性支持多态但灵活性差既支持多态基类指针调用又支持精准类型使用协变在工厂模式中的适用场景与限制1. 适用场景核心是 “工厂方法模式”工厂方法模式依赖虚函数重写实现 “一个工厂造一个产品”协变是该模式的 “最优语法搭配”需调用派生产品特有方法如果使用者不仅需要基类产品的通用接口还需要派生产品的专属功能协变是最安全的实现方式符合开闭原则新增派生产品 / 工厂时只需重写带协变返回值的createProduct()无需修改基类接口。2. 限制协变的语法边界仅支持指针 / 引用协变返回值不能是值类型如Fruit否则会触发对象切片且不符合 C 重写规则派生关系必须是公有派生产品必须是基类产品的公有派生私有 / 保护派生会导致协变失效不适用简单工厂简单工厂依赖 “静态方法 参数判断” 创建产品无虚函数重写因此无法利用协变基类接口需统一协变仅改变返回值类型虚函数的参数列表、const 属性等必须和基类完全一致。章节三: 组合组合是 C 面向对象编程中基于 “has-a有一个” 关系的类复用方式核心是将一个类成员类 / 组件类的对象作为另一个类整体类 / 容器类的成员变量通过封装成员对象的功能实现代码复用而非像继承那样基于 “is-a是一个” 关系复用接口。组合是 C 中比继承更灵活、耦合度更低的复用手段也是 “优先使用组合而非继承” 这一经典设计原则的核心载体。组合的核心本质组合的核心是 “整体 - 部分” 关系比如汽车Car有一个发动机Engine、电脑Computer有一个 CPU这类场景中“部分”Engine/CPU无法脱离 “整体”Car/Computer独立存在或无独立存在的意义适合用组合实现。关键特征成员对象是整体类的 “一部分”生命周期通常与整体类绑定整体类通过调用成员对象的公开接口实现功能复用而非直接继承其属性 / 方法整体类可完全封装成员对象对外仅暴露需要的接口符合 “封装” 原则。组合的两种形式强组合 vs 弱组合组合分为 “强组合” 和 “弱组合”核心区别是成员对象的生命周期是否由整体类管理类型实现方式生命周期特点适用场景强组合成员对象为值类型如Engine engine;与整体类完全绑定整体创建则成员创建整体销毁则成员销毁部分无法脱离整体存在如 Car-Engine弱组合成员对象为指针 / 引用如Engine* engine;成员对象生命周期由外部管理整体类仅持有指针需手动释放需动态替换成员如 Car 换不同发动机组合的基础语法示例强组合以 “汽车 发动机” 为例实现最典型的 “强组合”成员对象为值类型生命周期完全绑定#include iostream #include string using namespace std; // 成员类组件类发动机 class Engine { public: // 发动机的核心功能 void start() { cout 发动机启动嗡嗡嗡 endl; } void stop() { cout 发动机关闭嘀 endl; } // 构造/析构观察生命周期 Engine() { cout [生命周期] Engine 构造 endl; } ~Engine() { cout [生命周期] Engine 析构 endl; } }; // 整体类容器类汽车 —— 组合Engine class Car { private: // 核心将Engine作为私有成员封装外部无法直接访问 Engine engine; // 值类型成员强组合生命周期绑定 string brand; // 汽车自身属性 public: // 构造函数初始化自身成员同时触发Engine的构造 // 注意成员对象的构造顺序由“声明顺序”决定而非初始化列表顺序 Car(string b) : brand(b) { cout [生命周期] Car 构造 brand endl; } // 汽车的功能封装并复用Engine的功能 void startCar() { cout brand 准备启动 endl; engine.start(); // 调用成员对象的方法 cout brand 启动完成 endl; } void stopCar() { cout brand 准备停止 endl; engine.stop(); // 调用成员对象的方法 cout brand 停止完成 endl; } // 析构触发Engine的析构析构顺序与构造相反 ~Car() { cout [生命周期] Car 析构 brand endl; } }; // 测试代码 int main() { Car myCar(特斯拉); // 创建Car对象自动构造Engine myCar.startCar(); // 复用Engine的start() myCar.stopCar(); // 复用Engine的stop() return 0; }输出结果重点看生命周期[生命周期] Engine 构造 [生命周期] Car 构造特斯拉 特斯拉 准备启动 发动机启动嗡嗡嗡 特斯拉 启动完成 特斯拉 准备停止 发动机关闭嘀 特斯拉 停止完成 [生命周期] Car 析构特斯拉 [生命周期] Engine 析构关键解读构造顺序先构造成员对象Engine再构造整体类Car析构顺序先析构整体类Car再析构成员对象Engine封装性Engine 是 Car 的私有成员外部无法直接调用myCar.engine.start()只能通过 Car 提供的startCar()接口访问避免成员对象被滥用复用性Car 无需继承 Engine仅通过组合就能复用其功能且不依赖 Engine 的内部实现。弱组合示例指针形式class Car { private: Engine* engine; // 指针形式的成员对象弱组合 string brand; public: // 构造手动创建Engine对象 Car(string b) : brand(b) { engine new Engine(); // 外部管理成员对象的创建 cout [弱组合] Car 构造 brand endl; } // 析构必须手动释放engine否则内存泄漏 ~Car() { delete engine; // 手动销毁成员对象 cout [弱组合] Car 析构 brand endl; } // 支持动态替换发动机强组合无法实现 void replaceEngine(Engine* newEngine) { delete engine; // 释放旧发动机 engine newEngine; // 替换为新发动机 cout brand 更换发动机完成 endl; } };弱组合注意事项必须手动管理成员对象的内存new/delete否则会导致内存泄漏若涉及拷贝构造 / 赋值需自定义实现深拷贝避免多个 Car 对象共享同一个 Engine 指针导致重复释放。组合的典型使用场景整体 - 部分关系Car-Engine、Computer-CPU、Phone-Battery、Order-OrderItem 等功能复用且避免继承耦合比如要复用File类的 “读写” 功能但不想让Document类成为File的子类Document 不是 “一种” File动态扩展功能比如策略模式Strategy Pattern中通过组合不同的策略类如PaymentStrategy让订单类动态切换支付方式组合模式Composite Pattern用于实现 “树形结构”如文件夹 - 文件让整体和单个对象具有统一接口替代多重继承C 不支持多继承或多继承易出问题可通过组合多个类实现 “多功能复用”比如Car组合EngineWheelBattery。组合的注意事项成员对象的初始化顺序C 中成员对象的构造顺序由 “类内声明顺序” 决定而非构造函数初始化列表的顺序示例class C { A a; // 声明顺序1 B b; // 声明顺序2 public: C() : b(), a() {} // 初始化列表顺序是b→a但构造顺序仍是a→b };浅拷贝问题弱组合指针成员的默认拷贝构造 / 赋值运算符是 “浅拷贝”会导致多个对象共享同一个指针销毁时重复释放。需自定义拷贝构造和赋值运算符实现深拷贝单一职责原则避免一个类组合过多成员对象比如 Car 组合 EngineGPSAudioSeat...导致类的职责过重需拆分避免过度封装成员对象的核心接口应通过整体类暴露而非完全隐藏比如 Car 需暴露startCar()而非让用户无法操作发动机。组合 vs 继承核心对比维度组合has-a继承is-a关系本质整体 - 部分有一个父类 - 子类是一个耦合度低仅依赖成员类的公开接口高子类依赖父类的实现父类修改可能导致子类崩溃灵活性高可动态替换成员对象低继承关系在编译期固定无法动态改变接口暴露可控制仅暴露需要的接口子类继承父类所有公开 / 保护接口可能暴露多余接口代码复用封装复用成员类功能直接复用父类属性 / 方法典型问题浅拷贝弱组合菱形继承、对象切片、父类析构非虚导致内存泄漏核心能力多态、强制接口、框架扩展低耦合、动态替换、单一职责经典设计原则优先使用组合而非继承C 设计模式的核心原则之一是 “Favor composition over inheritance”原因继承的 “强耦合” 会导致代码脆弱比如父类新增一个虚函数可能破坏子类的重写逻辑组合的 “低耦合” 让代码更易扩展比如 Car 可替换燃油发动机 / 电动发动机无需修改 Car 的核心逻辑组合避免了继承的诸多 “坑”如菱形继承的二义性、对象切片等。