C++多态(函数重写、override 和 final、虚函数表、抽象类)

server/2024/12/18 11:30:28/

C++多态(函数重写、override 和 final、虚函数表、抽象类)

1. 多态的介绍

多态是 C++ 三大特性之一,多态的作用是让不同类型的对象(需要具有继承关系)调用同一全局函数具有不同的效果。

2. 重写

2.1 一般重写

重写(又称覆盖)是多态中的一个重要概念,重写是实现多态的条件之一。

重写指两个函数在基类和派生类中,同时满足函数名相同、参数类型相同、返回值相同,且函数必须是虚函数。这样的两个函数构成重写。

但要注意基类中的虚函数必须要使用 virtual 关键字,但派生类的虚函数不需要有 virtual 也能构成虚函数,因为虚函数重写的是函数体的实现,而函数的结构(函数名、返回值、参数等)用的是基类的。 但在实际使用中建议派生类的虚函数也使用 virtual 关键字以增加代码的可读性。

class A
{
public:virtual int func(int x, int y){return x + y;}
};class B:public A
{
public:virtual int func(int x, int y){return x - y;}
};

func() 在 A 类和 B 类中构成函数重写。

2.2 协变重写

协变是重写的一种特殊情况,它是指在满足重写的其他条件下,重写的两个函数的返回值不同。协变重写的返回值必须是类对象的指针或引用。 即基类虚函数返回基类基类对象的指针或引用,派生类返回派生类对象的指针或引用。

class A
{
public:virtual A* func(){return new A(*this);}
};class B:public A
{
public:virtual B* func(){return new B(*this);}
};

协变重写的意义在于,有些函数接口就是需要返回一个与类类型相同的对象的指针或引用,但多态的特点在类外部可能是通过父类的指针访问的成员函数,只有让返回值不同才能避免这种 BUG。

3.3 析构函数的重写

如果基类的析构函数为虚函数,此时派生类的析构函数只要定义,无论是否加关键字 virtual,都与基类的析构函数构成函数重写。虽然基类和派生类的析构函数名字不同,但编译器对析构函数的名字做了特殊处理,程序编译后析构函数会被统一改名成 destructor ,这样析构函数的重写便满足了重写的规则。

析构函数的重写在多态的使用中是很要必要的,如果派生类向内存申请了空间,在析构的时候如果不使用重写很容易造成内存泄漏。这是因为通过基类的指针或引用删除一个对象时,如果基类的析构函数没有声明为虚函数,编译器只会调用基类的析构函数,而不会检查对象的实际情况。

编译器不会调用派生类的析构函数,是因为虚函数指针的存在。在外部,控制派生类的是基类的指针,使用基类的指针调用的析构肯定是基类的析构。

所以在使用多态的程序中建议析构函数定义为虚函数,防止发生内存泄漏。

对比两段代码的运行结构:

Polymorphic1

Polymorphic2

3. 多态的使用方法

多态要在继承的前提下使用。

条件:

  1. 基类和派生类要完成虚函数的重写。
  2. 父类的指针或者引用去调用虚函数。

如有一个 person 类和一个 student 类,student 类是 person 类的派生类。这两种对象在调用买火车票代码时,person 类对象是全价票价,而 student 类是半价票价。实现这种情况的方法就是多态。

#include  <iostream>
using namespace std;class person
{
public:virtual void BuyTicek(){cout << "普通人-全价票" << endl;}
};class student :public person
{
public:virtual void BuyTicek(){cout << "学生-半价票" << endl;}
};void func(person* p)
{p->BuyTicek();
}int main()
{person p;student s;func(&p);func(&s);
}

Polymorphic3

4. override 和 final

4.1 override

override 和 final 是 C++11 提供的功能。override 能检查派生类中的虚函数是否和基类的虚函数构成重写,是一种检查代码是否编写正确的手段:

#include  <iostream>
using namespace std;class A
{
public:void func(){}};class B :public A
{
public:virtual void func() override{}
};int main()
{A a;B b;return 0;
}

Polymorphic4

Polymorphic5

4.2 final

final 可以使一个基类的虚函数不能被重写:

#include  <iostream>
using namespace std;class A 
{
public:virtual void func() final{}
};
class B :public A
{
public:virtual void func(){}
};int main()
{A a;B b;return 0;
}

Polymorphic6

final 也可以使一个类不能被继承。在final之前 C++98 提供一种使类不能被继承的方法:

把类的构造函数写在私有作用域,导致派生类无法访问基类的构造函数,无法实例化。但缺点是代码只有在实例化的时候才会报错。

而final关键字使程序在编译的时候就能够产生报错并提供准确的错误信息。

class A final
{
public:virtual void func(){}};class B :public A
{
public:virtual void func(){}
};
int main()
{A a;B b;	return 0;
}

Polymorphic7

5. 多态的原理(虚函数表)

一个类中如果有虚函数,那么这个类实例化时,内部除了成员变量,还会有一个虚函数表指针,简称虚表指针。 虚表指针是一个函数指针数组,它指向这个类中所有的虚函数。同类型的对象共用一张虚表(指向的地址相同),不同类型的对象虚表不同

这里的同类型是狭义上的相同,认为父类和子类是不同的类型。

Polymorphic8

在 32 位下,一个函数数组指针指向第一个元素的地址,占 4 个字节,int 类型占 4 个字节,char 类型占一个字节,共 9 个字节。又因为对齐值为 4,最终大小为 12 。

Polymorphic9

同类型的对象共用一张虚表。

因为派生类可以看作是特殊的基类,所以在使用多态的时候,派生类可以用基类的指针或引用来调用虚函数。在调用多态时,调用生成的指令就会去对应的虚表中找对应的虚函数来调用。

上面已经提到,相同的类型使用同一张虚表,不同类型使用不同的虚表。派生类虽然可以看成特殊的基类,但本质与基类不同,所以基类和派生类的虚表不同。运行时,调用的指令指向基类就调用基类的虚表,指向派生类就调用派生类的虚表

#include  <iostream>
using namespace std;class A
{
public:virtual void func1(){}virtual void func2(){}private:int i;char ch;
};
class B :public A
{
public:virtual void func1(){}virtual void func2(){}private:int i;char ch;
};int main()
{A a;B b;return 0;
}

image-20240418204347531

基类和派生类的虚表不同,因为它们本质不是相同的类。

6. 重载、重定义(隐藏)和重写(覆盖)的区别

  1. 函数重载,要求两个函数在同一作用域函数名相同,返回值相同,参数不同

  2. 重定义(隐藏),要求两个函数分别在基类和派生类的作用域,函数名相同

  3. 重写(覆盖),要求两个函数分别在基类和派生类的作用域函数名、参数、返回值相同(协变重写除外),且两个函数必须是虚函数。

在基类和派生类中,如果有同名函数,如果它不构成重定义(隐藏)就一定构成重写(覆盖)。

7. 抽象类

7.1 抽象类的定义

包含纯虚函数的类叫做抽象类。纯虚函数是一个只有声明没有定义的虚函数,它的声明方法为:

Polymorphic11

抽象类不能实例化出对象,它用来存放纯虚函数,而纯虚函数的意义是间接强制派生类重写虚函数。抽象类为基类时,它的派生类如果不重写纯虚函数的定义,那么基类的纯虚函数就被继承下来,导致派生类也无法实例化出对象。

#include  <iostream>
class A
{virtual void func() = 0;
};

Polymorphic12
Polymorphic13

7.2接口继承和实现继承

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

8. 多态与虚函数

  1. 虚函数和普通函数一样,都是存在代码段。而类中指向虚函数的虚表存在常量区(vs下),在构造函数中完成初始化。

  2. 子类有虚函数,继承的父类也有虚函数,那么子类的虚表就在父类中,子类对象就不需要单独建立虚表。

  3. 多继承情况下,派生类的虚表在第一个父类的虚表中。


http://www.ppmy.cn/server/151156.html

相关文章

华为OD E卷(100分)25-整数对最小和

前言 工作了十几年&#xff0c;从普通的研发工程师一路成长为研发经理、研发总监。临近40岁&#xff0c;本想辞职后换一个相对稳定的工作环境一直干到老, 没想到离职后三个多月了还没找到工作&#xff0c;愁肠百结。为了让自己有点事情做&#xff0c;也算提高一下自己的编程能力…

专访李飞飞:从2D到3D,AI将为我们带来哪些改变?

全文2,600 字&#xff0c;阅读约需6分钟 斯坦福大学教授李飞飞接受了 IEEE Spectrum 的独家采访。这位人工智能领域的传奇人物&#xff0c;因创建 ImageNet 数据集和竞赛而闻名于世。通过这一开创性工作&#xff0c;她为深度学习的蓬勃发展奠定了坚实基础。 ImageNet 竞赛要求…

Cookie,Seesion和Token区别及用途

Cookie&#xff0c;Seesion和Token区别及用途 简介 Cookie、Session、Token 和 JWT&#xff08;JSON Web Token&#xff09;都是用于在网络应用中进行身份验证和状态管理的机制。虽然它们有一些相似之处&#xff0c;但在实际应用中有着不同的作用和特点。 Cookie 定义&#…

爬虫运行中遇到反爬虫策略怎么办

在现代网络环境中&#xff0c;爬虫技术与反爬虫策略之间的博弈愈发激烈。为了应对网站的反爬虫措施&#xff0c;爬虫开发者需要采取一系列策略来确保数据抓取的成功率。本文将详细介绍几种常见的反爬虫策略及其应对方法&#xff0c;并提供相应的Java代码示例。 1. 用户代理&am…

(笔记)lib:no such lib的另一种错误可能:/etc/ld.so.conf没增加

[TOC]((笔记)lib:no such lib的另一种错误可能&#xff1a;/etc/ld.so.conf没增加) 0.需求说明 通过cmakelist去find一个库时&#xff0c;可能导致报错&#xff0c;例如”libsgm.so cannot open“。但明明已经make install了&#xff0c;所以还有一种可能&#xff1a; 共享库…

如何通过变更让 PostgreSQL 翻车

在开发应用程序和维护其后台数据库集群的过程中&#xff0c;我们经常会遇到实践与理论、开发环境与生产环境之间的差异。其中一个典型的例子就是变更数据库中的列类型。 对于在 PostgreSQL&#xff08;及其他符合 SQL 标准的系统&#xff09;中变更列类型的常规操作&#xff0…

leetcode--字符串

目录 344.反转字符串 541.反转字符串II 卡码网&#xff1a;替换数字 151.反转字符串中的单词 卡码网&#xff1a;右旋字符串 28.找出字符串中第一个匹配项的下标 459.重复的子字符串 344.反转字符串 编写一个函数&#xff0c;其作用是将输入的字符串反转过来。输入字符串以…

P8772 [蓝桥杯 2022 省 A] 求和

题目描述&#xff1a; 解题思路&#xff1a; 首先这题我们可以直接用两个for循环嵌套来控制两个变量来求值&#xff0c;但是这样做时间复杂度高。这里我们用到了一个前缀和差的方法。通过for循环变量第一个变量&#xff0c;用和差的方法的到第二个量&#xff0c;这样就只用了一…