C++ 多态详解

embedded/2024/10/21 11:36:03/

文章目录

  • 1. 多态的概念
  • 2. 多态的定义及实现
    • 2.1 多态的构成条件
    • 2.2 虚函数
    • 2.3 虚函数的重写
      • 2.3.1 虚函数重写的两个例外
    • 2.4 C++11 override 和 final
    • 2.5 重载、覆盖(重写)、隐藏(重定义)的对比
  • 3. 多态的原理
    • 3.1 虚函数表
    • 3.2多态的原理
  • 4. 单继承和多继承关系的虚函数表
    • 4.1 单继承中的虚函数表
    • 4.2 多继承中的虚函数表

1. 多态的概念

多态是面向对象编程中的一个重要概念,通俗来说,多态就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
举个例子:比如我们在12306买票,对于买票这个行为,成人买成人票就是全价,学生买学生票就是半价。
在这里插入图片描述

2. 多态的定义及实现

2.1 多态的构成条件

在继承中要构成多态还有两个条件

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写
    在这里插入图片描述
    多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如上面图片中的例子:Student继承了Person。Person对象买票全价,Student对象买票半价。

代码如下:

class Person
{
public:virtual void buy_ticket(){cout << "全价" << endl;}
};
class Student : public Person
{public:virtual void buy_ticket(){cout << "半价" << endl;}};
void func(Person& p)
{p.buy_ticket();
}
void func(Person* p)
{p->buy_ticket();
}
int main()
{Person p;Student s;//基类的引用func(p);func(s);//基类的指针func(&p);func(&s);return 0;
}

2.2 虚函数

virtual修饰的类成员函数称为虚函数。

class Person
{
public://buy_ticket()就是虚函数,被virtual修饰的类成员函数virtual void buy_ticket(){cout << "全价" << endl;}
};

2.3 虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
重写虚函数的目的是为了在派生类中提供一个特定于派生类的实现,从而实现特定的行为。重写后,当通过基类指针或引用调用虚函数时,如果指向或引用的是派生类对象,将会调用派生类中的虚函数,如果指向或引用的是基类对象,将会调用基类中的虚函数,这样就可以做到不同的派生类的对象在调用同一个函数时,能表现出不同的行为。
在这里插入图片描述

2.3.1 虚函数重写的两个例外

  1. 协变(基类与派生类虚函数返回值类型不同)
    派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
    在这里插入图片描述
  2. 析构函数的重写(基类与派生类析构函数的名字不同)
    如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
    这里我们可以间接验证一下:
    在这里插入图片描述
    因此在涉及到资源管理的时候,基类的析构函数最好加上virtual关键字修饰,否则可能在某些情况下,造成无法正确调用析构函数而造成内存泄漏。
    比如下面这种情况:
    在这里插入图片描述

2.4 C++11 override 和 final

  1. final:修饰虚函数,表示该虚函数不能再被重写
    在这里插入图片描述
  2. final:修饰类,表示该类不能被继承
    在这里插入图片描述
  3. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
    在这里插入图片描述

2.5 重载、覆盖(重写)、隐藏(重定义)的对比

在这里插入图片描述

3. 多态的原理

3.1 虚函数表

我们先来看一道题:

#include<iostream>
using namespace std;
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};int main()
{cout << sizeof(Base) << endl;return 0;
}

运行结果:(32位平台下是8,64位平台下位16。)
在这里插入图片描述
我们发现,Base类只有一个整型变量,就算考虑内存对齐,结果应该是4呀,这里为啥会输出8呢?
下面我们打开监视窗口来看下Base类对象的模型:
在这里插入图片描述
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

针对上面的代码我们做出以下改造
1.我们增加一个派生类Derive去继承Base
2.Derive中重写Func1
3.Base再增加一个虚函数Func2和一个普通函数Func3

#include<iostream>
using namespace std;class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}
private:int _b = 1;
};
class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};
int main()
{Base b;Derive d;return 0;
}

针对改造后的代码,我们接着往下分析派生类中这个表放了些什么呢?
在这里插入图片描述
通过观察和测试,我们发现了以下几点问题:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚
    表指针也就是存在部分的另一部分是自己的成员。
    在这里插入图片描述
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表
    中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数
    的覆盖
    。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函
    数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
    在这里插入图片描述
  5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生
    类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己
    新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  6. 虚函数存在哪的?虚表存在哪的?
    答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?
    在这里插入图片描述
    上面分析了这个半天了那么多态的原理到底是什么?下面我们来具体分析一下:

3.2多态的原理

现在我们再来看下之前写的买票的代码,Func函数传Person调用的Person::BuyTicket,传Student调用的是Student::BuyTicket:

#include<iostream>
using namespace std;
class Person 
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person 
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{p.BuyTicket();
}
int main()
{Person p;Func(p);Student s;Func(s);return 0;
}

在这里插入图片描述
上面我们分析出,当p指向谁就去谁的虚函数表中去取对应虚函数的地址,因此就实现出了不同对象完成同一行为时,展现出不同的形态。
而我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。这是为什么呢?我们来反思一下:
在这里插入图片描述
我们分析出,赋值不会拷贝虚函数指针,因此要实现出不同对象完成同一行为时,展现出不同的形态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。
下面我们在来分析一下多态调用和普通函数调用有什么区别呢?
在这里插入图片描述
通过上面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译链接时确认好的

4. 单继承和多继承关系的虚函数表

在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类
的虚表模型前面我们已经看过了,没什么需要特别研究的。

4.1 单继承中的虚函数表

分析如下代码:

class Base 
{ 
public :virtual void func1() { cout<<"Base::func1" <<endl;}virtual void func2() {cout<<"Base::func2" <<endl;}
private :int a;
};
class Derive :public Base 
{ 
public :virtual void func1() {cout<<"Derive::func1" <<endl;}virtual void func3() {cout<<"Derive::func3" <<endl;}virtual void func4() {cout<<"Derive::func4" <<endl;}
private :int b;
};int main()
{Base b;Derive d;return 0;
}

分析如下:
在这里插入图片描述
我们通过监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。而且通过内存窗口只能看清虚函数的个数,那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数。

class Base
{
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};
class Derive :public Base
{
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }
private:int b;
};
typedef void (*VFPTR)();
void PrintVFTable(VFPTR* vftptr)
{for (int i = 0; vftptr[i] != 0; ++i){printf("第%d个虚函数的地址:%p------>", i + 1, vftptr[i]);vftptr[i]();}printf("\n");
}
int main()
{Base b;Derive d;
//思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,
// 这个数组最后面放了一个nullptr
// 1.先取b的地址,强转成一个int*的指针
// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
// 4.虚表指针传递给PrintVTable进行打印虚表VFPTR* vftptr = (VFPTR*)(*(int*)&b);PrintVFTable(vftptr);vftptr = (VFPTR*)(*(int*)&d);PrintVFTable(vftptr);return 0;
}

运行结果如下:
在这里插入图片描述
因此我们可以推断出派生类的虚函数表模型:
在这里插入图片描述

4.2 多继承中的虚函数表

分析如下代码:

#include <iostream>
using namespace std;class Base1 
{
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1 = 1;
};
class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2 = 2;
};
class Derive : public Base1, public Base2 
{
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1 = 0;
};
typedef void(*VFPTR) ();
void PrintVFTable(VFPTR* vftptr)
{for (int i = 0; vftptr[i] != 0; ++i){printf("第%d个虚函数的地址:%p------>", i + 1, vftptr[i]);vftptr[i]();}printf("\n");
}
int main()
{Derive d;VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVFTable(vTableb1);VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));PrintVFTable(vTableb2);return 0;
}

通过监视创窗口看下d对象的模型:
在这里插入图片描述
通过监视窗口可以看出,d对象有两个虚函数指针和从两个基类继承下来的成员以及自己的成员,但是从监视窗口无法看出两个虚函数表具体放了哪几个虚函数,因此下面我们打印一下两个虚函数表:
观察下图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中!!!
在这里插入图片描述

从上图中可以看出,两个虚函数表中的func1是同一个函数,但是两张表中的地址不一样这是为啥呢?
在这里插入图片描述
下面从汇编代码看下,这个函数是如何调用的:
在这里插入图片描述
从上表中看出,两个表中func1的地址不同,是因为第二个虚函数表中的func1在调用的时候要修正this指针。

至此,本片文章就结束了,若本篇内容对您有所帮助,请三连点赞,关注,收藏支持下。

创作不易,白嫖不好,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!

如果本篇博客有任何错误,请批评指教,不胜感激 !!!
在这里插入图片描述


http://www.ppmy.cn/embedded/26943.html

相关文章

C++类的设计编程示例

一、银行账户类 【问题描述】 定义银行账户BankAccount类。 私有数据成员&#xff1a;余额balance&#xff08;整型&#xff09;。 公有成员方法&#xff1a; 无参构造方法BankAccount()&#xff1a;将账户余额初始化为0&#xff1b; 带参构造方法BankAccount(int m)&#xff1…

企业私服中使用Maven,标准的setting.xml文件

Maven Maven是一个项目管理和理解工具。它主要服务于以下几个方面: 构建管理:Maven可用于构建和管理任何基于Java平台的项目。 依赖管理:Maven有一个中央仓库,用于保存大量常用的库文件。当进行项目构建时,Maven会自动下载所需的库文件到本地仓库,这极大地简化了库文件…

Vue.js 3 应用开发与核心源码解析 阅读笔记

https://www.dedao.cn/ebook/reader?idV5R16yPmaYOMqGRAv82jkX4KDe175w7xRQ0rbx6pNgznl9VZPLJQyEBodb89mqoO 2022年出的书&#xff0c;针对Vue的版本是3.2.28&#xff0c;当前的版本是 3.4.21。 本书的一大特色是对Vue 3.x的核心源码&#xff08;响应式原理、双向绑定实现、虚…

泛型类,泛型方法,泛型接口,泛型的上界

在Java中&#xff0c;泛型&#xff08;Generics&#xff09;是一种机制&#xff0c;它允许程序员在定义类、接口和方法时使用类型参数。这些类型参数在实例化时会被实际的类型&#xff08;如Integer、String等&#xff09;所替换。泛型提供了编译时的类型检查&#xff0c;增加了…

net lambda 、 匿名函数 以及集合(实现IEnumerable的 如数组 、list等)

匿名函数&#xff1a;》》》 Action a1 delegate(int i) { Console.WriteLine(i); }; Lambda:>>> Aciont a1 (int i) > { Console.WriteLine(i); }; 可以简写 &#xff08;编译器会自动根据委托类型 推断&#xff09; Action a1 &#xff08;i&#xff09;> {…

鸿蒙OpenHarmony【小型系统 烧录】(基于Hi3516开发板)

烧录 针对Hi3516DV300开发板&#xff0c;除了DevEco Device Tool&#xff08;操作方法请参考烧录)&#xff09;外&#xff0c;还可以使用HiTool进行烧录。 前提条件 开发板相关源码已编译完成&#xff0c;已形成烧录文件。客户端&#xff08;操作平台&#xff0c;例如Window…

【opencv4.8.1 源码编译】windows10 OpenCV 4.8.1源码编译并实现 CUDA 12加速

Windows 下使用 CMake3.29.2 Visual Studio 2022 编译 OpenCV 4.8.1 及其扩展模块cuda12.0teslaT4显卡 记录自己在编译时踩过的坑&#xff0c;避免下次再犯或者给有需要的人。 在实际使用中&#xff0c;如果是对处理时间要求比较高的场景&#xff0c;使用OpenCV处理图片数据很…

全面展示自动驾驶最新发展动态“2024上海国际自动驾驶技术展会”

随着科技的飞速发展和人们生活水平的提高&#xff0c;汽车作为交通工具的角色正在逐渐转变&#xff0c;它已经不仅仅是一个简单的移动工具&#xff0c;而是成为了一种智能设备&#xff0c;融入了我们的生活之中。2024年上海自动驾驶及新能源汽车展会&#xff0c;就是这一变革的…