C++学习记录——십팔 多态

news/2025/1/16 21:02:15/

文章目录

  • 1、了解
  • 2、多态的条件
    • 1、析构函数重写
    • 2、协变
    • 3、关键字final和override
    • 4、重写(覆盖)、重载、隐藏(重定义)对比
  • 3、抽象类
  • 4、多态的原理
    • 1、虚函数表
    • 2、原理
    • 为什么父类指针或者引用可以,但实例化出的对象不可以形成多态?
    • 3、打印虚表
    • 4、多继承
    • 5、动静态绑定
    • 6、其它


1、了解

多态,就是多种形态,不同的对象去完成某个行为时会有不同的状态/结果。

在函数类型前加上virtual就表示这是虚函数

class Person
{
public:virtual void Buy() { cout << "买票-全价" << endl; }
};class Student : public Person
{
public://发生了重写/覆盖virtual void Buy() { cout << "买票-半价" << endl; }
};void Func(Person& p)
{p.Buy();
}int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}

结果就是一个全价一个半价,虽然发生了覆盖,但是不影响最终的结果,传哪个类的对象就打印什么。

多态的条件就是虚函数的重写和父类的指针或者引用去调用

刚才的代码,如果去掉引用或者重写或者两个都去掉,那就2个全价

这样的场景

class Person {
public:~Person() { cout << "~Person()" << endl; }
};class Student : public Person {
public:~Student() { cout << "~Student()" << endl; }
};int main()
{Person* ptr1 = new Person;Person* ptr2 = new Student;delete ptr1;delete ptr2;return 0;
}

在这里插入图片描述

ptr1释放时调用父类析构,但是ptr2也调用了父类析构,这是因为出现了隐藏关系,这时候多态就能派上用场了。

2、多态的条件

虚函数的重写——函数名、参数、返回值都相同
父类指针或者引用去调用

如果不满足多态,就看调用者的类型,调用这个类型的成员函数
满足多态,就看指向的对象的类型,调用这个类型的成员函数

class Person
{
public:void Buy() { cout << "买票-全价" << endl; }
};class Student : public Person
{
public:virtual void Buy() { cout << "买票-半价" << endl; }
};void Func(Person& p)
{p.Buy();
}

像这样父类没有virtal,不是多态,是隐藏,只不过子类对象去调用才能体现出隐藏。

	Student st;st.Buy();

如果父类函数有virtual,但是子类没有,那就输出一个全价和一个半价,但是这里仍然是多态。

子类可以不写,父类写了,编译器会认为子类重写了这个虚函数,这叫接口继承,子类会继承父类的函数,但是里面的实现会重写成子类的。

1、析构函数重写

在这里插入图片描述

在这里插入图片描述

func里面有父类指针指向了一个子类对象,这个条件满足,所以要加上另一个条件,把父类析构也写上virtual就可以形成多态了。

在这里插入图片描述

这里还可以这样理解,析构函数名字虽然不同,但编译器把它们统一变成了destructor

2、协变

虚函数的重写有三个不同,有一个例外就是返回值不同。但不能无脑不同,它要求必须是父子关系的指针或者引用。

class Person
{
public:virtual Person* Buy(){cout << "买票-全价" << endl;return this;}};class Student : public Person
{
public:virtual Student* Buy(){cout << "买票-半价" << endl;return this;}
};

它可以不只是现有类的指针,比如说在这之前定义A父类和B子类,就可以用A和B的指针。返回值类型也得对应,返回类对象就不行。

虚函数重写的例外有两个,一个是接口继承,一个是协变。

3、关键字final和override

final

修饰虚函数后这个虚函数不能被重写

class Car
{
public:virtual void Drive() final {}
};class Benz : public Car
{
public:virtual void Drive() { cout << "Benz-舒适" << endl; }
};int main()
{return 0;
}

override

检查是否完成重写,不是就报错

class Car
{
public:virtual void Drive() {}
};class Benz : public Car
{
public:virtual void Drive() override { cout << "Benz-舒适" << endl; }
};int main()
{return 0;
}

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

重载:两个函数在同一作用域;函数名相同,参数不同

重写:两个函数分别在基类和派生类的作用域;函数名/参数/返回值都必须相同(协变例外);两个函数必须是虚函数

重定义:两个函数分别在基类和派生类的作用域;函数名相同;两个基类和派生类的同名函数不构成重写就是重定义

3、抽象类

在虚函数后写上=0,这个函数就是纯虚函数,包含纯虚函数的类是抽象类,这种类不能实例化出对象。

在这里插入图片描述

在这里插入图片描述

一个类继承抽象类后也是抽象类,因为包含纯虚函数。如果重写了纯虚函数,子类的那个函数就不是纯虚函数,这时候子类就可以实例化对象了。

4、多态的原理

1、虚函数表

class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;char _ch;
};int main()
{cout << sizeof(Base) << endl;Base bb;return 0;
}

结果是12.两个变量占了8个字节,但是多出来的4是什么?

在这里插入图片描述

多出来的这个指针就是虚函数表指针。

在这里插入图片描述

2、原理

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 Mike;Func(Mike);Student Johnson;Func(Johnson);return 0;
}

在这里插入图片描述

func函数里,传父类对象,M就会有父类的虚函数,传子类对象,就会有子类的虚函数。父类虚表存父类虚函数,子类虚表存子类虚函数。如果不构成多态,那就什么类就用什么类的虚函数;如果是多态,就去指向的对象的虚表里去找。所以p这个指针看的只是虚表,它并不知道是哪个类对象。

为什么虚函数表不只放在类对象里,而是要找单独一个区域去放虚函数表?因为可能有多个虚函数。虚函数表本质是一个虚函数指针数组 ,如果有多个虚函数,就会有多个虚函数表。发生了重写,虚函数表就会被覆盖,没有就不会覆盖。虚函数表是在编译时就准备好的。虚函数表按声明顺序来确定数组下标的。

为什么父类指针或者引用可以,但实例化出的对象不可以形成多态?

父类指针或者引用可以指向虚函数表,子类会把父类的那部分切出来,让它指向对象,对应的还是子类的虚函数表。如果是对象,父类对象没问题,但如果是子类对象,那么会发生拷贝,把父类的内容拷贝到子类,那虚表会拷贝呢?不会,这个风险大,拷贝了可能很多映射关系就乱了。

---------------------------------------------------------------------------------------------------------------

虚函数和普通函数一样,存在代码段。

在main栈帧里,类实例化了对象,对象有一个指针,指向虚函数表,表里存的就是虚函数的地址。

3、打印虚表

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

如果在子类中写一个父类没有的函数,前面加上virtual,这时候如果调用监视窗口会发现它不在虚表,但是真的不在吗?内存窗口也不一定能看得出来,这样的话就得打印虚表来看了。

typedef void(*VF_PTR)();//函数指针的typedef需要把新定义的名字放在括号里,所以VF_PTR是void(*)()
void PrintVFTable(VF_PTR table[])
{for (int i = 0; table[i] != nullptr; ++i){printf("[%d]:%p\n", i, table[i]);}cout << endl;
}int main()
{Base b;Derive d;//要找到这个虚表,根据之前所写,虚表指针在这个对象的前4/8个字节PrintVFTable((VF_PTR*)(*(int*)&b));PrintVFTable((VF_PTR*)(*(int*)&d));return 0;
}

我们可以把类对象的地址强制转为int*类型,再解引用就得到了地址的前4个字节,然后把这个结果再转成VF_PTR类型传过去就好了。

在这里插入图片描述

另外的写法就是二级指针

	PrintVFTable((*(VF_PTR**)&b));PrintVFTable((*(VF_PTR**)&d));

但是第一种只能在32位下走,第二种两个都行。第一种int改成longlong就可以适应64位。

补全打印虚表函数

void PrintVFTable(VF_PTR* table)
{for (int i = 0; table[i] != nullptr; ++i){printf("[%d]:%p->", i, table[i]);VF_PTR f = table[i];f();}cout << endl;
}

就可以打印出来两个对象的虚表。func4也在其中。

虚表是编译阶段生成的,对象中虚表指针是在构造函数的初始化列表初始化的,虚表存在哪里?

可以通过这个方法来看。

	int x = 0;static int y = 0;//静态区int* z = new int;const char* p = "asdasd asda";//常量区printf("栈对象: %p\n", &x);printf("堆对象: %p\n", z);printf("静态区对象: %p\n", &y);printf("常量区对象: %p\n", p);printf("b对象虚表: %p\n", *((int*)&b));printf("d对象虚表: %p\n", *((int*)&b));

在这里插入图片描述

离常量区近,应当是在常量区,不过也有编译器放在静态区。

4、多继承

class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1;
};class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2;
};class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1;
};int main()
{Derive d;return 0;
}

d有两张虚表,base1和base2,func1发生了重写,两个虚表都有各自的func2,func3在哪里?我们可以打印虚表,但是有两个虚表,base1放在前头,所以按照之前的办法就只打印了base1,2没打印,可以用+sizeof(Base1)或者用偏移来打印base2.

	//PrintVFTable((VF_PTR*)(*(int*)((char*)&d + sizeof(Base1))));Base2* ptr2 = &d;PrintVFTable((VF_PTR*)(*(int*)(ptr2)));

ptr2发生了指针的偏移,正好就指向了Base2的虚表。最后结果就是放在第一个虚表中。

上面的代码中,func1重写了,但是地址不一样,但它们确实重写了。

	Base1* ptr1 = &d;Base2* ptr2 = &d;ptr1->func1();ptr2->func1();

反汇编代码中,ptr1->func1()call到了Base1的虚表地址,call后是一个jmp指令,jmp后面括号里的就是函数真实的地址,然后开始建立栈帧等;ptr2->func1()call的地址就不一样,jmp后函数的地址也不一样,并且在这次jmp后接下来的反汇编出现了一句sub,一句jmp,然后再接一句jmp,才到真正执行的函数的地址。它之所以这样走,是因为里面有一个sub,那行代码后还有一个ecx,ecx就是this指针;ptr1调用的func是子类的函数,ptr1指向对象的开始,ecx就指向对象的开始,所以不需要另外的处理;但是ptr2并不是这样,此时this并没有指向对象的开始,所以多出来的步骤是为了修正this指针。

5、动静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,
    比如:函数重载,cin, cout
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体
    行为,调用具体的函数,也称为动态多态。
  3. 本小节之前(5.2小节)买票的汇编代码很好的解释了什么是静态(编译器)绑定和动态(运行时)绑
    定。

6、其它

class A
{
public:virtual void func(){}
public:int _a;
};class B : virtual public A
{
public:virtual void func(){}
public:int _b;
};class C : virtual public A
{
public:virtual void func(){}
public:int _c;
};class D : public B, public C
{
public:int _d;
};int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

ABC都有函数,如果D不写,就会警告,说A的func函数重写不明确。A的虚表应当放B还是C重写的函数?无法确定,这时候就需要D去完成最终的重写。初始化也一样,D里面写了代码,BCA都对A初始化了,但是A用自己的初始化,如果没写A对自己的初始化,那就报错。

如果BC有新增的虚函数,不会放进A的虚表。不看A的话,D继承BC,D应当有两个虚表,那再加上A,就有了A的公共的虚表,所以D有三个虚表,一张A的虚表(指向虚函数表,里面存放函数指针),两张虚基表(指向存有偏移量的表)。

结束。


http://www.ppmy.cn/news/37839.html

相关文章

全网最详细的UI自动化测试元素定位方法总结

目录 前言 元素定位概述 常用的元素定位器 元素定位方法 元素定位技巧 总结 前言 UI自动化测试是软件测试中的一个重要环节&#xff0c;它可以通过模拟用户的实际操作&#xff0c;自动化执行UI界面上的测试用例&#xff0c;以提高测试效率和准确性。元素定位是UI自动化测…

vue尚品汇商城项目-day02【15.动态展示三级菜单联动】

文章目录15.动态展示三级菜单联动15.1动态调用展示三级菜单步骤15.2完成一级菜单鼠标划入显示背景色15.3控制二三级商品分类的显示与隐藏15.4演示卡顿现象引入防抖与节流15.5三级联动组件的路透跳转与传递参数本人其他相关文章链接15.动态展示三级菜单联动 问题1&#xff1a; 代…

Java设计模式(六)桥接模式

结构型模式&#xff0c;共七种&#xff1a;适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。 这种模式涉及到一个作为桥接的接口&#xff0c;使得实体类的功能独立于接口实现类。这两种类型的类可被结构化改变而互不影响。 它的主要特点是把抽象…

如何使用ChatGPT写论文?

使用ChatGPT写论文详细操作步骤 说明ChatGPT是一款ai产品&#xff0c;尽管它非常强大&#xff0c;对我们来说本质上也仅仅是一个辅助工具&#xff0c;切勿让AI完全取代我们的思考能力。目前的ChatGPT写论文还不能一步到位&#xff0c;只能通过一些技巧来完成整篇论文。使用Cha…

centos7在docker上安装es(elasticsearch)

因为需要部署kibana容器&#xff0c;因此需要让es和kibana容器互联 1.创建网络 docker network create es-net 2.将es的tar文件拉取到虚拟机中&#xff08;因为es文件较大&#xff0c;不建议直接使用docker进行拉取&#xff09; 没有es.tar文件的可以下载&#xff1a; 链接…

C++环境设置

本地环境设置 如果您想要设置 C 语言环境&#xff0c;您需要确保电脑上有以下两款可用的软件&#xff0c;文本编辑器和 C 编译器。 文本编辑器 这将用于输入您的程序。文本编辑器包括 Windows Notepad、OS Edit command、Brief、Epsilon、EMACS 和 vim/vi。 文本编辑器的名…

信息学奥赛一本通 1384:珍珠(bead)

【题目链接】 ybt 1384&#xff1a;珍珠(bead) 【题目考点】 1. 图论&#xff1a;floyd 求传递闭包 传递闭包&#xff1a;二维数组e&#xff0c;e[i][j]表示顶点i到顶点j是否有路径。 【解题思路】 这是个有向图。每颗珍珠是一个顶点&#xff0c;初始情况下&#xff0c;如…

为社会开发,无障碍开发,开发人员的公益时间

无障碍开发让每一个人受益无障碍开发让每一个人受益无障碍开发的重要性无障碍开发案例无障碍小助手百度无障碍开放平台Apple Watch 的无障碍功能Google 的无障碍开发指南微软的无障碍开发工具结论无障碍开发让每一个人受益 无障碍开发是指开发人员在设计和开发软件时&#xff…