千面之法: 释放 C++ 多态的灵活威力

千面之法: 释放 C++ 多态的灵活威力

目录

1:多态的概念

1.1:概念

2.多态的定义与实现

2.1:多态的构成条件

2.2:虚函数

2.3:虚函数的重写

2.3.1:虚函数重写的两个例外

2.3.1.1:协变(基类与派生类函数的返回值不同,基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用时)

2.3.1.2:析构函数的重写

2.4:C++11 override和final

2.4.1:final关键字

2.4.2:override关键字

2.5:重载、重写、 隐藏的对比

3.抽象类

3.1:概念

3.2:接口继承与实现继承

4:多态的原理

4.1:虚函数表

4.1.1:代码1

4.1.2:代码2

4.2:多态的原理

4.3:动态绑定与静态绑定

5:单继承与多继承关系的虚函数表

5.1:单继承中的虚函数表

5.2:多继承中的虚函数表

6:多态相关的问题


1:多态的概念

1.1:概念

通俗来讲,多态就是多种形态,具体一些就是当去完成某个行为的时候,当不同的对象去完成时会产生出不同的状态.

2.多态的定义与实现

2.1:多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为.譬如Student继承了Person,Person对象买票全价,Student对象买票半价.那么在继承中要构成多态还有两个条件1.必须通过基类的指针或者引用调用虚函数2.被调用的函数必须是虚函数,且派生类对基类的虚函数进行重写.
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; class Adult { public: virtual void BuyTicket() { cout << "买票----全价" << endl; } }; class Child : public Adult { public: virtual void BuyTicket() { cout << "买票----半价" << endl; } }; /* * 多态的条件 * 1.必须要有继承关系 * 2.必须要有虚函数(父类的虚函数和子类的虚函数,要求三同(函数名,参数名,返回值) */ void Func(Adult& a) { a.BuyTicket(); } int main() { Adult a; Child c; Func(a); Func(c); return 0; }

2.2:虚函数

概念:被virtual修饰的类成员函数称为虚函数

class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl;} };

2.3:虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类的虚函数返回值类型,函数名,参数列表完全相同),称子类的虚函数重写了基类的虚函数.

#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; class Adult { public: virtual void BuyTicket() { cout << "买票----全价" << endl; } }; class Child : public Adult { public: virtual void BuyTicket() { cout << "买票----半价" << endl; } }; /* * 多态的条件 * 1.必须要有继承关系 * 2.必须要有虚函数(父类的虚函数和子类的虚函数,要求三同(函数名,参数名,返回值) */ void Func(Adult& a) { a.BuyTicket(); } int main() { Adult a; Child c; Func(a); Func(c); return 0; }

2.3.1:虚函数重写的两个例外

2.3.1.1:协变(基类与派生类函数的返回值不同,基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用时)

派生类重写类虚函数时,与基类虚函数返回值类型不同,即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或者引用时.

2.3.1.2:析构函数的重写

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同.虽然函数名不相同,起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理称destructor.

#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; class Person { public: virtual ~Person() { cout << "~Person()" << endl; } }; //公有继承 class Student : public Person { virtual ~Student() { cout << "~Student" << endl; } }; int main() { Person* p1 = new Person; Person* p2 = new Student; delete p1; delete p2; return 0; }

2.4:C++11 override和final

C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

2.4.1:final关键字

修饰虚函数,表示该虚函数不能够再被重写

#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; class Car { public: virtual void Drive() final { } }; class Benz: public Car { virtual void Drive() { cout << "Benz" << endl; } }; int main() { return 0; }
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; //C++11的方法: final修饰的类叫最终类,不能继承 class Car final { public: private: // C++98的方法:父类的构造函数私有 // 子类的构造无法生成和实现,导致子类对象无法实例化 Car() { } }; class Benz :public Car { public }; int main() { Benz b; return 0; }

2.4.2:override关键字

检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错.

#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; class Car { public: //virtual void drive() //{ //} }; class Benz : public Car { public: virtual void Drive() override { cout << "virtual void Drive()" << endl; } }; int main() { return 0; }

2.5:重载、重写、 隐藏的对比

重载两个函数在同一个作用域函数名相同/参数不同重写(覆盖)两个函数分别在基类与派生类的作用域.函数名/参数/返回值都必须相同(满足三同)协变除外.两个函数必须是虚函数.重定义(隐藏)两个函数分别在基类与派生类的作用域函数名相同两个基类和派生类的同名函数不构成重写就是重定义.

3.抽象类

3.1:概念

在虚函数的后面加上 = 0,则这个函数被成为纯虚函数.包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能够实例化出对象.派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象.纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承.

#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; class Car { public: virtual void Drive() = 0; }; class Benz : public Car { public: }; class BMW: public Car { public: virtual void Drive() { cout << "BMW()" << endl; } }; int main() { Car* pBenz = new Benz; pBenz->Drive(); Car* pBMW = new BMW; pBMW->Drive(); return 0; }

3.2:接口继承与实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数

4:多态的原理

4.1:虚函数表

4.1.1:代码1

#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; class Base { public: virtual void Func1() { cout << "Func1" << endl; } private: int _b = 1; }; int main() { Base b; cout << sizeof(b) << endl; return 0; }
通过测试可以发现b对象是8字节.除了_b成员,还多了一个_vfptr放在对象的前面(有些平台可能会放到对象的最后面,这个跟平台有关系),对象中的这个指针叫做虚函数指针表(v代表virtual,f代表function).一个含有虚函数的类中至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表.

4.1.2:代码2

#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; class Base { public: virtual void Func1() { cout << "Base:Func1()" << endl; } virtual void Func2() { cout << "Base:Func2()" << endl; } virtual void Func3() { cout << "Base:Func3()" << endl; } private: int _b = 1; }; class Derive :public Base { public: virtual void Func1() { cout << "Derive:Func1()" << endl; } virtual void Func2() { cout << "Derive:Func2()" << endl; } private: int _d = 2; }; int main() { Base b; Derive d; return 0; }

4.2:多态的原理

#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl; } }; class Student :public Person { public: virtual void BuyTicket() { cout << "买票-半价" << endl; } }; void Func(Person& p) { p.BuyTicket(); } int main() { Person Mike; Func(Mike); Student Johnson; Func(Johnson); return 0; }

4.3:动态绑定与静态绑定

5:单继承与多继承关系的虚函数表

5.1:单继承中的虚函数表

#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; class Base { public: virtual void Func1() { cout << "Base::Func1()" << endl; } virtual void Func2() { cout << "Base::Func2()" << endl; } private: int _a = 1; }; class Derive : public Base { public: virtual void Func1() { cout << "Derive::Func1()" << endl; } virtual void Func3() { cout << "Derive::Func3()" << endl; } virtual void Func4() { cout << "Derive::Func4()" << endl; } private: int _b = 2; }; //打印对象虚基表,对象虚基表本质是一个函数指针数组 typedef void(*Vfptr)(); void PrintVfptr(Vfptr * vft) { for(size_t i = 0; i < 4; i++) { cout << vft[i] << "->"; vft[i](); } } int main() { Base b; Derive d; Vfptr* ptr = (Vfptr*)(*(int*)(&d)); PrintVfptr(ptr); return 0; }

5.2:多继承中的虚函数表

#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; //定义函数指针 typedef void (*Vfptr)(); class Base1 { public: virtual void func1() { cout << "Base1::func1()" << endl; } virtual void func2() { cout << "Base1::func2()" << endl; } private: int _b1; }; class Base2 { public: virtual void func1() { cout << "Derive:func1()" << endl; } virtual void func3() { cout << "Derive:func3()" << endl; } private: int _d1; }; class Derive : public Base1, public Base2 { virtual void func1() { cout << "Derive:func1()" << endl; } virtual void func3() { cout << "Derive:func3()" << endl; } private: int _d1; }; void PrintTable(Vfptr Vtable[]) { cout << "虚表地址>" << Vtable << endl; for (size_t i = 0; Vtable[i] != nullptr; i++) { cout << "第" << i << "个虚函数地址: 0X" << Vtable[i] << endl; Vtable[i](); } cout << endl; } int main() { Derive d; /* * (*(int*)(&d))强制类型转换为指针类型并且解引用,那么每次在访问的时候只访问四个字节的数据 * 取出d对象的头4个字节,就是虚表的指针,虚表的本质是存了一个虚函数的指针数组,这个数组最后面放了一个Nullptr */ Vfptr* vTableb1 = (Vfptr*)(*(int*)&d); PrintTable(vTableb1); /* * 强转成char *,char*类型的指针每次解引用跳过一个字节, */ Vfptr* vTableb2 = (Vfptr*)(*(int*)((char*)&d + sizeof(Base1))); PrintTable(vTableb2); return 0; } 

6:多态相关的问题

什么是多态.
  • 通俗来说,就是多种形态,具体一些就是去完成某个行为,当不同的对象去完成的时候会产生出不同的状态.
什么是重载、重写(覆盖)、重定义(隐藏)
  • 重载

(1):两个函数在同一个作用域.

(2):函数名相同/参数不同.

  • 重写(覆盖)

(1):两个函数分别在基类与派生类的作用域.

(2):要满足三同(函数名/参数/返回值都必须相同) PS:协变除外.

(3):两个函数都必须是虚函数.

  • 重定义(隐藏)

(1):两个函数分别在基类和派生类的作用域

(2):函数名相同

(3):两个基类和派生类的同名函数(不构成重写就是重定义).

inline(内联函数)可以是虚函数吗
  • 可以,不过编译器如果忽略inline属性,那么这个函数就不再是inline函数,而是会把虚函数表放到虚函数中.
静态成员函数可以是虚函数吗
  • 不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表
构造函数可以是虚函数吗
  • 不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的.
析构函数可以是虚函数吗
  • 可以,并且最好把基类的析构函数定义成虚函数.
什么场景下析构函数是虚函数
  • 如果基类的析构函数为虚函数,此时派生类的析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写.
对象是访问普通函数快还是虚函数快
  1. 首先如果是普通对象,是一样快的.、
  2. 如果是指针对象或者引用对象,则调用普通函数快一些,因为构成了多态,运行时调用虚函数需要到虚函数表中去查找.
虚函数表是在什么阶段生成的
  • 虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的.
C++菱形继承的问题,虚继承的原理
  • 菱形继承造成了数据冗余和二义性

Read more

人工智能:预训练语言模型与BERT实战应用

人工智能:预训练语言模型与BERT实战应用

人工智能:预训练语言模型与BERT实战应用 1.1 本章学习目标与重点 💡 学习目标:掌握预训练语言模型的核心思想、BERT模型的架构原理,以及基于BERT的文本分类任务实战流程。 💡 学习重点:理解BERT的双向注意力机制与掩码语言模型预训练任务,学会使用Hugging Face Transformers库调用BERT模型并完成微调。 1.2 预训练语言模型的发展历程与核心思想 1.2.1 为什么需要预训练语言模型 💡 传统的自然语言处理模型(如LSTM+词嵌入)存在两个核心痛点:一是需要大量标注数据才能训练出高性能模型,二是模型对语言上下文的理解能力有限。 预训练语言模型的出现解决了这些问题。它的核心思路是先在大规模无标注文本语料上进行预训练,学习通用的语言知识和语义表示,再针对特定任务进行微调。这种“预训练+微调”的范式,极大降低了对标注数据的依赖,同时显著提升了模型在各类NLP任务上的性能。 预训练语言模型的发展可以分为三个阶段: 1. 单向语言模型阶段:以ELMo为代表,通过双向LSTM分别学习正向和反向的语言表示,再拼接得到词向量。但ELMo本质还

By Ne0inhk
Linux中的patch和diff命令完全指南

Linux中的patch和diff命令完全指南

🔥作者简介: 一个平凡而乐于分享的小比特,中南民族大学通信工程专业研究生,研究方向无线联邦学习 🎬擅长领域:驱动开发,嵌入式软件开发,BSP开发 ❄️作者主页:一个平凡而乐于分享的小比特的个人主页 ✨收录专栏:Linux,本专栏目的在于,记录学习Linux操作系统的总结 欢迎大家点赞 👍 收藏 ⭐ 加关注哦!💖💖 Linux中的patch和diff命令完全指南 目录 1. 什么是diff和patch? 2. diff命令详解 3. patch命令详解 4. 实战场景应用 5. 最佳实践与技巧 什么是diff和patch? diff和patch是一对相辅相成的工具,用于比较文件差异和应用补丁。简单来说: * diff:比较两个文件/目录的差异,生成补丁文件 * patch:将diff生成的补丁应用到原文件上 原文件 old.txt diff命令 新文件 new.txt 补丁文件 patch.diff patch命令

By Ne0inhk
Flutter 三方库 changelog_cli 的鸿蒙化适配指南 - 自动化生成 CHANGELOG、标准化版本管理与工程化协作利器

Flutter 三方库 changelog_cli 的鸿蒙化适配指南 - 自动化生成 CHANGELOG、标准化版本管理与工程化协作利器

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 changelog_cli 的鸿蒙化适配指南 - 自动化生成 CHANGELOG、标准化版本管理与工程化协作利器 前言 在 Flutter for OpenHarmony 的企业级开发流程中,维护一份详实、规范的更新日志(CHANGELOG)是版本控制的核心环节。changelog_cli 是一个专为 Flutter 开发者设计的命令行工具,它能够基于特定的规范自动生成或更新日志。本文将探讨如何将该工具集成到鸿蒙项目的开发流水线中,大幅提升工程化协作效率。 一、原理解析 / 概念介绍 1.1 基础原理 changelog_cli 通过读取项目的 pubspec.yaml 版本信息和特定的配置文件,配合开发者在命令行输入的更新内容,自动拼装成符合 Keep a Changelog 规范的

By Ne0inhk
Linux:多线程---初识线程

Linux:多线程---初识线程

文章目录 * 1. 如何理解Linux中的线程? * 2. 重新定义线程和进程 * 3.重谈地址空间---多线程 * 4. Linux线程周边的概念 * 4.1 线程的优点 * 4.2 线程的缺点 * 4.3 线程异常 * 4.4 线程用途 * 4.5 线程库中的接口使用 * 序:本章之后,我们对Linux系统部分的知识就到这里结束,这是Linux系统部分的最后一个知识点—多线程!!!本章我们将知道什么是线程,并且重新理解什么是进程,以及Linux下特有的线程,还有线程的优缺点等等 线程:是进程内的一个执行分支。线程的执行粒度要比进程细。 1. 如何理解Linux中的线程? 地址空间是进程的资源窗口(加载动态库、申请内存和定义临时变量等,都要通过地址空间来完成!!!) 进程(1个task_struct,1个地址空间): 进程(多个task_struct,1个地址空间)

By Ne0inhk