C++学习笔记——多态
前面说到了继承,那么就衍生出了多态。
目录
概念
实现
1、前提
2、虚函数
3、重写
4、示例
4、抽象类
原理
1、虚表指针
2、虚表
多继承和单继承的虚函数表
1、多继承的虚表指针
2、菱形继承
彩蛋
概念
多态的概念用举例子的方式就比较好理解了。
例子①:买票,对于买票这同一件事情,但是却有成人票、学生票、儿童票等。这是多态。
例子②:变形,对于变形金刚变形,大黄蜂和擎天柱等变形后的形态不一样,这也是多态。
例子③:书写,每个人对书写这件事情,有的苍劲有力,有的龙飞凤舞,这也是多态。
总结一下,多态就是对同一个事件或者行为,不同的对象就会产生不同的现象。在c++里,事件或者行为就是函数,不同的现象就是函数执行后的结果。不同的对象对象调用相同的函数会有不同结果。就是多态了。说到这儿可以联想到前面的类型萃取。
实现
1、前提
构成多态需要有两个条件。
- 函数需要是虚函数
- 调用的对象需要是指针或者引用
2、虚函数
虚就是虚拟的意思,如同前面的虚拟继承一样。所以加的关键字也是一样的——virtual。但是两者的意义是不一样的。
3、重写
说到重写,又叫覆盖。由于前面的隐藏,基类的成员函数还存在,这里用到覆盖了,当定义了和父类同名的函数时,我们将其重写,这个时候,基类的函数和派生类的函数就是一个函数了。想到这,说一下前面的重载,重定义(隐藏)了,毕竟又有一个概念产生。所以这里要对比记忆一下,防止混淆。
- 重载:相同的作用域,函数名相同,但参数列表不同
- 重定义:在不同的作用域(继承),函数名相同,就构成了隐藏,即重定义
- 重写:基类的函数是虚函数的时候,派生类重新定义。
注意:
- 当基类定义成虚函数是,派生类继承过来也是虚函数,即使派生类不写virtual,编译器也会自动加上,它们现在共享这个函数。
- 协变:返回值的类型构成符字关系,并且是指针或者引用(了解)。
4、示例
//头文件Polymorphism.h
#pragma once
#include<iostream>
using namespace std;class A
{
public:A(int a = int()):a_(a){cout << "A()" << endl;}void func(){cout << "A的成员函数func" << endl;}virtual void func1(){cout << "A的虚函数" << endl;}
private:int a_;
};
class B :public A
{
public:B(int b = int()):b_(b){cout << "B()" << endl;}void func(){cout << "B的成员函数func" << endl;}virtual void func1(){cout << "B的虚函数" << endl;}
private:int b_;
};
void Poly(A& a)
{a.func1();
}
注意:一定要是引用或者指针,在上面的代码中,poly就是引用传参。不能传值,
4、抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
class Person
{
public:Person(const string name = "张三"):name_(name){}virtual void Print() = 0;
private:string name_;
};
class Student : public Person
{
public:Student(const int& stunum = 0):Person(),stunum_(stunum){}
private:int stunum_;
};
这个时候子类重写虚函数
class Student : public Person
{
public:Student(const int& stunum = 0):Person(),stunum_(stunum){}virtual void Print(){cout << "Student::Print()" << endl;}
private:int stunum_;
};
父类依旧不能创建对象,但是子类却可以。
原理
1、虚表指针
首先来看一个类,我们知道空类的大小,我们是给了一个字节类标识这个类的。那么下面的类是多大呢?
class Car
{
public:
//为了方便测试,先注释掉一些东西。Car(/*const string& band = "五菱宏光"*/)//:band_(band){}virtual void Drive(){cout << "秋名山车王" << endl;}
//private:
// string band_;
};
//该类是几个字节的大小?
//测试一下就知道了。
void test4()
{cout << "Car类的大小是:" << sizeof(Car) << "个字节" << endl;
}
int main()
{test4();system("pause");return 0;
}
下面来看看具体是怎么回事。
我创建了一个car对象,发现对象模型当中并不是空的,而是有一个_vfptr。这就是这节的主人公——虚表指针(v代表virtual,f代表 function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中, 虚函数表也简称虚表,虚表指针就是指向虚函数表的。下面来看一下在继承中,虚标指针的作用。
class Car
{
public:Car(const string& band = "五菱宏光"):band_(band){}virtual void Drive(){cout << "秋名山车王" << endl;}
private:string band_;
};class BWM : public Car
{
public:BWM(const string& band = "宝马"):Car(band){}virtual void Drive(){cout << "驾驶之王" << endl;}
};
发现派生类的_vfptr是来自基类的_vfptr。但是两个地址是不一样的。其实这里是发生了拷贝,派生类把基类的虚表指针和虚表都拷贝了一份,当派生类把继承来的虚函数重写了之后,就用重写后的虚函数的地址把原先的虚函数的地址覆盖掉。所以重写又叫覆盖。下面来说一下几个概念。
虚函数表:存放的是虚函数地址,它是一个指针数组
虚表指针:指向虚函数表的指针。只要有虚函数,就会有虚表指针。
2、虚表
上面说虚函数表是一个指针数组存放的虚函数的地址,那么如果派生类有自己的虚函数,或者继承的普通函数怎么存储的呢?
class Car
{
public:Car(const string& band = "五菱宏光"):band_(band){}virtual void Drive(){cout << "秋名山车王" << endl;}virtual void Func(){cout << "我是不想重写的虚函数" << endl;}void Func1(){cout << "我是基类的普通函数" << endl;}
private:string band_;
};class BWM : public Car
{
public:BWM(const string& band = "宝马"):Car(band){}virtual void Drive(){cout << "驾驶之王" << endl;}virtual void Print(){cout << "大家好,我是宝马" << endl;}
};
虚表生成:
- 先将基类中的虚表内容拷贝一份到派生类虚表中
- 如果派生类重写了基 类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
- 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后,这里没有显示而已,是编译器的监视窗口故意隐藏了这两个函数,后面用代码的形式展示。
虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。我们发现基类的普通函数是没有放进虚表的,
所以不是虚函数,所以不会放进虚表。最后再强调一下:
虚表存的是虚函数指 针,不是虚函数,虚函数和普通函数一样的,存在代码段的,只是他的指针又存到了虚表中,对象中存的不是虚表,存的是虚表指针
多继承和单继承的虚函数表
1、多继承的虚表指针
上面说,只要有虚函数,那么就至少有一个虚表指针,所以如果该类继承了多个类,就会有多个虚表指针。
class Base1
{
public:Base1(const int& b):b1(b){}virtual void Func1(){cout << "我是Base1的虚函数" << endl;}
private:int b1;
};
class Base2
{
public:Base2(const int& b) :b2(b) {}virtual void Func2(){cout << "我是Base2的虚函数" << endl;}
private:int b2;
};class Child : public Base1, public Base2
{
public:Child():Base1(1), Base2(2),c_(3){}virtual void Func2(){cout << "我是重写Base2的虚函数" << endl;}virtual void Func3(){cout << "我是派生类的虚函数" << endl;}
private:int c_;
};
可以看到上面派生类是有两个虚表指针的。在继承中,我们知道派生类的对象的对象模型是基类的成员在前面,自己的成员在后面,如果有多个继承的基类,会按照继承的顺序来存放。
如果是派生类自己虚函数应该刚在哪呢?同构一段测试代码来看一下
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){cout << " 第" << i << "个虚函数地址 :" << vTable[i];VFPTR f = vTable[i];f();}cout << endl;
}
void test5()
{Base1 b1(1);Base2 b2(2);Child c;// 思路:取出c对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的//指针数组,这个数组最后面放了一个nullptr// 1.先取b的地址,强转成一个int*的指针// 2.再解引用取值,就取到了c对象头4bytes的值,这个值就是指向虚表的指针// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。// 4.虚表指针传递给PrintVTable进行打印虚表VFPTR* vTableb = (VFPTR*)(*(int*)&c);PrintVTable(vTableb);VFPTR* vTabled = (VFPTR*)(*(int*)((char*)&c+ sizeof(b1)));PrintVTable(vTabled);}
通过上面的测试可以发现,我们上面画的对象模型是没有错的。而且验证了派生类自己虚函数是放在第一个虚表指针指针的虚表的。
2、菱形继承
下面我们再来说一下菱形继承。
前面说菱形继承是一个大坑,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗,现在看到虚函数表又是这么复杂。
前面说这里虚函数的virtual和虚拟继承的virtual是有区别的,区别是啥?
虚函数virtual是把基类的虚表,虚表指针都拷贝了一份,重写了就覆盖掉复制的虚表里面的函数地址。
而虚拟继承,是把有二义性的数据弄成共享的,可以通过VS的监视发现数据的变化。
现在就变成了
彩蛋
彩蛋其实就是三个问题:
1、静态成员可以是虚函数吗?
2、构造函数可以是虚函数吗?
3、析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答案请见下一篇博文——智能指针
C++学习笔记——多态相关推荐
- java多态怎么学_Java学习笔记---多态
在面向对象的程序设计中,多态是继数据抽象和继承之后的第三种基本特性: 多态通过分离做什么(基类对象)和怎么做(导出类对象),从另一角度将接口和实现分离开来.多态不但能够改善代码的组织结构和可读性,还能 ...
- C#学习笔记:多态与隐藏,覆盖
以继承为基础,继承举例: public class Person { public void Sayhello() { Console.WriteLine("Hello,I am a ...
- Java学习笔记-多态的具体体现
面向对象编程有四个特征:抽象.封装.继承.多态. 多态有四种体现形式:1.接口和接口的继承2.类和类的继承3.重载4.重写其中重载和重写是核心.# 重载:重载发生在同一类中,在该类中如果存在多个同名方 ...
- 【b站黑马程序员C++视频学习笔记-多态案例三-电脑组装】
多态案例三-电脑组装 电脑主要组成部件为CPU(用于计算),显卡(用于显示),内存条(用于存储).把每个零件封装出抽象父类,并且提供不同的厂商生产不同的零件,例如Intel厂商和Lenovo厂商.创建 ...
- 【b站黑马程序员C++视频学习笔记-多态案例二-制作饮品】
多态案例二-制作饮品 利用多态实现制作咖啡和茶水 Coffee和Tea继承了抽象类AbstractDrinking,并重写了AbstractDrinking的抽象函数 #include<iost ...
- SV学习笔记—多态与类型转换
0.前言 当同一操作作用于不同对象,能有不同的解释从而产生不同的结果,这就叫做多态,多态在验证中被大量使用 多态的实现基础是什么? 1.多态的实现基础是继承,没有继承就没有多态 2.多态通过子类覆盖父 ...
- 学习笔记 | 多态案例2-制作饮品
多态案例二-制作饮品 案例描述:制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料 利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶. #include <io ...
- Java学习笔记——多态(实例详解)
多态是某一事物,在不同时刻体现出来的不同的状态 就比如猫,狗,狼都是动物,我们可以说猫是动物,说狗是动物 但我们不能说动物是猫,动物是狼.也不能说猫是狗,或者狗是狼. 上面这几点都会在多态中体现出来 ...
- golang学习笔记(十六):多态的实现
golang 学习笔记 多态实现 package main import "fmt"//先定义接口 在根据接口实现功能 type Humaner1 interface {//方法 ...
最新文章
- React 组件学习
- Linux 服务器性能参数指标总结
- SAP里面的ATP的定义
- LeetCode 695. Max Area of Island javascript解决方案
- [转]AIX平台下如何增加用户和组的名称长度
- [JAVA]字符串单词倒转处理前面的空格
- GDAL查看DEM高程数据(java)
- FAQ系列 | 用MySQL实现发号器
- vue-cli3的命令行创建项目-(慕课网笔记)
- 乐高mindstormsev3_乐高MINDSTORMSEV3软件程序模块开发-2019年精选文档
- 简单工具类HttpUtils
- java语言介绍及特点分析(萌新入门须知内容)
- 安装imageai,tensorflow
- 国培 计算机远程培训心得,国培远程培训感言3篇
- 移动端开发vw+rem布局,即等比缩放布局(什么是vw?如何设置根元素html的字体大小?如何换算vw单位?文末:移动端开发步骤详解链接)
- Punti特征码定位器(原SignatureTest) 2022 Q1V1
- Kotlin代码转换成Java代码
- 人体感应模块stm32驱动
- C语言《位段结构体、联合体》
- 【软件工程实践】Hive研究-Blog6