C++学习:3多态
Posted 想文艺一点的程序员
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++学习:3多态相关的知识,希望对你有一定的参考价值。
目录
前面我们已经大概分析了:封装和继承。
- 封装:将成员变量私有化,然后提供读写的接口供别人进行调用。
- 继承:子类继承父类的成员变量。
先来分析一个 父类指针 和 子类指针
-
首先明确指针的目的是为了指向对象,所以就会有, 父类指针指向子类对象,和 子类指针指向父类对象。
-
父类指针指向子类对象:父类指针可以指向子类对象,是安全的,开发中经常用到(继承方式必须是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++学习:3多态的主要内容,如果未能解决你的问题,请参考以下文章