C++多态原理揭秘

news2024/12/28 5:54:14

🎈个人主页:🎈 :✨✨✨初阶牛✨✨✨
🐻强烈推荐优质专栏: 🍔🍟🌯C++的世界(持续更新中)
🐻推荐专栏1: 🍔🍟🌯C语言初阶
🐻推荐专栏2: 🍔🍟🌯C语言进阶
🔑个人信条: 🌵知行合一
🍉本篇简介:>:讲解C++中多态的底层原理.
金句分享:
✨我将玫瑰藏于身后,风起花落.✨

目录

  • 一、你分的清"重写","重载"和"重定义"吗?
    • 1.重写:
    • 2.重载:
    • 3.重定义
  • 二、抽象类
    • 🍭纯虚函数
        • 接口继承与实现继承
    • 🍉抽象类示例:
  • 三、解密多态原理
      • 基类中的虚表
      • 派生类的虚表
  • 四、多继承中的虚表

一、你分的清"重写","重载"和"重定义"吗?

1.重写:

(上一篇以及详细介绍了)

条件:
(1)分别在两个不同的作用域,基类和派生类.
(2)三同(函数名,返回值,函数参数列表)(斜变析构函数除外)
(3)是virtual修饰的虚函数.

实现效果:
不同对象使用同一个函数名,可以实现不同的行为,也就是多态.

示例:

class Person									//基类
{
public:
	virtual void test(int a,int b)
	{
		cout << a + b << endl;
	}
};

class Student:public Person						//派生类
{
public:
	virtual void test(int a,int b)
	{
		cout << a * b << endl;
	}
};


void Test(Person& p1)
{
	p1.test(2,3);
}
int main()
{
	Person p1;
	Student s1;

	Test(p1);
	Test(s1);

	return 0;
}

2.重载:

条件:
(1)在同一个作用域.(这个很重要).
(2)参数列表不同,参数个数不同,也可以是参数类型不同或者参数顺序不同。
返回值可同可不同.
(3)函数名相同

实现效果:
函数重载可以为不同的数据类型或参数组合提供相同的接口,使得代码更加方便调用和使用。

int Add(int a, int b)
{
	return a + b;
}

int Add(float a, float b)		//参数不同
{
	return a + b;
}
//float Add(float a, float b)		//也是ok的
//{
//	return a + b;
//}

double Add(double a, double b)
{
	return a + b;
}


int main()
{
	int a = 2;
	float b = 1.2f;
	double c = 2.2;

	cout << Add(a, a) << endl;
	cout << Add(b, b) << endl;
	cout << Add(c, c) << endl;
	return 0;
}

3.重定义

条件:
(1)分别在两个不同的作用域,基类和派生类.
(2)函数名相同,只要不构成重载.

实现效果:
隐藏派生类的函数.

class Person									//基类
{
public:
	 void test(int a,int b)
	{
		cout << a + b << endl;
	}
};

class Student:public Person						//派生类
{
public:
	 void test(int a)				//只要函数名相同即可
	{
		cout << a  << endl;
	}
};

二、抽象类

抽象类是一种特殊的类,它不能被实例化,只能被用作基类。抽象类通常包含一些纯虚函数,这些函数没有实现体,只有函数名。派生类必须实现这些纯虚函数,才能被实例化。
这点很重要,纯虚函数必须被重写.

🍭纯虚函数

纯虚函数是定义在抽象类中的特殊函数,它不需要具体的实现,而是由其派生类实现。
格式:函数声明的分号前加上=0

例如,下面就是一个纯虚函数的定义:

virtual void function() = 0;

抽象类: 包含纯虚函数的类就是抽象类.
抽象类不能被实例化,也就是不能创建对象但是可以定义指向抽象类的指针和引用,并通过派生类对象的地址来初始化它们。

派生类必须实现其基类中所有的纯虚函数,否则它仍然是抽象类,无法被实例化。

纯虚函数的作用是规范继承类的接口,强制派生类提供相应的实现,从而增强程序的可扩展性。同时,纯虚函数也可以作为基类中的一个默认实现,提供一些默认的行为。

抽象类的作用如下:

提供一种适合多态的机制。因为抽象类的纯虚函数只有函数名,没有实现体,所以无法被单独实例化。但是,抽象类可以被用作基类,在派生类中实现纯虚函数,从而实现不同的多态行为。

规范派生类的实现。抽象类中定义的纯虚函数,是对派生类接口的规范。派生类必须实现这些纯虚函数,否则无法被实例化。这样可以避免派生类在实现中遗漏必要的函数或参数,从而保证代码的正确性。

封装类的实现细节。抽象类中通常包含一些实现细节,这些细节对于使用派生类的代码来说并不需要知道。通过将这些细节封装在抽象类中,可以使代码更加清晰和简洁。

总之,抽象类是C++中面向对象编程的重要机制之一,它通过规范派生类的实现和封装类的实现细节,提高了代码的可读性、可维护性和可扩展性。

接口继承与实现继承

实现继承:
派生类继承了基类普通函数,可以使用函数,继承的是函数的实现。也就是实现继承.

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

🍉抽象类示例:

#include <iostream>
#include <string>

// 定义抽象水果类
class Fruit {
public:
    // 纯虚函数,只能在子类重写才有意义
    virtual string getName() const = 0;
    virtual string getColor() const = 0;

    // 虚函数,可以在子类中被重写
    virtual void printInfo() const {
        cout << "This is a " << getName() << "." << endl;
        cout << "Color: " << getColor() << "." << endl;
    }

    // 析构函数,需要为虚函数,确保在析构父类指针时,能够正确调用其子类的析构函数
    virtual ~Fruit() {}
};

// 定义苹果类,继承自水果类
class Apple : public Fruit {
public:
    string getName() const override {
        return "Apple";
    }


    string getColor() const override {
        return "Red";
    }
};

// 定义香蕉类,继承自水果类
class Banana : public Fruit {
public:
    string getName() const override {
        return "Banana";
    }

    string getColor() const override {
        return "Yellow";
    }
};

int main() {

    // Fruit f1;       //抽象类无法实例化出对象

    // 构建苹果对象,调用printInfo()方法

    //方法1
    Fruit* f1 = new Apple;
    f1->printInfo();

    //方法2:
    Apple apple;
    apple.printInfo();

    cout << endl;

    // 构建香蕉对象,调用printInfo()方法
    Banana banana;
    banana.printInfo();

    return 0;
}

抽象类无法直接实例化出对象,只有被继承,进行函数重写才有意义.

在这里插入图片描述

三、解密多态原理

还记得在刚刚接触类和对象的时候,我们需要了解对象的大小如何计算.
对于函数,所有的类都可能需要使用,这可以将函数存放在公共区域,也就是不在类中,不占用类的空间.
那试着计算一下People类的大小吧!

class People
{
public:
    virtual void Have_lunch()
    {
        cout << "你需要支付10元的午餐费!" << endl;
    }
    virtual void Test()
    {
        int a = 2;
    }
protected:
    int _b=2;
};

int main()
{
    cout << sizeof(People) << endl;
    People p1;

    return 0;
}

在这里插入图片描述

运行结果:

8

解析:

如下图:
vfptr是一个指针,占用四个字节(32位下).
_b是int类型占四个字节.
在这里插入图片描述

vfptr是什么呢?
Virtual Function Pointer虚函数指针.
虚函数指针,顾名思义,就是用于指向虚函数的指针.
对象中的这个指针我们叫做虚函数表指针。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表.

基类中的虚表

虚表中存放着虚函数的地址.
在这里插入图片描述

派生类的虚表

派生类的虚表有两部分构成:
第一部分: 从基类中继承下来的虚函数(如果在派生类中也定义了,就会重写,也就实现了多态).
第二部分: 派生类自己的虚函数,放在虚函数表的下半部分.(这里在监视窗口中没看到,但是在内存窗口可以看到).
内存窗口中看到的第三个函数指针,我们猜测是派生类自己的虚函数,下面再验证.
在这里插入图片描述
派生类的虚表生成:
先将基类中的虚表内容拷贝一份到派生类虚表中 .(继承下来)
如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 (重写)
派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。(新增)

在这里插入图片描述

原理:
多态是因为在派生类中,对继承下来的虚函数进行了重写.
当程序调用一个虚函数时,实际上是通过对象的vptr找到相应的虚函数表,再根据函数在虚函数表中的索引找到具体的函数地址。如果对象是派生类的实例,而且派生类中重写了虚函数,那么调用该函数时就会调用派生类中的版本。这种机制在程序运行时动态决定了具体调用哪个函数,从而实现了多态特性。
在这里插入图片描述

注意:
多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的

简单来说就是,普通的函数调用就是 call这个函数的地址,然后执行函数的语句就行,这就是静态的调用.

而多态不同,在执行函数调用时并不知道函数地址,而是运行起来后需要通过对象去对应的虚函数表中寻找,等找到对应的函数后再确定地址.(也就是动态调用).

如何打印虚函数表?

#include <iostream>
#include <string>
using namespace std;

class People
{
public:
    virtual void Have_lunch()
    {
        cout << "People::Have_lunch" << endl;
    }
    virtual void Test()
    {
        cout << "People::Test()" << endl;
    }
    void P1_test() //会被继承下去,但是不会进虚函数表
    {
        cout << "People::P1_test()" << endl;
    }

protected:
    int _b = 2;
};

class Teacher : public People
{
public:
    virtual void Have_lunch()
    {
        cout << "Teacher::Have_lunch()" << endl;
    }
    virtual void Test1()
    {
        cout << "Teacher::Test1()" << endl;
    }
    void Test_Teacher()
    {
        cout << "Teacher::Test_Teacher" << endl;
    }

protected:
    int _c;
};


//声明一个函数指针
typedef void (*VFPtr_Table)();      //函数指针的类型重命名不一样,这里不能写成typedef void (*)() VFPtr_Table

void Print_vfptr(VFPtr_Table table[])       //参数类型是一个 函数指针 数组
{

    for (int i = 0; table[i] != nullptr; i++)       //这里循环结束的条件是遇到空指针,这是VS特有,不具备跨平台特性
    {
        printf("table[%d]--->%p:: ", i, table[i]);
        table[i]();		//通过函数指针调用相应的函数
    }
}

int main()
{
    People p1;
    Teacher t1;


    cout << "People::VFPtr_Table::" << endl;
    Print_vfptr((VFPtr_Table *)*(int *)&p1); // &p1表示取出对象的地址
                                             //(int*)&p1表示获取对象的前四个字节,也就是指向虚表的地址
                                             //*((int*)&p1)对虚表指针解引用,得到虚表地址
                                             //(VFPtr_Table*)*(int *)&p1 将得到的虚表地址,强转为函数指针传参.
                                            
    cout << "Student::VFPtr_Table::" << endl;
    Print_vfptr((VFPtr_Table *)*(int *)&t1);
    return 0;
}

对函数指针不大了解的友友们,可能理解起来就困难一些了,这里注释牛牛已经算是讲解的比较详细了.

验证猜想:
在这里插入图片描述

在这里插入图片描述

  1. 虚函数存在哪的?虚表存在哪的?
    可不要说虚函数在存在虚表中,虚表是一个函数指针数组,里面存放的都是一个个函数指针. 他们只是指向虚函数的指针.
    那虚函数存在哪里呢?
    在这里插入图片描述

虚函数和普通函数一样的,都是存在代码段(常量区)的.
虚表看上去是存放在对象中,其实也不然,对象只是存放虚表指针.
那么虚表存在哪的呢?

对于这类问题,我们可以直接实践一波.
分别在栈区,堆区,常量区和静态区分别定义一个变量,打印其地址,再与虚表地址进行对比.

验证虚表位置:

#include <iostream>
#include <string>
using namespace std;

class People
{
public:
    virtual void Have_lunch()
    {
        cout << "People::Have_lunch" << endl;
    }
    virtual void Test()
    {
        cout << "People::Test()" << endl;
    }
    void P1_test() //会被继承下去,但是不会进虚函数表
    {
        cout << "People::P1_test()" << endl;
    }

protected:
    int _b = 2;
};

class Teacher : public People
{
public:
    virtual void Have_lunch()
    {
        cout << "Teacher::Have_lunch()" << endl;
    }
    virtual void Test1()
    {
        cout << "Teacher::Test1()" << endl;
    }
    void Test_Teacher()
    {
        cout << "Teacher::Test_Teacher" << endl;
    }

protected:
    int _c;
};

typedef void (*VFPtr_Table)();
void Print_vfptr(VFPtr_Table table[])
{

    for (int i = 0; table[i] != nullptr; i++)
    {
        printf("table[%d]--->%p:: ", i, table[i]);
        table[i]();
    }
}

int main()
{
    People p1;
    Teacher t1;
    int a = 0;              //栈区
    printf("栈区:%p\n", &a);



    int* p = new int;       //堆区
    printf("堆区:%p\n", p);

    static int  sa = 0;     //静态区
    printf("静态区:%p\n", &sa);

    const char* ca = "CSDN!! cjn";       //常量区(代码段)
    printf("常量区:%p\n", ca);
 
    cout << endl;
    printf("虚表1地址:%p\n",*((int*)&p1));         //&p1表示对象的地址
                                                    //(int*)&p1表示获取对象的前四个字节,也就是指向虚表的地址
                                                    //*((int*)&p1)对虚表指针解引用,得到虚表地址
    printf("虚表2地址:%p\n",*((int*)&t1));

    
    cout << "People::VFPtr_Table::" << endl;
    Print_vfptr((VFPtr_Table *)*(int *)&p1);

    cout << "Student::VFPtr_Table::" << endl;
    Print_vfptr((VFPtr_Table *)*(int *)&t1);
    return 0;
}

运行结果:
在这里插入图片描述
很明显,常量区的地址距离虚表最近.

四、多继承中的虚表

#include<iostream>

using namespace std;

class A
{
public:
    virtual void Fun1(){    cout << "A::Fun1()" << endl;    }
    virtual void Fun2(){    cout << "A::Fun2()" << endl;    }
};


class B 
{
public:
    virtual void Fun1() { cout << "B::Fun1()" << endl; }
    virtual void Fun3() { cout << "B::Fun3()" << endl; }

};


class C :public A ,public B
{
public:
    virtual void Fun1() { cout << "C::Fun1()" << endl; }
    virtual void Fun3() { cout << "C::Fun3()" << endl; }
    virtual void Fun4() { cout << "C::Fun4()" << endl; }
};

typedef void (*VFPtr_Table)();
void Print_vfptr(VFPtr_Table table[])
{

    for (int i = 0; table[i] != nullptr; i++)
    {
        printf("table[%d]--->%p:: ", i, table[i]);
        table[i]();
    }
}

int main()
{
    A a1;
    B b1;
    C c1;
   

    cout << "A::VFPtr_Table::" << endl;
    Print_vfptr((VFPtr_Table*)*(int*)&a1);
    cout << endl;

    cout << "B::VFPtr_Table::" << endl;
    Print_vfptr((VFPtr_Table*)*(int*)&b1);
    cout << endl;

    cout << "C::A::VFPtr_Table::" << endl;
    Print_vfptr((VFPtr_Table*)*(int*)&c1);
    cout << "C::B::VFPtr_Table::" << endl;
    Print_vfptr((VFPtr_Table*)*((int*)&c1 +1));

    return 0;
}

通过观察监视窗口,我们可以看到,派生类c中,有两个虚表.
在这里插入图片描述

在这里插入图片描述
(图片不清晰,请见谅.)

在这里插入图片描述

主要有两点:

  1. 基类中的虚函数,无论在派生类中是否被重写,都存放在派生类中对应的该基类虚表中.
    被重写的虚函数,在虚表中被覆盖.
  2. 派生类自己的虚函数,存放在第一个基类的虚表后面,

对于菱形虚拟继承,菱形继承都不推荐设计,就别谈菱形虚拟继承了,这里也就不讨论了.

c++中有关多态的知识,到这里就结尾了,如果文章有什么错误之处,希望与牛牛私信交流,牛牛会一 一改正的.

码文不易,三连支持一下吧!
在这里插入图片描述

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

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

相关文章

二进制分析工具-radare2使用教程

二进制分析工具-radare2使用教程 按照如下执行命令 按照如下执行命令 r2 -A 二进制文件

我为什么开始写技术博客

今天没有技术文章&#xff0c;只是想聊聊认真做CSDN和公众号以来的一些感想。 1.为什么开启技术分享 我不算是一个聪明的人&#xff0c;没有过目不忘的本事&#xff0c;所以从工作开始就养成了做笔记的习惯&#xff1b; 最开始15、16年做模型开发&#xff0c;那时候环境其实就…

*ST富吉-688272 三季报分析(20231117)

*ST富吉-688272 基本情况 公司名称&#xff1a;北京富吉瑞光电科技股份有限公司 A股简称&#xff1a;*ST富吉 成立日期&#xff1a;2011-01-20 上市日期&#xff1a;2021-10-18 所属行业&#xff1a;计算机、通信和其他电子设备制造业 周期性&#xff1a;1 主营业务&#xff1a…

23111702[含文档+PPT+源码等]计算机毕业设计javaweb高校宿舍管理系统寝室管理

文章目录 **软件开发环境及开发工具&#xff1a;****项目功能介绍&#xff1a;****论文截图&#xff1a;****实现&#xff1a;****代码片段&#xff1a;** 编程技术交流、源码分享、模板分享、网课教程 &#x1f427;裙&#xff1a;776871563 软件开发环境及开发工具&#xff…

Excel 文件比较工具 xlCompare 11.01 Crack

比较两个 Excel 文件之间的差异 xlCompare. xlCompare.com 是性能最佳的 Excel diff 工具&#xff0c;用于比较两个 Excel 文件或工作表并在线突出显示差异。xlCompare 包括免费的在线 Excel 和 CSV 文件比较服务以及用于比较和合并 Excel 文件的强大桌面工具。如果您想在线了…

STM32与ZigBee无线通信技术在工业自动化中的应用

工业自动化是指利用电子技术、计算机技术和通信技术等手段&#xff0c;对工厂、设备和生产过程进行自动化控制和管理的过程。在工业自动化中&#xff0c;可靠的无线通信技术对于实时数据的传输和设备的协同控制至关重要。本文将介绍STM32微控制器与ZigBee无线通信技术在工业自动…

MySQL 运算符二

逻辑运算符 逻辑运算符用来判断表达式的真假。如果表达式是真&#xff0c;结果返回 1。如果表达式是假&#xff0c;结果返回 0。 运算符号作用NOT 或 !逻辑非AND逻辑与OR逻辑或XOR逻辑异或 1、与 mysql> select 2 and 0; --------- | 2 and 0 | --------- | 0 | -…

python django 小程序商城源码

开发环境&#xff1a; PyCharm&#xff0c;mysql5.7&#xff0c;微信开发者工具 技术说明&#xff1a; python django html vue.js bootstrap 微信小程序 功能介绍&#xff1a; 用户端&#xff1a; 登录注册&#xff08;含授权登录&#xff09; 首页显示搜索商品(可根据…

SQL 的 AND、OR 和 NOT 运算符:条件筛选的高级用法

AND 运算符 SQL的AND运算符用于根据多个条件筛选记录&#xff0c;确保所有条件都为TRUE才返回记录。下面是AND运算符的基本语法&#xff1a; SELECT column1, column2, ... FROM table_name WHERE condition1 AND condition2 AND condition3 ...;column1, column2,等是您要选…

“具有分布式能源资源的多个智能家庭的能源管理的联邦强化学习”文章学习四——基于联邦深度学习的多智能家居能源管理

一、用于家庭能源管理的FRL算法 在本节中&#xff0c;我们将阐述提出的FRL算法&#xff08;算法1&#xff09;&#xff0c;该算法以分布式方式调度多个智能家庭的能量消耗。在提出的FRL框架中&#xff0c;LHEMS和GS相互迭代并有效训练LHEMS的模型。我们考虑了由LHEMS控制的空调…

Python | 机器学习之SVM支持向量机

​&#x1f308;个人主页&#xff1a;Sarapines Programmer&#x1f525; 系列专栏&#xff1a;《人工智能奇遇记》&#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 目录结构 1. 机器学习之SVM支持向量机概念 1.1 机器学习 1.2 SVM支持向量机 2. SVM支持向量机…

这就是不创业的最大的原因,机器视觉兄弟们创业要谨慎,为什么99.99%机器视觉公司老板是销售人员?

机器视觉公司&#xff0c;99%公司实行销售优先原则&#xff0c;企业老板99%从销售人员中产生。所以机器视觉兄弟们创业要谨慎。 企业的本质就是通过销售获得利润的组织&#xff0c;销售是立足之本&#xff0c;没有销售&#xff0c;创业就是耍流氓。因此&#xff0c;企业的一切…

基于SpringBoot的SSMP整合案例(消息一致性处理与表现层开发)

消息一致性处理 在后端执行完相应的操作后&#xff0c;我们需要将执行操作后的结果与数据返回前端&#xff0c;前端 调用我们传回去的数据&#xff0c;前端是如何知道我们传回去的数据名称的&#xff1f; 答&#xff1a;前后端遵循了同一个"协议"。这个协议就是定义…

贝茄莱BR AS实时数据采集功能

实时数据采集功能在PLC系统调试过程中&#xff0c;有助于调试人员对变量变化进行监测&#xff0c;通过波形对比&#xff0c;反应不同变量间的相互作用。该测试目的在于验证贝加莱系统组态软件的实时数据采集功能。 贝加莱系统组态软件提供Trace功能&#xff0c;连接PLC&#x…

国产高云FPGA:OV5640图像视频采集系统,提供Gowin工程源码和技术支持

目录 1、前言免责声明 2、相关方案推荐国产高云FPGA相关方案推荐国产高云FPGA基础教程 3、设计思路框架视频源选择OV5640摄像头配置及采集动态彩条Video Frame Buffer 图像缓存DDR3 Memory Interface 4、Gowin工程详解5、上板调试验证并演示准备工作静态演示 6、福利&#xff1…

解锁OpenAI潜力:OpenAI 全面解析与最佳实践

该项目是由OpenAI公司提供的一个大型代码库&#xff0c;其中包含了各类与OpenAI API相关的代码示例和最佳实践。 此项目名为OpenAI Cookbook&#xff0c;目的是为了帮助使用者更有效地利用OpenAI API&#xff0c;将其应用于自己的工作和生活中。具体来说&#xff0c;可以解决一…

CF1899C Yarik and Array(DP,贪心)

题目链接 题目 A subarray is a continuous part of array. Yarik recently found an array a of n elements and became very interested in finding the maximum sum of a non empty subarray. However, Yarik doesn’t like consecutive integers with the same parity, s…

基于SpringBoot+Redis的前后端分离外卖项目-苍穹外卖(五)

公共字段自动填充 1.1 问题分析1.2 实现思路1.3 代码开发1.3.1 步骤一1.3.2 步骤二1.3.3 步骤三 1.4 功能测试 1.1 问题分析 在前面我们已经完成了后台系统的员工管理功能和菜品分类功能的开发&#xff0c;在新增员工或者新增菜品分类时需要设置创建时间、创建人、修改时间、修…

虹科示波器 | 汽车免拆检修 | 2015款奔驰G63AMG车发动机偶尔自动熄火

一、故障现象 一辆2015款奔驰G63AMG车&#xff0c;搭载157发动机&#xff0c;累计行驶里程约为9.4万km。车主反映&#xff0c;该车低速行驶时&#xff0c;发动机偶尔会自动熄火&#xff0c;故障大概1个星期出现1次。 二、故障诊断 接车后路试&#xff0c;故障未能再现。用故障检…

简单线性回归函数

简单线性回归函数 定义术语理解简单线性回归例子 定义 线性回归&#xff1a;利用线性回归方程中最小平方函数对一个或多个自变量和因变量之间关系进行建模的一个回归分析。该建模的目标为找到各个系数的最佳值让预测误差最小 简单线性回归&#xff1a;只有一个自变量的线性回…