多态【C++】

2024-09-02 01:04
文章标签 c++ 多态

本文主要是介绍多态【C++】,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 概念
    • 概念
    • 虚函数
  • 定义及实现
    • 构成条件
    • 虚函数的重写
    • override和final
    • 重载/重定义(隐藏)/重写(覆盖)的区别
  • 抽象类
    • 概念
    • 接口继承和实现继承
  • 多态的原理
    • 虚函数表
  • 多继承关系的虚函数表

概念

概念

通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象调用时会产生处不同的结果

举个例子:假设在12306上买票时,军人会优先买票,普通人是全价买票,学生是半价买票

虚函数

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

class Person
{
public:virtual void BuyTicket() { cout << "买票全价" << endl; }
};

定义及实现

构成条件

多态是在不同继承关系的类对象,调用同一函数,产生了不同的行为

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

  1. 必须通过基类的指针或引用调用虚函数
  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 p;Func(p);Student s;Func(s);return 0;
}

虚函数的重写

虚函数的重写(覆盖):派生类中有一个和基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名称、参数列表完全相同),将此称为派生类的虚函数重写了基类的虚函数

**注意:**在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,也可以构成重写(因为基类被继承的虚函数仍保持虚函数属性),但是写法不规范,不建议使用

虚函数重写的两个例外:

  1. 协变(基类与派生类虚函数返回值不同)

派生类重写虚函数,与基类虚函数返回值类型不同。必须是基类虚函数返回基类的指针或引用,派生类虚函数返回派生类的指针或引用,称为协变(仅作了解,遇到了能看懂即可)

class A{};
class B : public A {};class Person
{
public:virtual A* BuyTicket() { cout << "买票全价" << endl; }
};class Student : public Person
{
public:virtual B* BuyTicket(){cout << "买票半价" << endl;}
};
  1. 析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,因为编译器对析构函数的名称做了特殊处理,编译后析构函数的名统一处理为destructor

析构函数需要设计为虚函数,因为当通过基类指针删除一个派生类对象时,如果基类的析构函数不是虚函数,那么只会调用基类的析构函数,而不会调用派生类的析构函数。而如果当基类的析构函数是虚函数时,删除派生类对象时会先调用派生的析构函数,在调用基类的析构函数

class Base 
{
public:virtual ~Base() // 虚析构函数{cout << "~Base()" << endl;}
};class Derived : public Base 
{
public:virtual ~Derived(){cout << "~Derived()" << endl;}
};int main() {Base* basePtr = new Derived();delete basePtr;  // 正确地调用Derived的析构函数return 0;
}

override和final

这两个关键字都是帮助用户在编译时检测虚函数重写,否则如果有错误,会在运行时报错

  1. override:检查派生类虚函数是否重写了基类某个虚函数
class Person
{
public:virtual void BuyTicket(){ cout << "买票全价" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket(int a) override{cout << "买票半价" << endl;}
};

在这里插入图片描述

  1. final:修饰虚函数,表示该虚函数不能再被重写
class Person
{
public:virtual void BuyTicket() final{ cout << "买票全价" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket(){cout << "买票半价" << endl;}
};

在这里插入图片描述

重载/重定义(隐藏)/重写(覆盖)的区别

在这里插入图片描述

抽象类

概念

在虚函数后协商 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫作抽象类(也叫做接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象

class Car
{
public:virtual void Drive() = 0;
};class Byd : public Car
{
public:virtual void Drive(){cout << "Byd-舒适" << endl;}
};class Xiaomi : public Car
{
public:virtual void Drive(){cout << "Xiaomi-操控" << endl;}
};int main()
{Car* pByd = new Byd;pByd->Drive();Car* pXiaomi = new Xiaomi;pXiaomi->Drive();delete pByd;delete pXiaomi;
}

抽象类的意义:

集合某一类事物的共同特征,例如可以将“动物”作为抽象类,代表所有动物共有的属性和行为。具体的不同种类如“猫”和“狗”则继承自这个抽象类。在这种情况下,“动物”这个抽象类本身无法实例化出具有实际意义的对象,而只有继承自它的具体类(如“猫”、“狗”)实例化出的对象才具有实际意义

接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现(整个函数)虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口**(不包括函数体)**,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数

多态的原理

虚函数表

class Base
{
public:virtual void Func1(){cout << "Func()" << endl;}private:int _b = 1;
};int main()
{Base b;cout << sizeof(b) << endl; // 结果为8return 0;
}

b对象的大小是8字节,通过监视窗口可以看到,除了_b成员,还多一个**_vfptr的指针**(类型是void),这个指针我们叫做虚函数表指针(v代表virtual,f代表function),指针指向的内容是虚函数表,这个数组中存放的是该对象的虚函数的指针通过这个数组中的指针可以找到对应的虚函数,一般这个数组最后会放一个nullptr。一个含有虚函数的类中至少有一个虚函数表指针,因为虚函数的地址要放到虚函数表中,虚函数表也称虚表(注意不要和继承中的虚基表搞混,虚基表中存放的是菱形继承的偏移量)

在这里插入图片描述

我们再来看下面这部分代码

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 = 0;
};class Derive : public Base
{
public:virtual void Func1() override { cout << "Derive::Func1()" << endl; }private:int _d = 0;
};int main()
{Base b;Derive d;return 0;
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

上述代码运行时可以在监视窗口中观察到

  1. 基类b对象和派生类d对象虚表是不一样的,d对象的Func1完成了重写,所以d对象的虚表中存放的是Derive::Func1(),所以虚函数的重写也叫覆盖。重写是语法的叫法,覆盖是原理层的叫法
  2. 派生类虚表的生成:a.现将基类的虚表内容拷贝一份放在派生类虚表中; b.如果派生类重写了某个虚函数用派生类自己的虚函数覆盖虚表中基类的虚函数;c.派生类新增的虚函数按其在派生类中的声明顺序增加到派生类的虚表中

那虚函数和虚表存放在哪里呢

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a = 1;
};void func()
{cout << "void func()" << endl;
}int main()
{Base b1;Base b2;static int a = 0;int b = 0;int* p1 = new int;const char* p2 = "hello world";printf("静态区:%p\n", &a);printf("栈:%p\n", &b);printf("堆:%p\n", p1);printf("代码段:%p\n", p2);printf("虚表:%p\n", *((int*)&b1));printf("虚函数地址:%p\n", &Base::func1);printf("普通函数地址:%p\n", func);return 0;
}

虚函数和虚表的地址

上述代码运行结果可得,虚表和虚函数存放在代码段(对象中存放的是虚表指针)

那为什么不同对象调用虚函数会有不同的行为呢

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 如上图红色箭头所示,当Func()函数中的people指针是mike时,people->BuyTicket在mike的虚表中找到的函数时Person::BuyTicket
  • 如上图蓝色箭头所示,当Func()函数中的people指针是johson时,people->BuyTicket在johson的虚表中找到的函数时Student::BuyTicket
  • 因此实现了不同对象完成统一行为,展现出不同的形态

**注意:**同一类型的对象共用同一虚表

多继承关系的虚函数表

多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表

这篇关于多态【C++】的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1128552

相关文章

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝

C++——stack、queue的实现及deque的介绍

目录 1.stack与queue的实现 1.1stack的实现  1.2 queue的实现 2.重温vector、list、stack、queue的介绍 2.1 STL标准库中stack和queue的底层结构  3.deque的简单介绍 3.1为什么选择deque作为stack和queue的底层默认容器  3.2 STL中对stack与queue的模拟实现 ①stack模拟实现

c++的初始化列表与const成员

初始化列表与const成员 const成员 使用const修饰的类、结构、联合的成员变量,在类对象创建完成前一定要初始化。 不能在构造函数中初始化const成员,因为执行构造函数时,类对象已经创建完成,只有类对象创建完成才能调用成员函数,构造函数虽然特殊但也是成员函数。 在定义const成员时进行初始化,该语法只有在C11语法标准下才支持。 初始化列表 在构造函数小括号后面,主要用于给

2024/9/8 c++ smart

1.通过自己编写的class来实现unique_ptr指针的功能 #include <iostream> using namespace std; template<class T> class unique_ptr { public:         //无参构造函数         unique_ptr();         //有参构造函数         unique_ptr(