C++多态原理揭秘

🎈个人主页:🎈 :✨✨✨初阶牛✨✨✨
🐻强烈推荐优质专栏: 🍔🍟🌯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++中有关多态的知识,到这里就结尾了,如果文章有什么错误之处,希望与牛牛私信交流,牛牛会一 一改正的.

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

天猫精灵/小爱同学+巴法云+Openwrt控制局电脑/群晖开关机

天猫精灵/小爱同学巴法云Openwrt控制局电脑/群晖开关机 事情的起因实战环境开始发车1.天猫精灵/小爱同学 连接 八法云 2.openwrt3.docker环节注意:sshpass 要先使用 ssh命令登陆一下你要唤醒或者远程关机的设备,不然可能因为一个登陆提示你是否登陆的yes/no导致程序没有反应,然…

任正非说:公司要逐步实行分灶吃饭,我们在管理上不能过于整齐划一,否则缺少战斗力。

你好&#xff01;这是华研荟【任正非说】系列的第42篇文章&#xff0c;让我们聆听任正非先生的真知灼见&#xff0c;学习华为的管理思想和管理理念。 一、我们必须在混沌中寻找战略方向。规划就是要抓住机会点&#xff0c;委员会是火花荟萃的地方&#xff0c;它预研的方向是可做…

ESP32 MicroPython LCD显示实验⑤

ESP32 MicroPython LCD显示实验⑤ 1、实验目的2、实验平台3、实验内容4、参考代码5、实验结果 1、实验目的 LCD显示屏显示中英文字符、显示图片 2、实验平台 智能小车(配备显示屏) 3、实验内容 小车配有2.0寸的TFT彩屏&#xff0c;内置有中文GBK字库&#xff0c;可以显示中…

值得你一生收藏的BMW宝马汽车底盘代号各个版本说明,方便今后查阅使用!

很少有汽车品牌像宝马一样&#xff0c;本属于内部交流使用的底盘代号&#xff08;Development Code&#xff09;&#xff0c;最终延伸为粉丝群体用以精准定位某一年代某一款车型的通用语。随着宝马加速推出新产品&#xff0c;每一年的底盘代号都在更新。你挚爱的强哥现将宝马所…

echarts 三角锥形柱状图 + 带阴影的折线图示例

该示例有如下几个特点&#xff1a; ①三角锥形折线图 ②折线图自带阴影 ③三角锥形鼠标放置时颜色改变 ④数据随着鼠标移动而展示 ⑤鼠标放置时tooltip样式自定义&#xff08;echarts 实现tooltip提示框样式自定义-CSDN博客&#xff09; 代码如下&#xff1a; this.options …

鸿蒙ToastDialog内嵌一个xml页面会弹跳到一个新页面《解决》

ToastDialog 土司组件 1.问题展示2.代码展示3.问题分析 1.问题展示 0.理想效果 错误效果: 1.首页展示页面 (未点击按钮前) 2.点击按钮之后&#xff0c;弹窗不在同一个位置 2.代码展示 1.点击按钮的 <?xml version"1.0" encoding"utf-8"?> <…

HTTP1.0协议详解

前言主要特点存在的不足与HTTP1.1的区别在Java中应用HTTP1.0协议知识拓展 前言 HTTP是由蒂姆伯纳斯李&#xff08;Tim Berners-Lee&#xff09;爵士创造的。他在1989年提出了一个构想&#xff0c;借助多文档之间相互关联形成的超文本&#xff08;HyperText&#xff09;&#x…

[开源]基于 AI 大语言模型 API 实现的 AI 助手全套开源解决方案

原文&#xff1a;[开源]基于 AI 大语言模型 API 实现的 AI 助手全套开源解决方案 一飞开源&#xff0c;介绍创意、新奇、有趣、实用的开源应用、系统、软件、硬件及技术&#xff0c;一个探索、发现、分享、使用与互动交流的开源技术社区平台。致力于打造活力开源社区&#xff0…

keepalived离线安装

上传离线安装包 将离线安装包拖动到服务器上 进入到离线安装包路径&#xff0c;执行下面脚本进行安装 rpm -Uvh --force --nodeps *.rpm

C++初级项目-webserver(1)

1.引言 Web服务器是一个基于Linux的简单的服务器程序&#xff0c;其主要功能是接收HTTP请求并发送HTTP响应&#xff0c;从而使客户端能够访问网站上的内容。本项目旨在使用C语言&#xff0c;基于epoll模型实现一个简单的Web服务器。选择epoll模型是为了高效地处理大量并发连接…

CF1899A Game with Integers(思维题)

题目链接 题目 题目大意 t 组测试样例 每组给一个正整数 n&#xff0c; 有两种操作&#xff1a; 1-1 A 和 B 轮流操作&#xff0c; 如果这个整数变成了一个能被3整除的数&#xff0c;A赢&#xff0c;输出First 如果在10次操作以内&#xff0c;n不能被3整数&#xff0c;B赢&…

TCP与UDP协议

TCP与UDP协议 1、TCP协议&#xff1a; 1、TCP特性&#xff1a; TCP 提供一种面向连接的、可靠的字节流服务。在一个 TCP 连接中&#xff0c;仅有两方进行彼此通信。广播和多播不能用于 TCP。TCP 使用校验和&#xff0c;确认和重传机制来保证可靠传输。TCP 给数据分节进行排序…

智能驾驶汽车虚拟仿真视频数据理解(一)

赛题官网 datawhale 赛题介绍 跑通demo paddle 跑通demo torch 提交的障碍物取最主要的那个&#xff1f;不考虑多物体提交。障碍物&#xff0c;尽可能选择状态发生变化的物体。如果没有明显变化的&#xff0c;则考虑周边的物体。车的状态最后趋于减速、停止&#xff0c;时序…

OpenAI的Whisper蒸馏:蒸馏后的Distil-Whisper速度提升6倍

1 Distil-Whisper诞生 Whisper 是 OpenAI 研发并开源的一个自动语音识别&#xff08;ASR&#xff0c;Automatic Speech Recognition&#xff09;模型&#xff0c;他们通过从网络上收集了 68 万小时的多语言&#xff08;98 种语言&#xff09;和多任务&#xff08;multitask&am…

Windows11怎样投屏到电视上?

电视屏幕通常比电脑显示器更大&#xff0c;能够提供更逼真的图像和更震撼的音效&#xff0c;因此不少人也喜欢将电脑屏幕投屏到电视上&#xff0c;缓解一下低头看电脑屏幕的烦恼。 Windows11如何将屏幕投射到安卓电视&#xff1f; 你需要在电脑和电视分贝安装AirDroid Cast的电…

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

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

hahahaha发到这里吧

一大早上笑死我 恭喜在座的各位&#xff0c;一直以为这次比赛public和private排名会相差不大&#xff0c;结果前6有4个人都是从银牌歘一下上来的&#xff0c;想象地到他们看到结果时的喜悦

python引入自己不同目录的模块

1.目录结构 from manual_data.utils import delete_and_insert_center

ceph学习笔记

ceph ceph osd lspoolsrbd ls -p testpool#查看 ceph 集群中有多少个 pool,并且每个 pool 容量及利 用情况 rados dfceph -sceph osd tree ceph dfceph versionsceph osd pool lsceph osd crush rule dumpceph auth print-key client.adminceph orch host lsceph crash lsceph…

搞科研、写论文,如何正确使用GPT?AIGC技术解析、提示词工程高级技巧、AI绘图、ChatGPT/GPT4应用

目录 专题一 OpenAI开发者大会最新技术发展及最新功能应用 专题二 AIGC技术解析 专题三 提示词工程高级技巧 专题四 ChatGPT/GPT4的实用案例 专题五 让ChatGPT/GPT4成为你的论文助手 专题六 让ChatGPT/GPT4成为你的编程助手 专题七 让ChatGPT/GPT4进行数据处理 专题八 …