参考C 类 对象 | 菜鸟教程 (runoob.com)封装、继承和多态是面向对象语言的三大特性。在熟悉这几个特性之前我们先了解下数据抽象的概念。数据抽象数据抽象是指只向外界提供关键信息并隐藏其后台的实现细节即只表现必要的信息而不呈现细节。数据抽象是一种依赖于接口和实现分离的编程设计技术。让我们举一个现实生活中的真实例子比如一台电视机您可以打开和关闭、切换频道、调整音量、添加外部组件如喇叭、录像机、DVD 播放器但是您不知道它的内部实现细节也就是说您并不知道它是如何通过缆线接收信号如何转换信号并最终显示在屏幕上。因此我们可以说电视把它的内部实现和外部接口分离开了您无需知道它的内部实现原理直接通过它的外部接口比如电源按钮、遥控器、声量控制器就可以操控电视。现在让我们言归正传就 C 编程而言C 类为数据抽象提供了可能。它们向外界提供了大量用于操作对象数据的公共方法也就是说外界实际上并不清楚类的内部实现。例如您的程序可以调用sort()函数而不需要知道函数中排序数据所用到的算法。实际上函数排序的底层实现会因库的版本不同而有所差异只要接口不变函数调用就可以照常工作。在 C 中我们使用类来定义我们自己的抽象数据类型ADT。您可以使用类iostream的cout对象来输出数据到标准输出如下所示实例#include iostream using namespace std; int main( ) { cout Hello C endl; return 0; }在这里您不需要理解cout是如何在用户的屏幕上显示文本。您只需要知道公共接口即可cout 的底层实现可以自由改变。注意这里的#include iostream是类的包含不是头文件的包含。数据抽象有两个重要的优势类的内部受到保护不会因无意的用户级错误导致对象状态受损。类实现可能随着时间的推移而发生变化以便应对不断变化的需求或者应对那些要求不改变用户级代码的错误报告。抽象把代码分离为接口和实现。所以在设计组件时必须保持接口独立于实现这样如果改变底层实现接口也将保持不变。在这种情况下不管任何程序使用接口接口都不会受到影响只需要将最新的实现重新编译即可。封装数据封装Data Encapsulation是面向对象编程OOP的一个基本概念它通过将数据和操作数据的函数封装在一个类中来实现。这种封装确保了数据的私有性和完整性防止了外部代码对其直接访问和修改。所有的 C 程序都有以下两个基本要素程序语句代码这是程序中执行动作的部分它们被称为函数。程序数据数据是程序的信息会受到程序函数的影响。封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念这样能避免受到外界的干扰和误用从而确保了安全。数据封装引申出了另一个重要的 OOP 概念即数据隐藏。数据封装是一种把数据和操作数据的函数捆绑在一起的机制数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。C 通过创建类来支持封装和数据隐藏public、protected、private。我们已经知道类包含私有成员private、保护成员protected和公有成员public成员。默认情况下在类中定义的所有项目都是私有的。例如class Box { public: double getVolume(void) { return length * breadth * height; } private: double length; // 长度 double breadth; // 宽度 double height; // 高度 };变量 length、breadth 和 height 都是私有的private。这意味着它们只能被 Box 类中的其他成员访问而不能被程序中其他部分访问。这是实现封装的一种方式。为了使类中的成员变成公有的即程序中的其他部分也能访问必须在这些成员前使用public关键字进行声明。所有定义在 public 标识符后边的变量或函数可以被程序中所有其他的函数访问。把一个类定义为另一个类的友元类会暴露实现细节从而降低了封装性。理想的做法是尽可能地对外隐藏每个类的实现细节。访问修饰符private: 私有成员只能在类的内部访问不能被类的外部代码直接访问。public: 公有成员可以被类的外部代码直接访问。protected: 受保护成员可以被类和其派生类访问。数据封装的优点数据隐藏: 通过将数据成员声明为私有防止外部代码直接访问这些数据。提高代码可维护性: 提供公共方法来访问和修改数据这使得可以在不影响外部代码的情况下修改类的内部实现。增强安全性: 防止不合法的数据输入和不当的修改操作。实现抽象: 提供了一种机制使得用户不需要了解类的内部实现细节只需要了解如何使用类的公共接口即可。继承面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类这使得创建和维护一个应用程序变得更容易。这样做也达到了重用代码功能和提高执行效率的效果。当创建一个类时您不需要重新编写新的数据成员和成员函数只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类新建的类称为派生类。继承代表了is a关系。例如哺乳动物是动物狗是哺乳动物因此狗是动物等等。代码如下// 基类 class Animal { // eat() 函数 // sleep() 函数 }; //派生类 class Dog : public Animal { // bark() 函数 };更多继续。基类 派生类一个类可以派生自多个类这意味着它可以从多个基类继承数据和函数。定义一个派生类我们使用一个类派生列表来指定基类。类派生列表以一个或多个基类命名形式如下class derived-class: access-specifier base-class其中访问修饰符 access-specifier 是public、protected或private其中的一个base-class 是之前定义过的某个类的名称。如果未使用访问修饰符 access-specifier则默认为 private。注意类继承的时候一般都会使用public继承。另外注意区分继承符号(:)和范围解析运算符(::)前者是一个冒号后者有两个冒号。范围解析运算符一般是放在前面以限定后面的范围继承这里的语法是基类在后面子类在前面。和类函数的外部定义容易搞混。假设有一个基类ShapeRectangle是它的派生类如下所示#include iostream using namespace std; // 基类 class Shape { public: void setWidth(int w) { width w; } void setHeight(int h) { height h; } protected: int width; int height; }; // 派生类 class Rectangle: public Shape { public: int getArea() { return (width * height); } }; int main(void) { Rectangle Rect; Rect.setWidth(5); Rect.setHeight(7); // 输出对象的面积 cout Total area: Rect.getArea() endl; return 0; }类继承语法反过来看比较容易理解。访问控制和继承当一个类派生自基类该基类可以被继承为public、protected或private几种类型。继承类型是通过上面讲解的访问修饰符 access-specifier 来指定的。继承中有public, protected, private三种继承方式它们相应地改变了基类成员的访问属性。1.public 继承基类 public 成员protected 成员private 成员的访问属性在派生类中分别变成public, protected, private2.protected 继承基类 public 成员protected 成员private 成员的访问属性在派生类中分别变成protected, protected, private3.private 继承基类 public 成员protected 成员private 成员的访问属性在派生类中分别变成private, private, private我们几乎不使用protected或private继承通常使用public继承。这里就相当于限制了派生类继承基类成员后的最高访问权限。派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问则应在基类中声明为 private。我们可以根据访问权限总结出不同的访问类型如下所示一个派生类继承了所有的基类方法但下列情况除外基类的构造函数、析构函数和拷贝构造函数。基类的重载运算符。基类的友元函数。另外注意但无论哪种继承方式下面两点都没有改变1.private 成员只能被本类成员类内和友元访问不能被派生类访问2.protected 成员可以被派生类访问。多继承多继承即一个子类可以有多个父类它继承了多个父类的特性。C 类可以从多个类继承成员语法如下class 派生类名:继承方式1基类名1,继承方式2基类名2,… { 派生类类体 };其中访问修饰符继承方式是public、protected或private其中的一个用来修饰每个基类各个基类之间用逗号分隔如上所示。现在让我们一起看看下面的实例#include iostream using namespace std; // 基类 Shape class Shape { public: void setWidth(int w) { width w; } void setHeight(int h) { height h; } protected: int width; int height; }; // 基类 PaintCost class PaintCost { public: int getCost(int area) { return area * 70; } }; // 派生类 class Rectangle: public Shape, public PaintCost { public: int getArea() { return (width * height); } }; int main(void) { Rectangle Rect; int area; Rect.setWidth(5); Rect.setHeight(7); area Rect.getArea(); // 输出对象的面积 cout Total area: Rect.getArea() endl; // 输出总花费 cout Total paint cost: $ Rect.getCost(area) endl; return 0; }更多待补充。继承场景下的构造顺序我们从认识几个概念开始梳理继承场景下的构造顺序先把两个词讲透父类子对象、成员子对象再单独说普通内置类型int、double 这种。1. 父类子对象base subobjectclass Base { public: int a; }; class Son : public Base { public: int b; };当你定义Son s;内存布局s 这个整体对象Son对象 ┌──────────────┬──────────────┐ │ Base部分 a │ Son自己的 b │ └──────────────┴──────────────┘父类子对象就是嵌在子类对象内部、属于 Base 那一块内存它本身就是一个独立的 Base 对象藏在 Son 里面。它不是一个单独变量不是 new 出来的是内嵌在 Son 里面的 Base 子对象。所以叫Base 子对象subobject。 所以构造 Son 的时候必须先把这块内嵌的 Base 子对象初始化好调用 Base 的构造。2. 成员子对象member subobject就是类里面定义的成员变量如果这个成员是一个类类型class MemberObj { public: MemberObj(){} }; class Son : public Base { MemberObj m; // m 是【成员子对象】 int b; };内存Son s ┌──Base子对象──┬──成员子对象 m ──┬──b──┐m是一个 MemberObj 类型的对象它内嵌在 Son 里面所以叫成员子对象。凡是类类型的成员都是成员子对象它自己也要走构造函数。3. 重点如果成员是普通内置类型int, char, double, 指针内置类型它是成员但它不是「子对象」这个是最容易混淆的地方区别类类型自定义 class/struct是子对象 →有构造函数必须调用构造初始化内置类型 int /double/char / 原生指针没有构造函数那它怎么处理分两种情况初始化列表给了初值Son():Base(), b(100){}→ 初始化阶段给这块内存写入 100。初始化列表没管它如果是局部对象这块内存就是随机垃圾值不会自动清零如果是全局 / 静态默认零初始化✅ 一句话内置类型没有构造函数不存在 “调用构造” 这个动作只是一块内存可以赋值 / 初始化。对比看这个例子#include iostream class Base { public: Base() { std::cout Base子对象构造\n; } }; class M { public: M() { std::cout M成员子对象构造\n; } }; class Son : public Base { M m; // 类类型成员子对象要调用构造 int num; // 内置类型普通成员不是子对象没有构造函数 public: Son() { std::cout Son构造函数体\n; std::cout num num \n; // 没初始化随机值 } }; int main() { Son s; }输出顺序Base子对象构造 M成员子对象构造 Son构造函数体 num 随机数顺序规则不变基类子对象 → 成员子对象类类型 → 进入构造函数体内置成员只是跟着一起处理但是不会有构造函数调用。4. 汇总一张表帮你分清名称类型是否子对象有没有构造函数Base 内嵌部分父类 class✅ 基类子对象✅ 有构造成员 m自定义 class类类型成员✅ 成员子对象✅ 有构造成员 numint/double内置基础类型❌ 不是子对象❌ 没有构造标准术语 subobject子对象只包含基类子对象 类类型的成员子对象。int 这种内置成员不属于 subobject。5. 再串一遍初始化顺序带上内置类型版本class Son : public Base { M m; int x; public: Son() : Base(), m(), x(10) {} };执行顺序构造 Base 基类子对象调用 Base ()构造 M 成员子对象调用 M ()初始化内置变量 x写入 10没有构造函数调用进入 Son 的构造函数体{}哪怕你初始化列表写成Son():x(10), m(), Base()执行顺序仍然是上面这个顺序初始化列表书写顺序不决定初始化顺序只看基类优先、成员按类内声明顺序。6. 极简总结父类子对象子类里面藏着一整块父类的对象是一个独立的 Base必须调用 Base 构造成员子对象类里面写的别的 class 类型成员它本身也是对象要调用它自己的构造int、double 这种普通内置成员是成员但不是子对象没有构造函数只是一块内存不调用构造有初始化值就填值没有就是脏数据。再来看看构造顺序一句话总顺序父类子对象 → 成员子对象按类中声明顺序 → 子类构造函数体析构顺序严格反过来子类构造体里面的逻辑收尾 → 成员子对象逆序销毁 → 父类子对象销毁1. 分步拆解 示例#include iostream class Base { public: Base() { std::cout 1. 父类Base构造\n; } }; class Member { public: Member() { std::cout 2. 成员Member构造\n; } }; class Son : public Base { private: Member m; // 成员声明顺序在这里 public: Son() : Base(), m() { std::cout 3. 子类Son构造函数体\n; } }; int main() { Son s; return 0; }输出1. 父类Base构造 2. 成员Member构造 3. 子类Son构造函数体2. 重点大坑初始化列表写的顺序不决定初始化顺序初始化顺序看【类里面的声明顺序】不是初始化列表从上到下class Son : public Base { private: Member m; int x; public: // 列表里先写x再写m没用 Son() : x(100), m() { } }; // 真实顺序Base → m → x → Son构造体这个是高频错题。3. 带上多层继承爷爷 → 父 → 子多层继承沿着继承链从最顶层的基类开始往下构造GrandBase → Base → Son → Son的成员 → Son构造体运行#include iostream class GrandBase { public: GrandBase(){std::cout 爷爷构造\n;} }; class Base : public GrandBase { public: Base(){std::cout 父类构造\n;} }; class Son : public Base { public: Son(){std::cout 子类构造体\n;} }; // Son s; // 爷爷构造 → 父类构造 → 子类构造体4. 多继承场景多个父类按 class 继承声明的先后顺序构造父类和初始化列表顺序无关class A { public: A(){std::cout A\n;} }; class B { public: B(){std::cout B\n;} }; class C : public B, public A { // 继承顺序先B后A public: C() : A(), B() {} // 列表反过来写不生效 }; // C c; // 输出 B → A → C构造体5. 再结合我们前面聊的知识点串起来子类构造初始化列表两种情况没写父类构造编译器自动补Base()调用父类无参构造父类没有无参构造就报错写了 Base (xxx)调用你指定的父类构造不需要父类有无参构造但是不管你写不写、调用哪个构造函数父类一定优先初始化。6. 析构顺序反过来构造GrandBase → Base → 成员 → Son析构Son 体结束 → 成员销毁 → Base 销毁 → GrandBase 销毁先造的后拆后造的先拆。7. 总结构造顺序先基类多继承按继承声明顺序、多层继承从最上层基类开始再成员按类内声明顺序最后执行子类构造函数体初始化列表书写顺序不控制初始化顺序析构顺序和构造完全相反本质子类对象包含基类子对象 成员子对象子对象必须先构造完成才能进入当前类构造函数体。注意上面讲的顺序是继承场景中创建子类对象时调用构造函数进行初始化的顺序不是创建对象时分配内存的顺序。拆开分成两大阶段非常清晰阶段 1分配内存只做一件事拿一块连续内存当执行Son s;编译器先算出Son对象总大小Base 部分 M 成员 int 成员 的大小之和向内存栈 / 堆一次性分配一整块连续内存✅ 重点内存是一次性全部分配好不是先分配父类内存、再分配子类内存分配完之后这块内存里面全是原始垃圾值还没有任何对象被构造构造函数还一次都没跑。内存布局在分配完就已经固定好了[ Base区域 ][ M区域 ][ int区域 ]布局顺序是固定的但此时只是三块裸内存不是有效的对象。阶段 2初始化阶段这就是我们前面一直在说的顺序调用构造的顺序内存已经全部拿到了接下来才依次在这块已经分配好的内存上去调用构造函数把裸内存变成合法对象在这块内存的 Base 区域 → 调用 Base 的构造函数构造基类子对象在 M 区域 → 调用 M 的构造函数构造成员子对象给 int 区域写入初始值内置类型无构造进入 Son 的构造函数体{} 所以分配一次性整块分配最先发生初始化构造调用顺序分配完成之后才开始遵循基类子对象 → 成员子对象 → 构造体。举个对比帮你彻底分清Son s;执行时序分配一整块 Son 大小的内存 ✅全部内存一起拿到Base () 构造函数执行初始化 Base 那块内存M () 构造函数执行初始化 M 那块内存初始化内置 int运行 Son 的构造函数体代码从内存/访问权限角度理解继承1. 内存层面子类对象里面包含父类子对象父类定义的所有非静态成员变量在创建子类对象时都会跟着分配内存全部存在子类对象内部的父类子对象里class Base{ private: int a; public: void func(){} }; class Son:public Base{}; Son s;s这块内存里实实在在存在Base的a也继承了成员函数Base::func。内存上全都带上了。但是a是private子类代码不能直接读写 a只能靠父类提供的 public 函数间接操作。存在 ≠ 有权访问。2. 哪些东西子类【不会继承】重点就算是 public 继承下面这些不属于继承的内容子类不会拿到构造函数、析构函数子类不会继承父类的构造、析构子类只是构造的时候必须调用父类构造不代表继承了它。friend友元关系友元不能继承Base 的友元不会自动变成 Son 的友元。父类的静态成员不属于子类独有是属于 Base 类本身所有对象共享一份不是子类对象自带的资源。重载的赋值运算符operator也不会被继承。补充成员函数只是代码不会复制多份继承是让子类对象可以调用这份函数代码不是复制一份函数给子类。3. 再区分三种继承方式public/protected/private 继承以public公有继承最常用举例父类 public → 子类中 public父类 protected → 子类中 protected父类 private →子类里面依然存在这个成员但是子类永远无法直接访问核心一句话private 成员在内存里还在只是对子类隐藏了访问入口。继承有啥用一句话复用代码 表达 “是一种” 的关系 支持多态这三个是最核心的。1. 代码复用最直观的好处多个类有大量重复的成员变量、成员函数就抽到父类里子类直接继承不用重复写一遍。例子// 父类动物 class Animal { protected: std::string name; public: void setName(std::string n) { name n; } void eat() { std::cout name 在吃东西\n; } }; // 子类狗、猫都属于动物继承Animal class Dog : public Animal { public: void bark() { std::cout 汪汪\n; } }; class Cat : public Animal { public: void meow() { std::cout 喵喵\n; } };Dog 和 Cat 不用再重复写name、setName、eat直接继承过来。如果没有继承Dog、Cat 里面都要拷贝一份一模一样的代码维护麻烦改一处要改多处。⚠️ 注意复用不是继承唯一目的也不是优先方案如果只是想复用代码优先用组合成员继承耦合更高。2. 表达语义is-a是一种公有继承设计初衷子类是父类的一种Dog is a Animal 狗是一种动物 ✅ 适合 public 继承Student is a Person 学生是一种人 ✅汽车里面有轮胎轮胎不是一种汽车 ❌ 这种不能用继承要用组合轮胎作为成员这个语义约束很重要违背 is-a 的继承代码很容易写得奇怪难维护。3. 实现多态最重要继承 虚函数就是 C 多态的基础。可以用父类指针 / 引用指向不同子类对象调用同一个函数执行不同版本的逻辑。class Animal { public: virtual void speak() {} }; class Dog : public Animal { public: void speak() override { std::cout 汪汪\n; } }; class Cat : public Animal { public: void speak() override { std::cout 喵喵\n; } }; void func(Animal a) { a.speak(); // 同一个调用根据实际对象自动调用狗或者猫的speak }调用Dog d; Cat c; func(d); func(c);好处func 函数不用修改以后新增鸭子、鸟只要继承 Animal、实现 speak直接传给 func。也就是对扩展开放对修改关闭开闭原则。4. 统一接口抽象类把父类做成抽象类带纯虚函数只规定接口不实现具体逻辑。强制所有子类必须实现这个接口保证一套统一规范。class Animal { public: virtual void speak() 0; // 纯虚函数抽象类不能实例化 }; // 所有继承它的子类必须实现speak5. 补充还有私有 / 保护继承很少用private 继承has-a 的一种另类复用几乎项目里很少用优先组合这个平时开发基本不用面试知道有就行。✅ 继承的缺点一定要一起记避免滥用继承继承是强耦合子类依赖父类的实现细节父类一改子类容易跟着出问题破坏封装父类内部的细节暴露给子类会带来菱形继承、二义性等坑多继承 经典设计原则优先使用组合而不是继承只有满足 is-a并且需要多态的时候才选公有继承多态一句话核心多态就是同一个接口使用不同的实例而执行不同操作。C 的多态分为两大类静态多态编译期多态和动态多态运行时多态。我们平时说的多态大多特指动态多态。一、分类总览静态多态编译期绑定 / 早绑定编译器在编译阶段就确定调用哪个函数。函数重载运算符重载模板函数模板、类模板动态多态运行期绑定 / 晚绑定⭐重点程序运行的时候才根据对象的实际类型决定调用哪个函数。 满足三个条件 ✅ 要有继承 ✅ 基类要有虚函数virtual✅ 用基类指针 / 引用指向派生类对象重点区分 静态多态看名字 参数 动态多态看指向的真实对象类型不是指针本身类型。这里也主要讲常规的多态也就是动态多态。一、动态多态核心1. 基础示例#include iostream using namespace std; // 基类 class Animal { public: // 虚函数 virtual void speak() { cout 动物在叫 endl; } }; // 派生类 class Cat : public Animal { public: // 重写虚函数函数签名完全一致 void speak() override { cout 喵喵喵 endl; } }; class Dog : public Animal { public: void speak() override { cout 汪汪汪 endl; } }; // 接收基类引用同一个接口 void doSpeak(Animal a) { a.speak(); } int main() { Cat cat; Dog dog; doSpeak(cat); // 喵喵喵 doSpeak(dog); // 汪汪汪 return 0; }如果去掉virtual不会发生多态永远调用基类speak()overrideC11 关键字显式标记重写编译器检查是否正确重写推荐加上✅ 重写override要求 函数名、参数列表、返回值协变除外必须完全一致基类函数带 virtual。❌ 区分重载、重写、隐藏重载同一个作用域函数名相同参数不同重写父子类虚函数签名完全一致隐藏父子类函数名相同没有 virtual派生类函数屏蔽基类函数注意如果是把虚函数声明写在.h头文件类里面实现写在.cpp源文件里写法和普通成员函数一样实现不加 virtual也不加override。✅ 记住virtual、override只写在类内声明处源文件实现上不能写2. 协变返回类型特殊重写派生类虚函数返回值可以是基类返回值的子类指针 / 引用也算合法重写class Base { public: virtual Base* fun() { return this; } }; class Son : public Base { public: Son* fun() override { return this; } };二、虚函数底层原理虚表 vtable 虚指针 vptr这是面试高频考点虚函数表vtable每个含有虚函数的类都会生成一张虚函数表存虚函数的地址属于类不属于对象。虚指针vptr每个对象内部会多一个隐藏指针vptr指向本类的虚表属于对象。执行流程基类指针 p - 对象里的 vptr - 虚表 - 拿到虚函数地址 - 调用示例内存模型Animal类虚表Animal::speak Cat类虚表 Cat::speak Dog类虚表 Dog::speak当Animal* p new Cat;p 指向 Cat 对象对象里的 vptr 指向 Cat 的虚表调用p-speak()就找到 Cat 的版本。⚠️ 注意虚表在编译阶段就构造好放在只读数据段vptr 在构造函数中初始化构造函数里面调用虚函数不会多态此时子类还没构造完成vptr 还是基类的。析构函数最好写成虚析构虚析构函数非常重要class Animal { public: virtual ~Animal() { cout Animal析构 endl; } }; class Cat : public Animal { public: ~Cat() override { cout Cat析构 endl; } }; int main() { Animal* p new Cat; delete p; // 如果析构不是virtual只会调用Animal析构内存泄漏 return 0; }规则只要类有可能被继承并且用基类指针释放派生类对象析构必须virtual。三、纯虚函数 抽象类纯虚函数格式virtual void func() 0;没有函数实现0代表纯虚包含纯虚函数的类叫抽象类抽象类不能实例化对象派生类必须重写所有纯虚函数否则派生类依然是抽象类。class Shape { public: virtual void area() 0; // 纯虚函数 }; class Circle : public Shape { public: void area() override { cout 计算圆形面积 endl; } };抽象类一般用来做接口规范也就是接口类。C 没有专门的 interface 关键字抽象类就是 C 的接口。四、常见坑点总结值传递切割slicingvoid test(Animal a) { // 值传递不是引用/指针 a.speak(); }传入 Cat 对象时会发生对象切片只保留基类部分丢失子类信息没有多态。 多态必须用指针或者引用。静态函数不能是虚函数static 不属于对象没有 vptr。内联函数inline可以是 virtualinline 只是建议运行时虚调用会忽略 inline。友元函数不能是虚函数友元不是类成员。私有虚函数C 允许 private virtual依然可以被子类重写只是外部不能直接调用。class Base { private: virtual void foo(){} }; class Son : public Base { public: void foo() override {} };这个语法合法设计模式里偶尔用到。虚函数带来开销每个对象多一个 vptr虚表占空间调用时多一次查表一般开销很小不用纠结。五、静态多态 vs 动态多态对比表特性静态多态动态多态绑定时机编译期运行期实现方式重载、模板继承 virtual 基类指针 / 引用开销无运行时开销vptr、虚表查找少量开销灵活性低高C 范式泛型编程OOP 面向对象现代 C模板静态多态性能更好虚函数动态多态适合需要运行时灵活切换类型的场景。多态的意义多态的目标让上层代码只依赖父类抽象接口不用关心具体是哪个子类新增子类的时候原来的上层代码不用改动。也就是实现面向对象里非常重要的开闭原则对扩展开放对修改关闭。我们先对比「没有多态怎么写代码」你一下子就能感受到痛点。场景动物叫没有多态class Dog { public: void speak() { cout 汪汪; } }; class Cat { public: void speak() { cout 喵喵; } }; // 想要统一处理只能写多个重载或者if判断 void test(Dog d) { d.speak(); } void test(Cat c) { c.speak(); }或者写一个函数里面 if 判断类型void test(string type){ if(type dog){ Dog d; d.speak(); } else if(type cat){ Cat c; c.speak(); } }❌ 问题来了以后我再加鸭子 Duck、小鸡 Chick……每加一种动物我就要去修改这个 test 函数增加 if 分支 / 增加重载函数。改老代码就有风险改错、引入 bug。有多态之后上层只写一份函数只认识父类 Animalvoid test(Animal a) { a.speak(); }以后新增鸭子class Duck : public Animal{ void speak() override { cout 嘎嘎; } };✅原来的 test 函数一行都不用改直接传 Duck 对象进去就能用。上层代码稳定不动功能只通过新增子类来扩展。再提炼多态带来的几个价值1. 解耦上层和具体子类解绑上层业务代码只和抽象父类打交道不知道、也不需要知道底层是哪个子类。比如框架写一个 “图形渲染器”只知道基类 Shape调用draw()后面可以放圆形、矩形、三角形…… 渲染器完全不用改框架、库大量依赖这个思想框架写通用逻辑使用者新增子类来扩展功能。2. 统一一套接口规范搭配抽象类 / 纯虚函数父类定好所有子类都必须实现speak()/draw()。只要继承这个抽象类接口名字、调用方式固定不容易写乱。相当于父类立下规矩。3. 代码好组织分层清晰抽象层父类定义能力具体实现层各个子类各自实现自己独特的逻辑业务分层清晰不会全部揉在一堆 if else 里面if-else 多了就是臭代码叫 “分支地狱”一个很重要的误区不是 “为了同一个函数名能做不同事” 才做多态函数重载也能同名不同行为重载是编译期就确定好调用哪个适合参数不一样的场景多态是同一个接口、同一个参数对象不一样行为不一样适合一套框架接纳无限多新实现。对比重载函数名相同参数不同 → 编译期多态虚函数运行时多态接口一模一样对象类型不同 → 运行时多态
阅读完成 · 觉得有帮助?