C++ 多态 | 虚函数、抽象类、虚函数表

文章目录

  • 多态
  • 虚函数
    • 重写
    • 重定义(参数不同)
    • 协变(返回值不同)
    • 析构函数重写(函数名不同)
    • final和override
  • 重载、重写、重定义
  • 抽象类
  • 多态的原理
    • 虚函数
    • 常见问题解析
    • 虚函数表


多态

一种事物,多种形态。换言之,对于同一个行为,不同的对象去完成就会产生不同的结果。

多态的构成条件

多态是继承体系中的一个行为,如果要在继承体系中构成多态,需要满足两个条件:

  1. 必须通过基类的指针or引用调用虚函数

  2. 被调用的函数必须是虚函数,并且派生类必须要对继承的基类的虚函数进行重写


虚函数

虚函数就是被 virtual 修饰的类成员函数 (这里的 virtual 和虚继承的 virtual 虽然是同一个关键字,但是作用不一样)。

  • 任何构造函数之外的非静态函数都可以是虚函数。
  • 关键字 virtual 只能出现在类内部的声明语句之前而不能用于类外部的函数定义
  • 如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。

重写

一般情况下,当派生类中有一个和基类完全相同的虚函数(函数名、返回值、参数完全相同),则说明子类的虚函数重写了基类的虚函数。

class Human
{
public:virtual void print(){cout << "i am a human" << endl;}
};class Student : public Human
{
public:virtual void print(){cout << "i am a student" << endl;}
};void ShowIdentity(Human &human) // 形参是基类引用,构成多态
{human.print(); // 被调用的函数是虚函数
}int main()
{Human h;Student s;ShowIdentity(h); ShowIdentity(s);
}

在这里插入图片描述
通常如果不满足函数名、返回值、参数完全相同,则不构成重写,即无法实现多态。但也有例外:


重定义(参数不同)

参数不同则会变成重定义

class Base{
public:virtual void Show(int n = 10)const{ // 提供缺省参数值std::cout << "Base:" << n << std::endl;}
};class Base1 : public Base{
public:virtual void Show(int n = 20)const{ // 重新定义继承而来的缺省参数值std::cout << "Base1:" << n << std::endl;}
};int main(){Base* p1 = new Base1;        p1->Show();           return 0;
}

在这里插入图片描述

如果子类重写了缺省值,此时的子类的缺省值是无效的,使用的还是父类的缺省值。

因为虚函数是动态绑定,而缺省值是静态绑定。

  • 对于 p1,他的静态类型即指针的类型——Base,所以这里的缺省值是 Base 的缺省值。
  • 而动态类型也就是指向的对象是 Base1,所以这里调用的虚函数则是 Base1 中的虚函数。
  • 调用了 Base1 中的虚函数,Base 中的缺省值,因此输出 Base1:10

或者可以更简单的一句话描述,虚函数的重写只重写函数实现,不重写缺省值。

动态绑定和静态绑定

  • 对象的静态类型:对象在声明时采用的类型。是在编译期确定的。(比如上面的 p1Base 是静态类型,指向的对象的类型 Base1 是动态类型)
  • 对象的动态类型:目前所指对象的类型。是在运行期决定的。

对象的动态类型可以更改,但是静态类型无法更改。

  • 静态绑定:绑定的是对象的静态类型,发生在编译期。
  • 动态绑定:绑定的是对象的动态类型,发生在运行期。

协变(返回值不同)

当基类和派生类的返回值类型不同时,如果基类对象返回基类的 引用or指针,派生类对象返回的是派生类的 引用or指针能实现多态。这样实现多态的方式叫协变

class Human
{
public:virtual Human& print(){cout << "i am a human" << endl;return *this;}
};class Student : public Human
{
public:virtual Student& print(){cout << "i am a student" << endl;return *this;}
};

在这里插入图片描述
但如果返回类型不是对应类的 指针or引用,则不足以构成协变:
在这里插入图片描述


析构函数重写(函数名不同)

在特定条件下,函数名不同也能实现多态,最好的例子是析构函数,编译器为了让析构函数实现多态,会将它的名字处理成destructor ,也就是说,实际上析构函数的函数名也是“相同的”,其多态实现遵循重写的规定。

class Human
{
public:~Human(){cout << "~Human()" << endl;}
};class Student : public Human
{
public:~Student(){cout << "~Student()" << endl;}
};

在这里插入图片描述
可以看到,如果不构成多态,那么指针是什么类型的,就会调用什么类型的析构函数。那么,如果派生类的析构函数中有资源释放的操作,而这里却没有释放掉那些资源,就会导致内存泄漏的问题。

所以为了防止这种情况,必须要将析构函数定义为虚函数。这也就是编译器将析构函数重命名为 destructor 的原因:
在这里插入图片描述


final和override

finaloverrideC++11 中提供给用户用来检测是否进行重写的两个关键字。

final

使用 final 修饰的基类虚函数不能被重写。

如果虚函数不想被派生类重写,就可以用 final 来修饰这个虚函数:
在这里插入图片描述

override

override 关键字是用来检测派生类虚函数是否构成重写的关键字。C++11 允许派生类显式地注明它覆盖了继承基类的虚函数。

在我们写代码的时候难免会出现些小错误,如 基类虚函数没有 virtual 或者 派生类虚函数名拼错 等问题,这些问题不会被编译器检查出来,发生错误时也很难一下子锁定,所以 C++ 增添了 override 这一层保险,当修饰的虚函数不构成重写时就会编译错误。

具体做法是在:

  1. 形参列表后面
  2. 或者 const 成员函数的 const 关键字后面
  3. 或者 引用成员函数的引用限定符后面

加一个关键字 override

下例中,基类虚函数没有 virtual 因此会报错:
在这里插入图片描述


重载、重写、重定义

重载:

  1. 在同一作用域
  2. 函数名相同,参数的类型、顺序、数量不同。
  3. 重载不一定要求返回值相同:参数相同、返回值不同不构成重载;参数、返回值都不同则构成重载。(会发现返回值不同是否构成重载还是看参数相同与否……)

重写(覆盖):

  1. 作用域不同,一个在基类一个在派生类。
  2. 函数名,参数,返回值必须相同(协变和析构函数除外)
  3. 基类和派生类必须都是虚函数(派生类可以不加 virtual,基类的虚函数属性可以继承,但是最好要加上 virtual

考虑这样一个问题,下面有几个虚函数:
在这里插入图片描述

正确答案是 3 个,A 中的 fun1,B 中的 fun1fun2。原因就如第三点所说,基类的虚函数属性可以继承 ,但是如果有 C类 继承了 B类 ,且也有一个 没有virtual关键字的 void fun1(); 函数 ,该函数并不是虚函数,因为 B类fun1 并没有显式声明 virtual 属性。

而形如 fun2 这样的,子类是虚函数而父类没有 virtual 属性的,父类的 fun2 不是虚函数,虚函数不具备对称性。

重定义(隐藏):

  1. 作用域不同,一个在基类一个在派生类
  2. 函数名相同
  3. 派生类和基类同名函数如果不构成重写那就是重定义

重定义无法覆盖虚函数,只能覆盖普通函数,但是父类被覆盖的普通函数可以通过作用域运算符调用:

class A
{
public:virtual void f2(){cout << "A.f2()" << endl;}
};
class B :public A {
public:void f2(int){cout << "B.f2(int)" << endl;}
};
class C:public B{
public:// C类中的两个f2函数互相构成重载,但又分别构成重定义和重写void f2() { // 重写了A类中的虚函数f2()cout << "C.f2()" << endl;}void f2(int) { // 重定义了B类中的普通函数f2(int)cout << "C.f2(int)" << endl;}
};

请添加图片描述


抽象类

如果在虚函数的后面加上 =0,并且不进行实现,这样的虚函数就叫做纯虚函数。而包含纯虚函数的类,也叫做抽象(基)类或者接口类。抽象类不能实例化出对象,因为他所具有的信息不足以描述一个对象,派生类继承后也只有在重写纯虚函数后才能实例化出对象。

我们也可以对纯虚函数提供定义,不过函数体必须在类的外部。

抽象类就像是一个蓝图,为派生类描述好一个大概的架构,派生类必须实现完这些架构,至于要在这些架构上面做些什么,增加什么,就属于派生类自己的问题。

class Human
{
public:virtual void print() = 0;
};class Student : public Human
{
public:virtual void print(){cout << "i am a student" << endl;}
};class Teacher : public Human
{
public:virtual void print(){cout << "i am a teacher" << endl;}
};void ShowIdentity(Human& human)
{human.print();
}

在这里插入图片描述

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

多态的原理

虚函数

class Human
{
public:virtual void print(){cout << "i am a human" << endl;}virtual void test1(){cout << "1test1" << endl;}void test2(){cout << "1test1" << endl;}int _age;
};class Student : public Human
{
public:virtual void print() {cout << "i am a student" << endl;}void test2(){cout << "2test2" << endl;}int _stuNum;
};

在这里插入图片描述

我们创建一个 Human 类对象 h,观察它的大小,按理来说应该输出 4,因为它只有一个 int类型 的数据成员,但是结果却是 8

在这里插入图片描述
可以看到奇怪的是除了 _age 之外,还有个 void**(void*类型的指针,注意不是数组) 类型的 _vfptr ,这个 _vfptr 也被称为虚函数表指针,其指向了一个函数指针数组,这个函数指针数组也就是虚函数表,其中的每一个成员指针指向的都是虚函数,而不是虚函数的 test2 则没有被放入表中。

此时再创建一个 Student 类的对象 s,进一步观察:

在这里插入图片描述
我们可以看到,如果派生类实现了某个虚函数的重写,那么在派生类的虚函数表中,重写的虚函数就会覆盖掉原有的函数,如Student::print。而没有完成重写的 test1 则依旧保留着从基类继承下来的虚函数 Human::test1

总结

  • 派生类会继承基类的虚函数表,如果派生类完成了重写,则会将重写的虚函数覆盖掉原有的函数。
  • 指针或引用指向哪一个对象,就调用对象中虚函数表中对应位置的虚函数,来实现多态。

继续分析构成多态的另一个条件,为什么必须要指针或者引用才能构成多态?
在这里插入图片描述

  • 如果将派生类对象赋值给基类对象,会因为对象切割,导致他的内存布局整个被修改,完全转换为基类对象的类型,虚函数表也与基类相同,所以不能实现多态。
  • 如果用基类指针或者引用指向派生类对象,他们的内存布局是兼容的,不会像赋值一样改变派生类对象的内存结构,所以派生类对象的虚函数表得到了保留,所以他可以通过访问派生类对象的虚函数表来实现多态。

总结一下派生类虚函数表的生成过程:

  1. 首先派生类会将基类的虚函数表拷贝过来
  2. 如果派生类完成了对虚函数的重写,则用重写后的虚函数覆盖掉虚函数表中继承下来的基类虚函数
  3. 如果派生类自己又新增了虚函数,则添加在虚函数表的最后面

常见问题解析

内联函数可以是虚函数吗?

不可以,内联函数没有地址,无法放进虚函数表中。

静态成员函数可以是虚函数吗?

不可以,静态成员函数没有 this指针,无法访问虚函数表。

构造函数可以是虚函数吗?

不可以,虚函数表指针也是对象的成员之一,是在构造函数初始化列表初始化时才生成的

析构函数可以是虚函数吗?

可以,上面有写,最好把基类析构函数声明为虚函数,防止使用基类指针或者引用指向派生类对象时,派生类的析构函数没有调用,可能导致内存泄漏。

对象访问虚函数快还是普通函数快?

  • 如果不构成多态,虚函数和普通函数的访问是一样快的,都是直接在编译时符号表中找到函数的地址后调用。
  • 如果构成多态,调用虚函数就得在运行期虚函数表中查找,就会导致速度变慢,所以普通函数更快一些。

虚函数表

从上面的观察可以看出来,虚函数存于虚函数表中,那么虚函数表又存储在哪里呢?

虚函数表在编译阶段生成,存储于代码段。

详情可以看这篇博客。

注意:

  • 同一个类的不同实例(对象)共用同一份虚函数表。
  • 子类 特有的虚函数 会加在父类 虚函数表 中的 父类虚函数的后面
  • 如果子类继承多个父类、且这些父类都有虚函数表,子类特有的虚函数 加在 第一个父类的虚函数表 中。
  • 如果子类继承多个父类、但只有部分父类有虚函数表,子类特有的虚函数 加在 第一个有虚函数表的父类虚函数表 中。
  • 如果子类继承多个父类、且这些父类都没有虚函数表,子类会自己创建一个虚函数表来存储特有的虚函数。
    在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/443737.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++ 运算符重载(一) | 输入/输出,相等/不等,复合赋值,下标,自增/自减,成员访问运算符

文章目录输出运算符<<输入运算符>>相等/不等运算符复合赋值运算符下标运算符自增/自减运算符成员访问运算符输出运算符<< 通常情况下&#xff0c;输出运算符的第一个形参是一个 非常量ostream对象的引用 。之所以 ostream 是非常量是因为向流写入内容会改变…

C++ 重载函数调用运算符 | 再探lambda,函数对象,可调用对象

文章目录重载函数调用运算符lambdalambda等价于函数对象lambda等价于类标准库函数对象可调用对象与function可调用对象function函数重载与function重载函数调用运算符 函数调用运算符必须是成员函数。 一个类可以定义多个不同版本的调用运算符&#xff0c;互相之间应该在参数数…

C++ 运算符重载(二) | 类型转换运算符,二义性问题

文章目录类型转换运算符概念避免过度使用类型转换函数解决上述问题的方法转换为 bool显式的类型转换运算符类型转换二义性重载函数与类型转换结合导致的二义性重载运算符与类型转换结合导致的二义性类型转换运算符 概念 类型转换运算符&#xff08;conversion operator&#…

分布式理论:CAP、BASE | 分布式存储与一致性哈希

文章目录分布式理论CAP定理BASE理论分布式存储与一致性哈希简单哈希一致性哈希虚拟节点分布式理论 CAP定理 一致性&#xff08;Consistency&#xff09;&#xff1a; 在分布式系统中的所有数据副本&#xff0c;在同一时刻是否一致&#xff08;所有节点访问同一份最新的数据副…

分布式系统概念 | 分布式事务:2PC、3PC、本地消息表

文章目录分布式事务2PC&#xff08;二阶段提交协议&#xff09;执行流程优缺点3PC&#xff08;三阶段提交协议&#xff09;执行流程优缺点本地消息表&#xff08;异步确保&#xff09;分布式事务 分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分…

数据结构算法 | 单调栈

文章目录算法概述题目下一个更大的元素 I思路代码下一个更大元素 II思路代码132 模式思路代码接雨水思路算法概述 当题目出现 「找到最近一个比其大的元素」 的字眼时&#xff0c;自然会想到 「单调栈」 。——三叶姐 单调栈以严格递增or递减的规则将无序的数列进行选择性排序…

最长下降子序列

文章目录题目解法DP暴搜思路代码实现贪心二分思路代码实现题目 给出一组数据 nums&#xff0c;求出其最长下降子序列&#xff08;子序列允许不连续&#xff09;的长度。&#xff08;类似于lc的最长递增子序列&#xff09; 示例&#xff1a; 输入&#xff1a; 6 // 数组元素个…

Linux 服务器程序规范、服务器日志、用户、进程间的关系

文章目录服务器程序规范日志rsyslogd 守护进程syslog函数openlog函数setlogmask函数closelog函数用户进程间的关系进程组会话系统资源限制改变工作目录和根目录服务器程序后台化服务器程序规范 Linux 服务器程序一般以后台进程&#xff08;守护进程[daemon]&#xff09;形式运…

IO模型 :阻塞IO、非阻塞IO、信号驱动IO、异步IO、多路复用IO

文章目录IO模型阻塞IO非阻塞IO信号驱动IO多路复用IO异步IOIO模型 根据各自的特性不同&#xff0c;IO模型被分为阻塞IO、非阻塞IO、信号驱动IO、异步IO、多路复用IO五类。 最主要的两个区别就是阻塞与非阻塞&#xff0c;同步与异步。 阻塞与非阻塞 阻塞与非阻塞最主要的区别就…

Tomcat服务器集群与负载均衡实现

一、前言 在单一的服务器上执行WEB应用程序有一些重大的问题&#xff0c;当网站成功建成并开始接受大量请求时&#xff0c;单一服务器终究无法满足需要处理的负荷量&#xff0c;所以就有点显得有点力不从心了。另外一个常见的问题是会产生单点故障&#xff0c;如果该服务器坏掉…

Linux服务器 | 事件处理模式:Reactor模式、Proactor模式

文章目录Reactor模式Proactor模式同步I/O模型模拟Proactor模式两者的优缺点ReactorProactor同步I/O模型通常用于实现 Reactor 模式&#xff0c;异步I/O模型通常用于实现 Proactor 模式。&#xff08;不是绝对的&#xff0c;同步I/O也可模拟出 Proactor 模式&#xff09; React…

Linux服务器 | 服务器模型与三个模块、两种并发模式:半同步/半异步、领导者/追随者

文章目录两种服务器模型及三个模块C/S模型P2P模型I/O处理单元、逻辑单元、存储单元并发同步与异步半同步/半异步模式变体&#xff1a;半同步/半反应堆模式改进&#xff1a;高效的半同步/半异步模式领导者/追随者模式组件 &#xff1a;句柄集、线程集、事件处理器工作流程两种服…

字符串匹配之KMP(KnuthMorrisPratt)算法(图解)

文章目录最长相等前后缀next数组概念代码实现图解GetNext中的回溯改进代码实现代码复杂度分析最长相等前后缀 给出一个字符串 ababa 前缀集合&#xff1a;{a, ab, aba, abab} 后缀集合&#xff1a;{a, ba, aba, baba} 相等前后缀 即上面用同样颜色标识出来的集合元素&#…

Android入门(一) | Android Studio的配置与使用

文章目录安装配置Android Studio使用Android Studio模拟器更改Android SDK的路径Hello World&#xff01;安装配置Android Studio 从这一步开始&#xff1a; 一直点 next 即可&#xff0c;直到存储路径的选择上&#xff0c;可以放到非 C 盘&#xff0c;这里我放到 D 盘了&am…

Android 入门(四) | Intent 实现 Activity 切换

文章目录Intent显式 Intent定义两个 xml 文件android:orientationmatch_parent 和 wrap_contentIntent函数定义两个 Activity隐式 Intent更多隐式 Intent 的用法用隐式 Intent 打开系统浏览器自建 Activity 以响应打开网页的 Intent向下一个活动传递数据返回数据给上一个活动In…

Android入门(二) | 项目目录及主要文件作用分析

文章目录项目目录分析app目录分析AndroidManifest.xml 分析MainActivity.kt 分析build.gradle 分析最外层目录下的 build.gradleapp 目录下的 build.gradle项目目录分析 我们来看一下 src/main/res 下的一些文件&#xff1a; .gradle 和 .idea &#xff1a;这两个目录下放置…

Android入门(三) | Android 的日志工具 Logcat

文章目录日志工具类 android.util.LogLogcat 中的过滤器日志工具类 android.util.Log Log 从属日志工具类 android.util.Log &#xff0c;该类提供了五个方法供我们打印日志&#xff1a; Log.v() &#xff1a;用于打印那些最为琐碎的、意义最小的日志信息。对应级别 verbose&…

Android入门(五) | Activity 的生命周期

文章目录Activity 的状态及生命周期实现管理生命周期FirstActivitySecondActivityDialogActivity运行结果旧活动被回收了还能返回吗&#xff1f;Activity 的状态及生命周期 Android 的应用程序运用 栈&#xff08;Back Stack&#xff09; 的思想来管理 Activity&#xff1a; …

Android入门(六) | Activity 的启动模式 及 生产环境中关于 Activity 的小技巧

文章目录Activity 的启动模式standardsingleTopsingleTasksingleInstance技巧了解当前界面是哪个 Activity随时随地退出程序启动活动的最佳写法Activity 的启动模式 standard&#xff1a;默认的启动方式&#xff0c;每次启动一个活动都会重新创建singleTop&#xff1a;如果该活…

Android入门(七) | 常用控件

文章目录TextView 控件&#xff1a;文本信息Button 控件&#xff1a;按钮EditText 控件&#xff1a;输入框ImageView 控件&#xff1a;图片ProgressBar 控件&#xff1a;进度条AlertDialog 控件&#xff1a;提示框ProgressDialog 控件&#xff1a;带有进度条的提示框TextView 控…