关键词:
目录
前面我们已经大概分析了:封装和继承。
- 封装:将成员变量私有化,然后提供读写的接口供别人进行调用。
- 继承:子类继承父类的成员变量。
先来分析一个 父类指针 和 子类指针
-
首先明确指针的目的是为了指向对象,所以就会有, 父类指针指向子类对象,和 子类指针指向父类对象。
-
父类指针指向子类对象:父类指针可以指向子类对象,是安全的,开发中经常用到(继承方式必须是public)
-
子类指针指向父类对象:子类指针指向父类对象是不安全的 (后面分析为什么是不安全的)
提供一个好记忆的办法:
- 父类指针指向子类对象:学生属于一个人,所以人可以指向学生。
- 子类指针指向父类对象:人不一定都是学生,所以学生不能指向人。
分析安全问题:
父类指针指向子类对象是安全的
- 访问范围:父类指针只能访问父类当中成员变量。
- 拥有范围:子类对象当中肯定拥有父类的全部成员变量。
子类指针指向父类对象是不安全的
- 访问范围:子类指针不仅可以访问父类当中的全部成员,而且可以访问自己的独立的成员。
- 拥有范围:父类对象当中,肯定不会包含子类当中独特的成员变量。
多态
先来铺垫一个项目:假设我们要写一个动物园训练系统
#include <iostream>
using namespace std;
class Cat
public:
void speak() cout << "miao miao miao " << endl;
void eat() cout << "miao eat " << endl;
;
class Dog
public:
void speak() cout << "wang wang wang " << endl;
void eat() cout << "wang eat " << endl;
;
class Pig
public:
void speak() cout << "heng heng heng " << endl;
void eat() cout << "heng eat " << endl;
;
// 训练猫
void train_Cat(Cat *p)
p->speak();
p->eat();
// 训练狗
void train_Dog(Dog *p)
p->speak();
p->eat();
// 训练猪
void train_Pig(Pig *p)
p->speak();
p->eat();
int main()
train_Cat(new Cat);
train_Dog(new Dog);
train_Pig(new Pig);
getchar();
return 0;
运行结果:
miao miao miao
miao eat
wang wang wang
wang eat
heng heng heng
heng eat
分析:
- 每个动物训练的项目有很多相同的,比如 吃饭 、 说话、睡觉 等等
- 假设我们有上百种动物,我们的 train_xxx 函数就需要写上百个。(非常麻烦)
解决:
- 抽离出共同的项目,然后将他们放到父类当中。
- 然后让子类去继承。
1、重写
我理解的重写有几点要求:
1、是子类 重新写 父类的成员函数。
2、重写函数的参数列表必须相同,(不相同的话就成了重载)
3、返回值、函数名、参数列表,都必须和父类的成员函数,完全一模一样。
本能的认为:p 指向的 Cat 对象,那么调用的函数也应该是 Cat 的成员函数呀。
可以发现结果是不如人意的
分析:产生这种情况的原因:
- 默认情况下,C++是不存在多态的。
- C++编译器只会根据指针类型调用对应的函数
- 父类的指针,那么就调用父类的成员函数。 子类的指针,那么就调用子类的成员函数。
测试:
分析汇编:
- 根本不会进行对象检查
- 根本不理会指向的是一个什么对象
2、虚函数
解决办法:使用虚函数 (先不要理解为什么,后面进行分析)
虚函数:被virtual修饰的成员函数
C++中的多态通过虚函数(virtual function)来实现
- 只要在父类中声明为虚函数
- 子类中重写的函数也自动变成虚函数(也就是说子类中可以省略virtual关键字)
3、总结多态
我认为的多态:重写函数 + Vritual关键字
- 子类必须要重写父类的成员函数。
- 父类的对应的成员函数,必须添加 virtual 关键字。
多态的含义:
- 同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。(函数重写,注意不是重载)
- 在运行时,可以识别出真正的对象类型,调用对应子类中的函数。(虚函数:可以识别出真正的对象类型)
多态的要素
- 子类重写父类的成员函数(override)
- 父类指针指向子类对象
- 利用父类指针调用重写的成员函数
注意:重写 和 Vitual 缺一个都不能构成多态。
-
缺重写:本身就只有一个函数,根本就没有多种形态。
-
缺Vitual:识别不了对应的对象。
4、虚函数表
- 虚函数的实现原理是虚表,这个虚表里面存储着最终需要调用的虚函数地址,这个虚表也叫虚函数表
问题:虚函数表在哪块内存里面?
- 答:在系统给分配的一块内存里面。
问题:怎么找到虚函数表?
- 答:在每个对象的内存当中有一个指针,指向这个虚函数表。
分析这 12 个字节分别是什么:
- 最前面的4个字节:存放虚函数表的地址值。
- 后面的 8 个字节 : 存放成员变量。
分析虚表里面的值:
- 0-3 字节:speak() 函数的首地址
- 4-7 字节:eat() 函数的首地址
注意:所有的 Pig 对象(不管在全局区、栈、堆)共用同一份虚表.
再次分析多态的原理:
(1)如果对象里面没有指向虚函数表的指针,父类指针就根本找不到虚函数表,那么就根本无法调用子类重写的函数。
(2)如果有了 virtual 关键字,那么 对象里面就有了 指向虚函数表的指针,父类指针 就可以顺着找到子类重写的函数。
5、虚表的汇编
首先了解一点:
call 的类型有两种情况
-
直接 call: 以 E8 开头,直接调用指定函数地址的函数。
-
间接 call: 以 FF 开头,直接调用 eax 当中放的地址,随之 eax 的值发生改变,调用的地址也发生改变。
通过指针来间接调用:
虚函数的作用:
- 挺高动态性,函数可以进行动态调用。而不是编译的时候就弄好了。
6、虚表的细节
(1)每一个类,都有自己独立的虚表。 同一类的对象共用一个虚表。
(2)调用虚函数的时候,子类当中没有重写该函数,那么调用哪个函数呢?
- 调用的是父类的成员函数
- 虚表里面仍然还有 2 个函数地址。(8 个字节)
- 只是第一个并不是子类的,而是父类的成员函数。
(3)疑问3:当我们是父类指针,指向父类对象的时候,会产生虚表嘛?
- 也是通过虚表,进行动态调用的。
总结:真正实现的多态,对父类对象和子类对象一视同仁。
- 只要有虚函数,无论子类还是父类都会生成虚表。
- 都是通过对象前四个字节,间接找到虚表的地址,从而调用虚函数。
(4)父类被声明为虚函数,子类会自动变为虚函数。
而子类被声明为虚函数,父类并不会自动变为虚函数。
(5)子类有的方法,父类当中没有这个方法,那会怎么调用?
- 如果父类当中没有这个方法,就不能通过父类指针来进行多态调用。
再会到多态的本质:
- 子类必须重写该父类的函数。(所以 当父类当中没有这个方法,子类就构不成重写)
- 将对应的函数变为虚函数
- 最后,通过父类指针来通过对象当中前 4 个的虚函数指针,来实现动态调用。
7、调用父类的成员函数实现
铺垫:
我们重写父类函数的目的有两种情况:
第一种:
- 父类函数的思想逻辑,我们一点也不想要
- 我们要全部自己重新写
第二种:
- 父类函数的思想逻辑,我们可以参考一点
- 保留父类当中的代码,我们只写一部分自己独特的思想逻辑
8、虚析构函数
如果有多态,应该将析构函数声明为虚函数(虚析构函数) 。
为什么要这样做呢?
分别分析以下的两种情况:
不变为虚函数:
不会有虚表产生,就是直接调用,根本不会去找子类的析构函数。
delete 父类指针的时候,只会调用父类的析构函数,并不会调用子类的析构函数,析构不完整。
变为虚函数:
产生虚表,将析构函数放到虚表当中,指针就可以找到子类的析构函数。
delete父类指针时,才会先调用子类的析构函数,然后调用父类的析构函数,保证析构的完整性 。
9、纯虚函数、抽象类
首先不要被唬住,不难就是一个定义而已。
铺垫有一种情况:
父类当中的函数很抽象,我们无法具体的进行实现,所以将他设置为 纯虚函数。
纯虚函数:没有函数体,且初始化为0的虚函数,用来定义接口规范。
怎么理解定义接口规范:(提示其他人怎么编写代码)
-
比如作为一个动物,应该有这些最基本的函数(eat、speak、run)。
-
告诉子类,你们应该自己实现这个函数。具体怎么实现,自己去写。
抽象类:
- 含有纯虚函数的类,不可以实例化(不可以创建对象)。(只要有一个纯虚函数,那么就不可以实例化)
- 抽象类也可以包含非纯虚函数、成员变量
- 如果父类是抽象类,子类没有完全重写纯虚函数,那么这个子类依然是抽象类。
多继承
很多编程语言都没有这个特性,因为太复杂了。
多继承:C++允许一个类可以有多个父类(不建议使用,会增加程序设计复杂度)
分析下列代码:
1、有一个 Student 类,属性为 score,方法为 study。
2、有一个 Worker 类,属性为 salary,方法为 work。
这时候出现一个 Undergraduate 类,是一个大学生,他可以一边做学生,也可以一边兼职打工。
3、Undergraduate 类,有自己独特的属性 grade (年级),独特的方法 play。
Undergraduate 将他所有的父类成员变量都继承了过来。
1、多继承体系下的构造函数调用
2、多继承的虚表
- 如果子类继承的多个父类都有虚函数,那么子类对象就会产生对应的多张虚表
3、同名函数、成员变量的调用
- 同名函数的调用
- 同名变量的调用:
4、菱形继承
不要被唬住,就是继承模式看起来像是一个菱形。
问题:
1、成员变量的冗余、重复。
- Student、Worker 都继承了 m_age ,所以 Student 和 Worker 都有 m_age 成员变量。故每个都有两个成员变量。
- Undergraduate 里面就有 5 个成员变量,一共20个字节。其中有 2 份m_age
struct Undergraduate
int m_age;
int m_score;
int m_age;
int m_salary;
int m_grade;
2、二义性:我们不知道 m_age 来自于 Student 还是 Worker
5、虚继承
- 虚继承可以解决菱形继承带来的问题
写代码:
- 两个东西必须同时虚继承同一个类
虚继承分析各个对象的内存分布:
注意:
- 一但是虚继承,会将虚基类的成员变量放到最后面。
- 一但是虚继承,对象内存当中,会多出 4 个字节(虚表指针)。 (此虚表指针并不是指向虚函数的虚表指针)
虚表的内容:
0:虚表指针 与 本类起始的偏移量。(虚表指针放在本类起始的第一个)
8/20:虚基类第一个成员变量 与 本类起始的偏移量 (虚基类第一个成员变量 在本类当中的什么位置)
内存大小:
- Student/Worker 类占有 12 个字节。
- Undergraduate 类 占有 24 个字节。
6、多继承的应用
假设有一个兼职中心,招聘兼职,岗位如下:
1、保姆:要求 扫地、做饭
2、老师:要求 踢足球、打篮球
应聘的角色:
1、在校大学生
2、上班族
分析:
- 招聘机构定义接口规范,标明保姆需要会什么,老师需要会什么。
- 然后应聘者自己去继承,从而实现这些接口。
#include <iostream>
using namespace std;
class JobBaomu
virtual void clean() = 0; // 保姆必须符合这两个条件,但是你具体怎么做饭、怎么打扫自己实现
virtual void cook() = 0;
;
class JobTeacher
virtual void playFootball () = 0;
virtual void playBasketball () = 0;
;
// 学生这两个职业都可以胜任,所以可以都继承下来
class Student : public JobBaomu, public JobTeacher
int m_score;
public:
// 这些需要我们自己来实现
void clean()
void cook()
void playFootball()
void playBasketball()
;
class Worker
int m_salary;
;
int main()
getchar();
return 0;
static 静态成员
静态成员:分为两种
含义:被static修饰的 成员变量 和 成员函数 。(一定要注意可以分为两种)
怎么访问?
- 对象(对象 . 静态成员)
- 对象指针(对象指针 -> 静态成员)
- 类访问(类名 :: 静态成员)
1、静态成员变量
接下来分析静态成员变量的特点:
内存位置:存储在数据段(全局区,类似于全局变量),整个程序运行过程中只有一份内存。
怎么理解只有一个内存?
- 所有对象共用这一个成员变量。
- 对象没有创建之前,就有这个静态成员变量。(静态成员变量是不依赖与对象的)
2、对比全局变量,它可以设定访问权限(public、 protected、 private),达到局部共享的目的。
- 全局变量:在外面我们不能设定访问权限,外面都可以访问。
- 静态变量设置为 public :外面也可以访问。
- 静态变量设置为 protected :父类及其子类都可以访问
- 静态变量设置为 private:只有这个类才可以访问。
3、必须初始化,必须在类外面初始化,初始化时不能带static,如果类的声明和实现分离(在实现.cpp中初始化)
2、静态成员函数
怎么访问?
- 对象(对象 . 静态成员)
- 对象指针(对象指针 -> 静态成员)
- 类名访问(类名 :: 静态成员)
静态成员函数的特点:
1、内部不能使用this指针(this指针只能用在非静态成员函数内部)
2、内部不能访问非静态成员变量\\函数,只能访问静态成员变量\\函数
3、非静态成员函数内部可以访问静态成员变量\\函数 (很简单,因为在全局区,所以可以进行访问)
为什么?我们先来分析 this 是干什么用的,this 指向对象的首地址。 (所以说 this 是紧紧依附于对象的)
再来分析类成员函数
- 因为可以通过类名来进行访问,所以他是不依赖于对象的。
- 既然不依赖于对象存在,所以 this 指针就没有意义了。
4、不能是虚函数(虚函数只能是非静态成员函数)
思考:虚函数是用在多态上面。(父类指针 -> 子类对象),所以说是牵着到对象,所以不可以。
5、构造函数、析构函数不能是静态
思考:构造函数、析构函数,是在对象创建和销毁的时候调用的,所以说也是牵扯着对象。
6、当声明和实现分离时,实现部分不能带static
3、静态成员汇编分析
-
普通成员变量:放在栈空间上
-
静态成员变量:放在 data segment 上面(地址是写死的)
-
全局变量:放在 data segment 上面
4、静态成员的继承
首先明确一点:静态成员只有一份,不会被继承!!!
继承只是继承 非静态成员变量。
5、static 的应用
假设我们想要监控该类对象,到底产生了多少个?
思路:
- 通过构造函数,因为每创建一个对象,就会调用一次构造函数。
- 通过析构函数,因为每销毁一个对象,就会调用一次析构函数。
这个变量为普通成员变量:
全局变量
- 外面所有函数都可以访问,所以就比较危险。
- 我们希望只有 构造函数 和 析构函数 可以访问。
private 静态变量:
改进:留个接口来进行访问
- 依赖于对象,至少创建一个对象。
- 只能通过对象来进行访问。
继续改进:使用静态成员函数
- 不依赖于对象,不需要创建一个对象
- 通过类名来直接访问。
6、静态成员经典应用 – 单例模式
单例模式:设计模式当中的一种
应用场合:保证每个类永远只创建一个对象。
- 在百度网盘运行的时候,只有这一个窗口图标。
单例模式的步骤:
1、构造函数私有化(private)———— 外部就不能创建对象了。
2、既然类外不能创建,所以我们要在类内进行创建。
分析需求:想要通过一个函数来创建一个对象
- 参数:不需要
- 返回值:返回对象的首地址。
要求:只能创建一个对象。
- 第一次调用:返回新 new 的对象的首地址。
- 第二次调用:返回之前对象的首地址。
改进:将函数改为 static 函数。
缺点:
- 全局变量到处可以修改
- 通过修改全局变量,可以创建多个对象
改进:将全局变量变为类内私有静态变量。
完善一个 delete 的接口:
总结单例模式:
1、构造函数私有化
2、定义一个私有的 static 成员变量指向唯一的那个单例对象
3、提供一个公共的访问单例对象的接口
7、单例模式的完善
单例模式:只能生成一个对象。
缺点:对象赋值的时候,不报错。 因为只有一个对象,赋值也没有意义。
改进: 将 = (赋值运算符)进行重载
const 类型的参数,不能进行赋值运算。
const 成员
首先要明白,const 成员也分为两部分:
- const 成员变量
- const 成员函数
1、const 成员变量
语法糖:
-
必须初始化(类内部初始化),可以在声明的时候直接初始化赋值。(static 必须在全局区初始化)
-
非static的const成员变量还可以在构造函数的初始化列表中初始化。
-
非静态的 const 成员变量,在每一份对象当中都存在。
const 的参数可以接收:非const 和 const 成员两种参数
2、const 成员函数
特点:
- const关键字写在参数列表后面,函数的声明和实现都必须带const
- 内部不能修改非static成员变量、成员函数 (限制内部不能改变普通成员变量、成员函数 )
-
内部只能调用 const成员函数、 static成员函数
因为 const成员函数、 static成员函数 都不能访问 非static成员变量。
- const成员函数和非const成员函数构成重载
- const 对象调用 const 成员函数。
为什么要这么做?
- 当我们要定义const 对象的时候,我们希望 const 对象当中的 非 static 元素不能进行修改。
- 正好 cosnt 成员函数当中就不可以对 非 static 元素进行修改。
- 普通对象:改不改都可以
- const 对象:必须不能改
总结:
- 非const对象(指针)优先调用非const成员函数 ,实在没有非const成员函数,才会调用 const 成员函数。
- const对象(指针)只能调用const成员函数、 static成员函数
3、引用类型成员
c++多态(代码片段)
文章目录一.多态的概念二.多态的定义和实现(1).虚函数(2).虚函数的重写(3).多态的构成条件(4).override/final(5).重载/隐藏/重写的区别三.抽象类四.多态的原理(1).虚函数表(2).单继承中的虚函数表(3).多继承中的虚函数表一.多态的概念... 查看详情
c++多态(代码片段)
文章目录概念一、多态的定义及实现1.多态的构成条件2.virtual关键字和虚函数3.虚函数重写4.虚函数重写的三个例外(1)协变(2)析构函数重写①不需要重写虚构函数②需要重写析构函数(3)子类的虚函数... 查看详情
c++多态(代码片段)
文章目录概念一、多态的定义及实现1.多态的构成条件2.virtual关键字和虚函数3.虚函数重写4.虚函数重写的三个例外(1)协变(2)析构函数重写①不需要重写虚构函数②需要重写析构函数(3)子类的虚函数... 查看详情
[c++]多态详解(代码片段)
...erride和final关键字3.3重载、重写、重定义的对比4.抽象类5.多态的原理5.1问题提出5.1.1包含虚函数的类和不包含虚函数的类有什么区别?5.2.2派生类与基类的虚表是否相同?5.2虚函数表5.2.1虚函数表概念5.2.2虚函数表的构建规... 查看详情
[c++]面向对象语言三大特性--多态(代码片段)
大家这篇文章描述的是我在学习多态时的一些学习笔记,和问题,还有一些面试题,希望这篇文章对你会有帮助文章目录什么是多态?如何才可以看到多态这一属性呢?(概念)什么虚函数?如何使用多态... 查看详情
c++中的多态(代码片段)
目录前言一.多态的概念 1.1概念 1.2多态的构成条件 1.3 虚函数 1.4虚函数的重写 1.4.1协变 1.4.2析构函数的重写 1.4.3C++11里的override和final关键字 1.5抽象... 查看详情
c++基础语法多态(代码片段)
C++基础语法(六)多态一、什么是多态1、静态的多态2、动态的多态二、构成多态的条件与实现1、多态的构成条件问题一:什么是虚函数问题二:什么是函数重写2、纯虚函数和抽象类纯虚函数的定义抽象类3... 查看详情
c++基础语法多态(代码片段)
C++基础语法(六)多态一、什么是多态1、静态的多态2、动态的多态二、构成多态的条件与实现1、多态的构成条件问题一:什么是虚函数问题二:什么是函数重写2、纯虚函数和抽象类纯虚函数的定义抽象类3... 查看详情
c++多态详解(代码片段)
文章目录1.多态的概念2.c++中多态的分类1.静态多态2.2动态多态3.多态的构成条件3.1两个概念的介绍3.1.1虚函数3.1.2虚函数的重写3.2多态构成条件3.3虚函数重写的两个例外4.final与override5.抽象类1.多态的概念多态,通俗来讲... 查看详情
c++多态的基本概念(代码片段)
动态,多态满足条件:1、有继承关系(父子关系)2、子类要重写父类的虚函数(对应的speak函数),与重载不同3、函数返回值的类型,函数名称参数列表完全相同动态、多态的使用1、父类的指针或者引用,执行子类对象#include&... 查看详情
[c/c++]详解c++的多态(代码片段)
本文详细介绍了C++中的多态,从多态的定义,到多态的原理。目录一、多态的定义及实现1.多态的构成条件2.虚函数3.虚函数的重写4.虚函数重写的例外(1)协变(2) 析构函数的重写5.C++11中override和final(1ÿ... 查看详情
[c/c++]详解c++的多态(代码片段)
本文详细介绍了C++中的多态,从多态的定义,到多态的原理。目录一、多态的定义及实现1.多态的构成条件2.虚函数3.虚函数的重写4.虚函数重写的例外(1)协变(2) 析构函数的重写5.C++11中override和final(1ÿ... 查看详情
第一周java学习总结(代码片段)
1、Java的地位(1)网络地位编写应用程序的主导地位。因其有平台无关性的特点。(2)语言地位面向对象编程基础地位。(3)需求地位在许多软件产品编写中名列前茅。2、Java的特点(1)简单相对C++程序不容易出错,语言更明... 查看详情
c++从青铜到王者第十八篇:c++之多态(代码片段)
系列文章目录文章目录系列文章目录前言一、多态的概念1.多态的概念二、多态的定义及实现1.多态的构成条件2.虚函数的认识3.虚函数的重写1.虚函数重写的两个例外之协变2.虚函数重写的两个例外之析构函数的重写4.C++11ove... 查看详情
c++多态:多态的构成条件finaloverride协变析构函数的重写抽象类(代码片段)
文章目录⏰1.多态的定义和实现🌕多态的浅层理解🌕多态的构成条件⏰2.虚函数📕虚函数的重写规则📕虚函数重写条件的两个例外🍁1.协变(返回值不同)🍁2.析构函数的重写(函数名不同... 查看详情
c++多态(代码片段)
文章目录一.多态的概念二.多态的定义和实现(1).虚函数(2).虚函数的重写(3).多态的构成条件(4).override/final(5).重载/隐藏/重写的区别三.抽象类四.多态的原理(1).虚函数表(2).单继承中的虚函数表(3).多继承中的虚函数表一.多态的概念... 查看详情
c++多态(代码片段)
文章目录一.多态的概念二.多态的定义和实现(1).虚函数(2).虚函数的重写(3).多态的构成条件(4).override/final(5).重载/隐藏/重写的区别三.抽象类四.多态的原理(1).虚函数表(2).单继承中的虚函数表(3).多继承中的虚函数表一.多态的概念... 查看详情
通过最基础的例子讲解c++多态--没有更简单的了(代码片段)
文章目录多态1.多态的基本概念2.多态的原理分析3.多态案例,计算器设计4.纯虚函数和抽象类5.多态案例设计2,制作各种饮品6.虚析构和纯虚析构7.多态案例设计三:电脑的组装多态1.多态的基本概念函数重载和运算符... 查看详情