21.多态

news2025/3/19 4:25:57

一、多态概念

多种形态。

静态多态:编译时多态。(函数重载)

动态多态:运行时多态。(继承关系下,调用父类指针或引用,对于不同的对象有不同的行为)

二、多态的定义及实现

1)多态的构成条件

多态时一个继承关系下的类对象,去调用同一函数,产生了不同的行为。

实现多态的两个必须条件:

1.必须是父类的指针或引用调用虚函数。

2.被调用的函数必须是虚函数,子类必须对父类的虚函数重写/覆盖。

2)虚函数

易错点1:

virtual关键字只在声明时加上,在类外实现时不能加

易错点2:

static和virtual是不能同时使用的。

原因:static成员函数没有this指针,且virtual成员函数依赖于this指针来实现动态绑定。

 易错点3:

重定义就是隐藏。

虚函数:类成员函数前加virtual修饰。

注:非成员函数不能加virtual修饰。

虚函数的重写/覆盖:子类中有一个和父类完全相同的虚函数(参数列表,函数名,返回值)

例如:

#include <iostream>
using namespace std;

class Person
{
public:
	virtual void BuyTicket()
	{}
};

class Teacher :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "老师,买全票" << endl;
	}
};

class Student :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "学生,优惠买票" << endl;
	}
};

void Buy(Person* ptr)
{
	ptr->BuyTicket();
}

int main()
{
	Student s;
	Teacher t;

	Buy(&s);
	Buy(&t);
	return 0;
}

其中,满足了多态,和ptr的类型无关,与ptr指向的对象有关。

注意:重写父类虚函数时,子类的虚函数可以不加virtual,也构成重写。

原因:继承后父类的虚函数被继承下来了在子类中保持虚函数属性,但这种写法不规范。

面试题:

 以下程序输出结果是什么()
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

class A
{
public:
	virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
	virtual void test() { func(); }
};
class B : public A
{
public:
	void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
	B* p = new B;
	p->test();
	return 0;
}

答案是B,输出结果为:B->1

分析:A类的func和B类的func构成重写,因为参数列表,返回值,函数名都一致,其中参数列表是否不同和缺省值具体是多少无关,B类中即使没加virtual,也没有问题,因为继承后父类的虚函数被继承下来在子类中仍旧保持虚函数属性

编译器对于重写的函数的处理:

3)虚函数重写的一些其他问题

协变:

概念:子类重写父类虚函数时,与父类虚函数返回值类型不同。

不同指必须是:父类返回值返回父类的指针和引用,子类返回值返回子类的指针和引用。

可以是自身的继承关系,也可以是其他的继承关系。

测试代码:

#include <iostream>
using namespace std;

class A
{};
class B :public A
{};

class Person
{
public:
	virtual A* func()
	{
		cout << "Person::func()" << endl;
		return nullptr;
	}
};
class Student :public Person
{
public:
	virtual B* func()
	{
		cout << "Student::func()" << endl;
		return nullptr;
	}
};
class Teacher :public Person
{
public:
	virtual B* func()
	{
		cout << "Teacher::func()" << endl;
		return nullptr;
	}
};
void f(Person* ptr)
{
	ptr->func();
}

int main()
{
	Person p;
	Student s;
	Teacher t;

	f(&p);
	f(&s);
	f(&t);
	return 0;
}

 可以看到,用其他类的继承关系实现协变也可。

析构函数的重写

父类的析构函数为虚函数,此时子类析构函数只要定义,无论是否加virtual,都构成重写。

原因:编译器对析构函数的名称特殊处理,编译后析构函数的名称统⼀处理成destructor
为什么父类的析构函数建议设计成虚函数?
测试代码:
#include <iostream>
using namespace std;

class A
{
public:
	~A()
	{
		cout << "~A()" << endl;
	}
protected:
	int _a;
};
class B :public A
{
public:
	~B()
	{
		delete[] _b;
		cout << "~B()" << endl;
	}
private:
	int* _b = new int[10];
};

int main()
{
	B* b = new B;
	A* a = new B;

	delete b;
	delete a;
	return 0;
}
结果为:可以明确看到,若A* a = new B; 这种情况,如果B类不重写A类的析构函数,那么就会导致delete a; 时,只调用了父类A的析构函数,没有调用子类B的析构函数,导致内存泄漏。
解决方法:重写父类A的析构函数。

4)重载/重写/隐藏的比较(同名函数的关系)

重载:

1.两个函数在同一作用域

2.函数名相同,参数不同(参数列表不同),返回值不限

重写/覆盖:

1.两个函数分别在继承体系的父类和子类作用域

2.函数名,参数,返回值必须相同,协变例外

3.两个函数必须都是虚函数

隐藏:

1.两个函数分别在继承体系的父类和子类作用域

2.函数名相同

3.父子类的成员变量名相同(成员变量方面) 

 三、纯虚函数和抽象类

定义方式:在纯虚函数后面加上=0。

class A
{
public:
	virtual void func() = 0;
};

不需要定义实现(但在语法上可以实现,没有必要),只要声明即可。

抽象类:包含纯虚函数的类。

抽象类不能实例化出对象。

子类继承父类,若没有重写父类的纯虚函数,那么子类也是抽象类。(间接强制重写纯虚函数,不重写实例化不出对象)

四、多态的原理

下面编译为32位程序的运行结果是什么()
A. 编译报错 B. 运行报错 C. 8  D. 12

class Base
{
public:
	virtual void Func1()
	{
		std::cout << "Func1()" << endl;
	}
protected:
	int _b = 1;
	char _ch = 'x';
};
int main()
{
	Base b;
	cout << sizeof(b) << endl;
	return 0;
}

答案是D,sizeof(b)是12字节。

原因,b对象中有_vfptr,虚函数表的指针,存在对象的起始位置,类型为void**,在32位环境下是指针是4字节,根据内存对齐规则,对齐后为12字节。

多态原理分析: 

测试代码:

#include <iostream>
using namespace std;

class Person
{
public:
	virtual void func()
	{
		cout << "Person::func()" << endl;
	}
protected:
	string _name;
};
class Student :public Person
{
public:
	virtual void func()
	{
		cout << "Student::func()" << endl;
	}
private:
	int _sid;
};
class Teacher :public Person
{
public:
	virtual void func()
	{
		cout << "Teacher::func()" << endl;
	}
private:
	int _tid;
};
void f(Person* ptr)
{
	ptr->func();
}

int main()
{
	Person p;
	Student s;
	Teacher t;

	f(&p);
	f(&s);
	f(&t);
	return 0;
}

通过监视窗口可以看到,p,s,t对象各有一个虚函数表指针_vfptr(virtual function table 虚函数表指针),且指针的值不同,说明指向不同的空间,而且指向的内容函数指针不同,p对象为它的Person::func的函数指针,重写的s和t对象为各自重写的函数指针。

逻辑结构如图:

指向谁调用谁,运行时,到指向对象的虚函数表中找到对应虚函数的地址,进行调用,达到多态。

动态绑定和静态绑定

静态绑定:对不满足多态条件(指针或引用调用虚函数)在编译时确定,编译时确定调用函数的地址。

动态绑定:满足多态条件的函数调用是在运行时绑定,也就是运行时到指定对象的虚函数表中找到调用函数的地址。

虚函数表

易错点1:

虚函数表是在编译时生成的。

易错点2:

#include<iostream>
using namespace std;

class A
{
public:
    virtual void f()
    {
        cout << "A::f()" << endl;
    }
};
class B : public A
{
private:
    virtual void f()
    {
        cout << "B::f()" << endl;
    }
};

int main()
{
    A* pa = (A*)new B;
    pa->f();
	return 0;
}

输出:B::f() 。
原因:即使B中重写的f()函数是私有的,但是已经构成重写了,把虚函数表中原有的A的f()函数指针覆盖了,最终调用的位置不变,只是执行函数发生变化访问权限的检查是基于指针的类型A而不是指向的对象实际类型B

易错点3:

class A
{
public:
  A ():m_iVal(0){test();}
  virtual void func() { std::cout<<m_iVal<<‘ ’;}
  void test(){func();}
public:
  int m_iVal;
};


class B : public A
{
public:
  B(){test();}
  virtual void func()
  {
    ++m_iVal;
    std::cout<<m_iVal<<‘ ’;
  }
};

int main(int argc ,char* argv[])
{
  A*p = new B;
  p->test();
  return 0;
}

输出结果为:0 1 2

分析:最开始,new B,开内存,调用B的构造,由于B继承A,初始化列表调用A的默认构造,初始化列表m_iVal被初始化成0,然后调用test(),由于此时还处于父类对象构造阶段,多态机制还没有生效,调用的是父类A的func(),打印0 。然后借着就是B的构造,调用test(),此时父类已经构造好了,func也实现了重写,满足多态机制,调用重写后的func,打印1 。然后A* p调用test(),符合多态机制,调用重写后的func,打印2 。

1)父类对象的虚函数表中存放所有虚函数的地址。

同类型对象共用一张虚表,不同类型对象有各自独立的虚表,因此子类和父类有各自虚表。

2)子类由两部分构成,继承下来的父类和自己的成员。一般情况下,继承下来的父类中有虚函数表指针,自己就不会再生成虚函数表指针。

3)子类重写的父类的虚函数,子类的虚函数表中对应的虚函数就会被覆盖成子类重写的虚函数地址。

测试代码:

#include <iostream>
using namespace std;

class A
{
public:
	virtual void print()
	{
		cout << "A::print()" << endl;
	}
	virtual void funcA()
	{
		cout << "A::funcA()" << endl;
	}
private:
	int _a;
};

class B :public A
{
public:
	virtual void print()
	{
		cout << "B::print()" << endl;
	}
	virtual void funcB()
	{
		cout << "B::funcB()" << endl;
	}
private:
	int _b;
};

class C :public B
{
public:
	virtual void print()
	{
		cout << "C::print()" << endl;
	}
	virtual void funcC()
	{
		cout << "C::funcC()" << endl;
	}
private:
	int _c;
};


int main()
{
	C c;
	c.funcB();

	return 0;
}

可以看到,在纯单继承体系中,只有一张虚函数表,存放所有继承体系的虚函数地址,其中被重写的虚函数只存放最终版(之前的都被覆盖了)。

4)子类的虚函数表中包含,父类的虚函数地址,子类重写(覆盖的虚函数地址),子类自己的虚函数地址。

测试代码:

#include <iostream>
using namespace std;

class A
{
public:
	virtual void func1()
	{}
};

class B
{
public:
	virtual void func2()
	{}
};

class C :public A, public B
{
public:
	virtual void func3()
	{}
};

typedef void(C::* P)();
int main()
{
	A a;
	B b;
	C c;
	P p = &C::func3;
	int n = 0;
	return 0;
}

可以看到,多继承,C同时继承A和B,A和B各自独立有一个虚函数表指针,C自身的虚函数指针存放在第一个继承的类A的虚函数指针表中。
5)虚函数表本质是⼀个存虚函数指针的指针数组,一般情况这个数组最后面放了⼀个0x00000000标记。(这个C++并没有进行规定,各个编译器自行定义的,vs系列编译器会再后面放个0x00000000标记,g++系列编译器不会放)

6)虚函数和普通函数一样,存放在代码段的,编译后是一段指令,只是虚函数的地址又存到虚函数表中。

7)虚函数表存在哪里?C++没有规定,以下是一个测试程序,来测试虚函数表存在哪里。

测试代码:

#include <iostream>
using namespace std;

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
	void func5() { cout << "Base::func5" << endl; }
protected:
	int a = 1;
};
class Derive : public Base
{
public:
	// 重写基类的func1
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func1" << endl; }
	void func4() { cout << "Derive::func4" << endl; }
protected:
	int b = 2;
};

int main()
{
	int i = 0;
	static int j = 1;
	int* p1 = new int;
	const char* p2 = "xxxxxxxx";
	printf("栈:%p\n", &i);
	printf("静态区:%p\n", &j);
	printf("堆:%p\n", p1);
	printf("常量区:%p\n", p2);
	Base b;
	Derive d;
	Base* p3 = &b;
	Derive* p4 = &d;
	printf("Person虚表地址:%p\n", *(int*)p3);
	printf("Student虚表地址:%p\n", *(int*)p4);
	printf("虚函数地址:%p\n", &Base::func1);
	printf("普通函数地址:%p\n", &Base::func5);
	return 0;
}

可以看出Person虚表地址和Student虚表地址和常量区非常接近,可以推断出vs2022的虚表是存在常量区的。

可以看出Person和Student虚表地址和常量区最接近,可以推断出g++4.8.5的虚表存在常量区。

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

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

相关文章

【蓝桥杯】第十三届C++B组省赛

⭐️个人主页&#xff1a;小羊 ⭐️所属专栏&#xff1a;蓝桥杯 很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~ 目录 试题A&#xff1a;九进制转十进制试题B&#xff1a;顺子日期试题C&#xff1a;刷题统计试题D&#xff1a;修剪灌木试题E&#xf…

C# PaddleOCR字符识别

1 安装Nuget 2 C# using System; using OpenCvSharp; using Sdcb.PaddleOCR; using Sdcb.PaddleOCR.Models.Local; using Sdcb.PaddleOCR.Models; using Sdcb.PaddleInference;namespace ConsoleApp1 {public class MichaelOCR{string imagePath "D:\\BUFFER\\VS\\Text\…

多环境开发-Profiles

在实际的项目开发中&#xff0c;我们通常会涉及多个环境&#xff0c;如开发环境&#xff08;dev&#xff09;、测试环境&#xff08;test&#xff09;和生产环境&#xff08;pro&#xff09;。在不同的环境下&#xff0c;程序的配置信息会有所不同&#xff0c;例如连接的数据库…

《TCP/IP网络编程》学习笔记 | Chapter 18:多线程服务器端的实现

《TCP/IP网络编程》学习笔记 | Chapter 18&#xff1a;多线程服务器端的实现 《TCP/IP网络编程》学习笔记 | Chapter 18&#xff1a;多线程服务器端的实现线程的概念引入线程的背景线程与进程的区别 线程创建与运行pthread_createpthread_join可在临界区内调用的函数工作&#…

MambaVision:一种Mamba-Transformer混合视觉骨干网络

摘要 我们提出了一种新型混合Mamba-Transformer主干网络&#xff0c;称为MambaVision&#xff0c;该网络专为视觉应用而设计。我们的核心贡献包括重新设计Mamba公式&#xff0c;以增强其对视觉特征的高效建模能力。此外&#xff0c;我们还对将视觉Transformer&#xff08;ViT&…

深度学习-服务器训练SparseDrive过程记录

1、cuda安装 1.1 卸载安装失败的cuda 参考&#xff1a;https://blog.csdn.net/weixin_40826634/article/details/127493809 注意&#xff1a;因为/usr/local/cuda-xx.x/bin/下没有卸载脚本&#xff0c;很可能是apt安装的&#xff0c;所以通过执行下面的命令删除&#xff1a; a…

学习单片机需要多长时间才能进行简单的项目开发?

之前有老铁问我&#xff0c;学单片机到底要多久&#xff0c;才能进行简单的项目开发&#xff1f;是三个月速成&#xff0c;还是三年磨一剑&#xff1f; 今天咱们就来聊聊这个话题&#xff0c;我不是什么高高在上的专家&#xff0c;就是个踩过无数坑、烧过几块板子的“技术老友”…

stm32 L432KC(mbed)入门第一课

目录 一. 前言 二. 专栏意义 三. MS入门第一课 一. 前言 新的一年MS课程又开始了&#xff0c;同时也到了该专栏的第三个年头。在前两年中&#xff0c;该专栏帮助了很多第一次接触单片机的同学。其中&#xff0c;有的同学订阅专栏是为了更好的完成并且通过MS这门课程&#xf…

批量给 Excel 添加或删除密码保护|Excel 批量设置打开密码和只读密码

我们在将 Excel 文档发送给第三方或者进行存档的时候&#xff0c;对 Excel 文档添加密码保护是非常重要的一个操作。添加保护后的 Excel 文档。就只能有相应权限的用户才能够打开或者编辑操作。尤其是当我们 Excel 文档中内容非常敏感非常重要的时候&#xff0c;添加保护就显得…

4.JVM-垃圾回收介绍

记录个人学习中记录笔记&#xff0c;如有错误请您指正&#xff0c;谢谢&#x1f64f; 垃圾回收器发展史 传统垃圾回收: 分代回收 不同代有不同的垃圾回收机制 保底 标记清除算法 垃圾识别算法 引用计数法 缺陷:下图2 出现循环引用 无法解决 可达性分析 大部分(Java,pytho…

Redis-锁-商品秒杀防止超卖

一、秒杀&#xff08;Seckill&#xff09;​ 1. ​定义 ​秒杀&#xff1a;短时间内&#xff08;如1秒内&#xff09;大量用户同时抢购 ​限量低价商品 的营销活动。​典型场景&#xff1a;双11热门商品抢购、小米手机首发、演唱会门票开售。 2. ​技术挑战 挑战点说明后果…

第一个vue项目

项目目录 启动vue项目 npm run serve 1.vue.config.js文件 (CLI通过vue-cli-serve启动项目&#xff0c;解析配置配置文件vue-condig-js&#xff09; // vue.config.js //引入path板块&#xff0c;这是Node.js的一个内置模块&#xff0c;用于处理文件路径&#xff0c;这里引用…

基于CNN的多种类蝴蝶图像分类

基于CNN的多种类蝴蝶图像分类&#x1f98b; 基于卷积神经网络对64992786张图像&#xff0c;75种不同类别的蝴蝶进行可视化分析、模型训练及分类展示 导入库 import pandas as pd import os import matplotlib.pyplot as plt import seaborn as sns import numpy as np from …

Unity插件-适用于画面传输的FMETP STREAM使用方法(三)基础使用

目录 一、插件介绍 二、组件介绍 三、Game View Streaming 1、使用 FM Network UDP 的基本设置 Server Scene Client Scene 2、使用使用 FM WebSocket 的基本设置 四、Audio Streaming 五、Microphone Streaming 一、插件介绍 ​​​​​​Unity插件-适用于画面传输的…

微信小程序wx.request接口报错(errno: 600001, errMsg: “request:fail -2:net::ERR_FAILED“)

来看看报错 报错如下: 请求发送部分,代码如下: uni.request({url: self.serverUrl "/getRealName",method: GET,data: {"code": self.info.code,},header: {"Authorization": uni.getStorageSync(tokenHead) uni.getStorageSync(token)}}…

基于Python+MySQL编写的(WinForm)图书管理系统

一、项目需求分析 1.1 项目介绍 项目背景 图书馆管理系统是一些单位不可缺少的部分,书籍是人类不可缺少的精神食粮&#xff0c;尤其对于学校来说&#xff0c;尤其重要。所以图书馆管理系统应该能够为用户提供充足的信息和快捷的查询手段。但一直以来人们使用传统人工的方式管…

[贪心算法] 摆动序列

1.解析 这里我们的贪心体现在&#xff0c;这里我们只需要找到每一个拐点位置的数字即可&#xff0c; 证明&#xff1a; 当我们在A点时&#xff0c;我们下一步的选择有四种 A到D这个线段内的数字&#xff08;不包括D&#xff09;选择D点D到F的点F之后的点 对于A到D来说&#xf…

WPF未来展望:紧跟技术发展趋势,探索新的可能性

WPF未来展望&#xff1a;紧跟技术发展趋势&#xff0c;探索新的可能性 一、前言二、WPF 与.NET 技术的融合发展2.1 拥抱.NET Core2.2 利用.NET 5 及后续版本的新特性 三、WPF 在新兴技术领域的应用拓展3.1 与云计算的结合3.2 融入物联网生态 四、WPF 在用户体验和设计方面的创新…

低空经济腾飞:无人机送货、空中通勤,未来已来

近年来&#xff0c;低空经济逐渐成为社会关注的焦点。从无人机送货到“空中的士”&#xff0c;再到飞行培训的火热进行&#xff0c;低空经济正迎来前所未有的发展机遇。随着技术进步和政策支持&#xff0c;这一曾经看似遥远的未来场景&#xff0c;正逐步变为现实。 低空经济如何…

QT编译器mingw与msvc区别及环境配置

一.QT编译器mingw与msvc主要区别 二.QT开发环境配置 1. MinGW 配置 安装步骤&#xff1a; 通过 Qt 官方安装器 安装时勾选 MinGW 组件&#xff08;如 Qt 6.7.0 MinGW 64-bit&#xff09;。 确保系统环境变量包含 MinGW 的 bin 目录&#xff08;如 C:\Qt\Tools\mingw1120_64…