【c++随笔13】多态

【c++随笔13】多态

  • 多态性(Polymorphism)在面向对象编程中是一个重要概念,它允许以统一的方式处理不同类型的对象,并在运行时动态确定实际执行的方法或函数。
  • 一、什么是多态性?
    • 1、关键概念:C++的多态性
    • 2、多态定义
    • 3、没有 静态多态、动态多态
  • 二、多态的详细介绍
    • 1、多态的构成条件
    • 2、覆盖(override)——重写
    • 3、多态构成的两个意外
      • 3.1、协变——构成多态
      • 3.2、父虚子非虚——构成多态
    • 4、析构函数的重写
      • 1. 直接定义对象
      • 2. 使用new操作符在堆上创建对象
      • 3、结论:在堆上构建对象,且基类指针指向派生类的情况下,如果不加virtual,会发生内存泄漏,派生类不会析构。
    • 5、final (C++11)
    • 6、override(C++11)
    • 7、重载、覆盖、隐藏的对比
  • 三、抽象类
    • 1、纯虚函数
    • 2、 抽象类(abstract class)
    • 3、抽象类指针
    • 4、- 抽象类实例化?
    • 5、接口继承(Interface Inheritance)和实现继承(Implementation Inheritance)是面向对象编程中的两种继承方式。

原创作者:郑同学的笔记
原创地址:https://zhengjunxue.blog.csdn.net/article/details/131858812
qq技术交流群:921273910

多态性(Polymorphism)在面向对象编程中是一个重要概念,它允许以统一的方式处理不同类型的对象,并在运行时动态确定实际执行的方法或函数。

一、什么是多态性?

1、关键概念:C++的多态性

我们查看《C++ Primer 第5版》第15.3章节 虚函数中的介绍(p537页)

OOP的核心思想是多态性(polymorphism)。多态性这个词源自希腊语,其含义是“多种形式”。我们把具有继承关系的多个类型称为多态类型,因为我们能使用这些类型的“多种形式”而无须在意它们的差异。引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在。


当我们使用基类的引用或指针调用基类中定义的一个函数时,我们并不知道该函数真正作用的对象是什么类型,因为它可能是一个基类的对象也可能是一个派生类的对象。如果该函数是虚函数,则直到运行时才会决定到底执行哪个版本,判断的依据是引用或指针所绑定的对象的真实类型。


另一方面,对非虚函数的调用在编译时进行绑定。类似的,通过对象进行的函数(虚函数或非虚函数)调用也在编译时绑定。对象的类型是确定不变的,我们无论如何都不可能令对象的动态类型与静态类型不一致。因此,通过对象进行的函数调用将在编译时绑定到该对象所属类中的函数版本上。


Note当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。

2、多态定义

我们依然查看《C++ Primer 第5版》第15章节末尾 术语表中的介绍(p576页)

多态性(polymorphism)当用于面向对象编程的范畴时,多态性的含义是指程序能通过引用或指针的动态类型获取类型特定行为的能力。

动态类型(dynamic type)对象在运行时的类型。引用所引对象或者指针所指对象的动态类型可能与该引用或指针的静态类型不同。基类的指针或引用可以指向一个派生类对象。在这样的情况中,静态类型是基类的引用(或指针),而动态类型是派生类的引用(或指针)。

静态类型(static type)对象被定义的类型或表达式产生的类型。静态类型在编译时是已知的。

3、没有 静态多态、动态多态

我们看网上有很多资料介绍动态时,都会提到多态分为静态多态(比如函数重载等)和动态多态,而当我们看了上面书中的定义和介绍后会明白,网上的说法是有问题的。

在c++领域:

  • 只有多态、不区分静态多态和动态多态;
  • 网上说的c++动态多态就是指的c++中的多态;
  • 网上说的静态多态,不符合《C++ Primer 第5版》多态的概念;
  • 静态多态按照《C++ Primer 第5版》中书写demo,无法实现多态;

二、多态的详细介绍

动态多态性是在运行时确定方法或函数的调用,根据实际对象的类型进行动态绑定。这种多态性通过虚函数和基类指针或引用来实现。

简单来说,
多态: 就是多种形态,不同的对象去完成同样的事情会产生不同的结果。
举个例子:就拿购票系统来说,不同的人对于购票这个行为产生的结果就是不同的,学生购票时购买的是半价票,普通人购票的时候购买的是全价票。

1、多态的构成条件

继承中想要构成多态,必须满足以下两个条件:

① 必须是子类的虚函数重写成父类函数(重写:三同 + 虚函数)
② 必须是父类的指针或者引用去调用虚函数。

  • 三同指的是:同函数名、同参数、同返回值。
  • 虚函数:即被 virtual 修饰的类成员函数。
  • 指针调用
#include <iostream>
using namespace std;

class Person {
public:
	Person(const char* name)
		: _name(name) {}

	// 虚函数
	virtual void BuyTicket() {
		cout << _name << ": " << "Person-> 买票   全价 100¥" << endl;
	}

protected:
	string _name;
};

class Student : public Person {
public:
	Student(const char* name)
		: Person(name) {}

	// 虚函数 + 函数名/参数/返回 -> 重写(覆盖)
	virtual void BuyTicket() {
		cout << _name << ": " << "Student-> 买票 半价 50¥" << endl;
	}
};

class Soldier : public Person {
public:
	Soldier(const char* name)
		: Person(name) {}

	// 虚函数 + 函数名/参数/返回 -> 重写(覆盖)
	virtual void BuyTicket() {
		cout << _name << ": " << "Soldier-> 优先买预留票 全价 100¥" << endl;
	}
};

/* 接收身份 */
void Pay(Person* ptr) {
	ptr->BuyTicket();  // 到底是谁在买票,取决于传来的是谁
	delete ptr;
}

int main()
{
	Person* p1 = new Person("小明爸爸");
	Student* stu = new Student("小明");
	Soldier* so = new Soldier("小明爷爷");

	Pay(p1);
	Pay(stu);
	Pay(so);

    return 0;
}

输出

在这里插入图片描述

  • 引用调用
/* 接收身份 */
void Pay(Person& ptr) {
	ptr.BuyTicket();  // 到底是谁在买票,取决于传来的是谁
}

int main()
{
	Person p1("小明爸爸");
	Student stu("小明");
	Soldier so("小明爷爷");

	Pay(p1);
	Pay(stu);
	Pay(so);

	return 0;
}

2、覆盖(override)——重写

我们依然查看《C++ Primer 第5版》第15章节末尾 术语表中的介绍(p576页)

  • 覆盖(override)派生类中定义的虚函数如果与基类中定义的同名虚函数有相同的形参列表,则派生类版本将覆盖基类的版本。

覆盖也被有的文章叫做”重写“。用 virtual 虚函数,并且做到函数名、参数和返回值相同,就能够达到 “重写” 的效果:

重写是为了将一个已有的事物进行某些改变以适应新的要求。
重写是子类对父类的允许访问的方法的实现过程进行重新编写,返回值和形参都不能改变。 即:“外壳不变,核心重写。”

3、多态构成的两个意外

刚才说了,三同+虚函数,就能达到重写的效果(也就是多态)。但是,还有两个意外,也能达成多态的效果。

3.1、协变——构成多态

  • C++中的协变(Covariance)指的是派生类可以返回基类中相同函数签名的返回类型的子类型。

  • 在C++中,当一个虚函数在基类中使用了virtual关键字声明为虚函数时,派生类可以对该虚函数进行重写,并且在派生类中返回类型可以是基类返回类型的子类型。这种返回类型的子类型关系称为协变。

协变的类型必须是父子关系。

观察下面的代码,并没有达到 “三同” 的标准,它的返回值是不同的,但依旧构成多态:

class A {};
class B : public A {};
 
class Person {
public:
	virtual A* f() {
		cout << "virtual A* Person::f()" << endl;
		return nullptr;
	}
};
 
class Student : public Person {
public:
	virtual B* f() {
		cout << "virtual B* Student:::f()" << endl;
		return nullptr;
	};
};
 
int main(void)
{
	Person p;
	Student s;
	Person* ptr = &p;
	ptr->f();
 
	ptr = &s;
	ptr->f();
 
	return 0;
}

输出

在这里插入图片描述

当class A、class B是父子关系时,就不能协变:

3.2、父虚子非虚——构成多态

现在来讲第二个例外。

  • 父类的虚函数没了无法构成多态:
  • 但是,子类的虚函数没了却能构成多态:
#include <iostream>
using namespace std;


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

class Person {
public:
	virtual A* f() {
		cout << "virtual A* Person::f()" << endl;
		return nullptr;
	}
};

class Student : public Person {
public:
	B* f() {
		cout << "virtual B* Student:::f()" << endl;
		return nullptr;
	};
};

int main(void)
{
	Person p;
	Student s;
	Person* ptr = &p;
	ptr->f();

	ptr = &s;
	ptr->f();

	return 0;
}

输出

在这里插入图片描述

4、析构函数的重写

1. 直接定义对象

#include <iostream>
using namespace std;

class Person {
public:
	~Person() {  //不加virtual
	// virtual ~Person() { //加virtual
		cout << "~Person()" << endl;
	}
};

class Student : public Person {
public:
	~Student() {
		cout << "~Student()" << endl;
	}
};

int main(void)
{
	Person p;
	Student s;

	return 0;
}
  • 加virtual输出
    在这里插入图片描述

  • 不加virtual输出
    在这里插入图片描述

2. 使用new操作符在堆上创建对象

#include <iostream>
using namespace std;

class Person {
public:
	~Person() {  //不加virtual
	//virtual ~Person() { //加virtual
		cout << "~Person()" << endl;
	}
};

class Student : public Person {
public:
	~Student() {
		cout << "~Student()" << endl;
	}
};

int main(void)
{
	cout << "=================不加virtual======================\n";
	Person *ptr = new Person();
	delete ptr;

	cout << "=======================================\n";
	Student *ptr2 = new Student();
	delete ptr2;

	cout << "=======================================\n";
	Person *ptr3 = new Student();
	delete ptr3;

	return 0;
}

  • 不加virtual
    在这里插入图片描述

  • 加virtual
    在这里插入图片描述

刚才我们看到了,如果这里不加 virtual,~Student 是没有调用析构的。
这其实是非常致命的,是不经意间会发生的内存泄露。

3、结论:在堆上构建对象,且基类指针指向派生类的情况下,如果不加virtual,会发生内存泄漏,派生类不会析构。

5、final (C++11)

在C++中,final是一个关键字,用于修饰类、函数或虚函数,具有不同的作用。

  1. 修饰类:使用final关键字修饰类时,表示该类是最终类,不能被其他类继承。例如:
class Base final {
    // ...
};

class Derived : public Base {  // 错误,Derived不能继承自final类Base
    // ...
};

在上述示例中,Base类被声明为final,因此Derived类不能继承自Base类。

  1. 修饰函数:使用final关键字修饰成员函数时,表示该函数是最终版本,不能被派生类重写。例如:
class Base {
public:
    virtual void func() final {
        // ...
    }
};

class Derived : public Base {
public:
    void func() override {  // 错误,无法重写被声明为final的函数
        // ...
    }
};

在上述示例中,Base类中的func()函数被声明为final,因此Derived类无法对其进行重写。

  1. 修饰虚函数:与修饰普通成员函数类似,使用final关键字修饰虚函数时,表示该虚函数是最终版本,不能被派生类重写。例如:
class Base {
public:
    virtual void func() final {
        // ...
    }
};

class Derived : public Base {
public:
    void func() override {  // 错误,无法重写被声明为final的虚函数
        // ...
    }
};

在上述示例中,Base类中的虚函数func()被声明为final,因此Derived类无法对其进行重写。

通过使用final关键字,可以显式地阻止类、函数或虚函数被继承、重写或覆盖,从而提高程序的安全性和可靠性。

6、override(C++11)

override是C++11引入的关键字,用于显式地标记派生类中对基类虚函数的重写。它的主要作用是增加代码的可读性和可维护性,并提供编译器的静态检查,避免错误的重写行为。

在C++中,当派生类要重写基类的虚函数时,可以使用override关键字进行标记。通过使用override关键字,可以确保派生类的函数签名与基类的虚函数完全匹配,否则编译器会发出错误。这有助于及时发现错误的重写行为。

以下是使用override关键字的示例:

class Base {
public:
    virtual void func() const {
        // ...
    }
};

class Derived : public Base {
public:
    void func() const override {
        // ...
    }
};

在上述示例中,Base类中的虚函数func()被定义为virtual void func() const,而在Derived类中,重写的函数也被定义为void func() const,并使用override关键字进行标记。如果Derived类的函数签名与基类的虚函数不匹配,或者没有正确使用override关键字,编译器将会报错。

7、重载、覆盖、隐藏的对比

在这里插入图片描述

三、抽象类

1、纯虚函数

我们依然查看《C++ Primer 第5版》第15章节末尾 术语表中的介绍(p576页)

  • 纯虚函数(pure virtual)在类的内部声明虚函数时,在分号之前使用了=0。一个纯虚函数不需要(但是可以)被定义。含有纯虚函数的类是抽象基类。如果派生类没有对继承而来的纯虚函数定义自己的版本,则该派生类也是抽象的。

纯虚函数是通过在函数声明后面加上= 0来声明的,表示该函数没有实现,派生类必须重写它。

virtual void pureVirtualFunction() = 0;

在上述示例中,纯虚函数pureVirtualFunction()。

  • 纯虚函数是否可以实现?
    纯虚函数也是可以实现的:
/* 抽象类 */
class Car {
public:
    // 实现没有价值,因为压根没有对象会调用它
	virtual void Drive() = 0 {      // 纯虚函数
		cout << "Drive()" << endl;   
	}
};

2、 抽象类(abstract class)

  • 包含纯虚函数的类,就是 抽象类(abstract class),也叫接口类。
class AbstractClass {
public:
    virtual void pureVirtualFunction() = 0;
};

在上述示例中,AbstractClass是一个抽象类,它具有一个纯虚函数pureVirtualFunction()。派生类必须重写这个函数。

抽象类可以包含纯虚函数(没有实现)和带有实现的函数

3、抽象类指针

虽然父类是抽象类不能定义对象,但是可以定义指针。

定义指针时如果 new 父类对象因为是纯虚函数,自然是 new 不出来的,但是可以 new 子类对象:

#include <iostream>
using namespace std;


/* 抽象类 */
class Car {
public:
	virtual void Drive() = 0;
};

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

int main(void)
{
	Car* pBenz1 = new Benz;
	pBenz1->Drive();

	Benz* pBenz2 = new Benz;
	pBenz2->Drive();

	return 0;
}

4、- 抽象类实例化?

抽象类不能实例化出对象,子类即使在继承后也不能实例化出对象,除非子类重写。

5、接口继承(Interface Inheritance)和实现继承(Implementation Inheritance)是面向对象编程中的两种继承方式。

接口继承指的是一个类从一个或多个接口中继承方法声明,但并不继承这些方法的具体实现。接口只包含纯虚函数(在C++中使用纯虚函数定义接口)或者抽象方法(在其他语言中)。通过接口继承,一个类可以实现多个接口,从而表达出它具备了多个行为或功能。

实现继承指的是子类从父类中继承方法声明和实现。实现继承建立了类的层次结构,允许子类继承并重用父类的代码。子类可以通过继承父类的属性和方法,并且可以根据需要添加新的属性和方法,甚至可以重写父类的方法来改变其行为。

  • 普通函数的继承是一种实现继承,子类继承了父类函数,可以使用函数,继承的是函数的实现。
  • 虚函数的继承是一种接口继承,子类继承的是父类虚函数的接口,目的是为了重写,
  • 达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
  • 出现虚函数就是为了提醒你重写的,以实现多态。如果虚函数不重写,那写成虚函数就没价值了。

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

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

相关文章

【带头学C++】----- 七、链表 ---- 7.1 链表的概述

目录 七、链表 7.1 链表的是什么&#xff1f; 7.2数组和链表的优点和缺点 7.3 链表概述 ​编辑 7.4 设计静态链表 7.4.1 定义一个结点&#xff08;结构体&#xff09; 7.4.2 使用头结点构建一个单向链表 七、链表 7.1 链表的是什么&#xff1f; C链表是一种数据结构&a…

3-docker安装centos7

CentOS7.9下安装完成docker后&#xff0c;后续我们可以在其上安装centos7系统。具体操作如下&#xff1a; 1.以root用户登录CentOS7.9服务器&#xff0c;拉取centos7 images 命令&#xff1a; docker pull centos:centos7 2.加载centos7 images并登录验证 命令&#xff1a;…

Codeforces Round 910 (Div. 2)(D~F)

1898D - Absolute Beauty 题意&#xff1a;给定长度为n的数组a和b&#xff0c;定义b数组的价值为&#xff0c;现可以交换一次b数组中的任意两个元素&#xff0c;求b数组的价值最大值。 思路&#xff1a;绝对值问题可以放在数轴上去解决。绝对值即为区间长度 观察上述三种情况&…

Appium自动化测试:通过appium的inspector功能无法启动app的原因

在打开appium-desktop程序&#xff0c;点击inspector功能&#xff0c;填写app的配置信息&#xff0c;启动服务提示如下&#xff1a; 报错信息&#xff1a; An unknown server-side error occurred while processing the command. Original error: Cannot start the cc.knowyo…

C/C++统计数 2021年12月电子学会青少年软件编程(C/C++)等级考试一级真题答案解析

目录 C/C统计数 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、程序说明 五、运行结果 六、考点分析 C/C统计数 2021年12月 C/C编程等级考试一级编程题 一、题目要求 1、编程实现 给定一个数的序列S&#xff0c;以及一个区间[L, R], 求序列…

环境配置|GitHub——解决Github无法显示图片以及README无法显示图片

一、问题背景 最近在整理之前写过的实验、项目&#xff0c;打算把这些东西写成blog&#xff0c;并把工程文件整理上传到Github上。但在上传README文件的时候&#xff0c;发现github无法显示README中的图片&#xff0c;如下图所示&#xff1a; 在README中该图片路径为&#xff1…

【LeetCode刷题日志】232.用栈实现队列

&#x1f388;个人主页&#xff1a;库库的里昂 &#x1f390;C/C领域新星创作者 &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏✨收录专栏&#xff1a;LeetCode 刷题日志&#x1f91d;希望作者的文章能对你有所帮助&#xff0c;有不足的地方请在评论区留言指正&#xff0c;…

算法——动态规划(新)

什么是动态规划&#xff1f; 动态规划算法的基本思想-求解步骤-基本要素和一些经典的动态规划问题【干货】-CSDN博客 一、三步问题 面试题 08.01. 三步问题 - 力扣&#xff08;LeetCode&#xff09; 思路 我们要知道&#xff0c;走楼梯&#xff0c;前三个阶梯步数已经知道&…

Git分支详解

文章目录 一、分支1.1 查看本地分支1.2 创建本地分支1.3 切换分支&#xff08;checkout&#xff09;1.1 合并分支&#xff08;merge&#xff09;1.1 删除分支 二、解决冲突三、实际开发中分支使用原则和流程四、案例&#xff1a;创建并切换到dev01分支&#xff0c;在dev01分支提…

计算机网络期末复习(知识点)

一、计算机网络体系结构 计算机网络&因特网&#xff1a; 计算机网络定义&#xff1a;将地理位置不同的具有独立功能的多台计算机及其外部设备&#xff0c;通过通信线路连接起来&#xff0c;在网络操作系统&#xff0c;网络关联软件及网络协议的管理和协调下&#xff0c;实…

软件测试技术之地图导航的测试用例

外观测试 屏幕显示不能有花屏、黑点和闪屏&#xff0c;清晰度、亮度、颜色要正常。 检测所有按键都能起到相应作用&#xff0c;是否手感不良。 UI显示状态、颜色、清晰度、效果。 控制&#xff1a;放大&#xff0c;缩小&#xff0c;音量调节功能测试。 交叉路口查询测试&am…

AIGC实战 - 使用变分自编码器生成面部图像

AIGC实战 - 使用变分自编码器生成面部图像 0. 前言1. 数据集分析2. 训练变分自编码器2.1 变分自编码器架构2.2 变分自编码器分析 3. 生成新的面部图像4. 潜空间算术5. 人脸变换小结系列链接 0. 前言 在自编码器和变分自编码器上&#xff0c;我们都仅使用具有两个维度的潜空间。…

【算法挨揍日记】day28——413. 等差数列划分、978. 最长湍流子数组

413. 等差数列划分 413. 等差数列划分 题目描述&#xff1a; 如果一个数列 至少有三个元素 &#xff0c;并且任意两个相邻元素之差相同&#xff0c;则称该数列为等差数列。 例如&#xff0c;[1,3,5,7,9]、[7,7,7,7] 和 [3,-1,-5,-9] 都是等差数列。 给你一个整数数组 nums…

性能测试学习——项目环境搭建和Jmete学习二

项目环境搭建、Jmeter学习二 环境的部署虚拟机的安装虚拟机中添加项目操作步骤 使用环境的注意事项Jmeter的安装和简单使用Jemter的使用的进阶Jemter元件 Jmeter属性执行顺序和作用域作用域以自定义用户变量和用户参数(前置处理器)为例如何解决用户变量和线程组同级时&#xff…

Node.js之TCP(net)

Hi I’m Shendi Node.js之TCP&#xff08;net&#xff09; 最近使用Nodejs编写程序&#xff0c;需要用到自己编写的分布式工具&#xff0c;于是需要将Java版的用NodeJs重新写一遍&#xff0c;需要使用到TCP通信&#xff0c;于是在这里记录下Node.js TCP 的使用方法 依赖 需要使…

c语言-输入输出详解

文章目录 格式化输入输出占位符printfscanf 字符串输入输出puts&#xff08;&#xff09;gets&#xff08;&#xff09; 字符输入输出putchar&#xff08;&#xff09;getchar&#xff08;&#xff09; 区别 格式化输入输出 输入输出的库函数的头文件&#xff1a; #include<…

动态规划专项---最长上升子序列模型

文章目录 怪盗基德的滑翔翼登山合唱队形友好城市最大上升子序列和拦截导弹导弹防御系统最长公共上升子序列 一、怪盗基德的滑翔翼OJ链接 本题思路:本题是上升子序列模型中比较简单的模型&#xff0c;分别是从前往后和从后往前走一遍LIS即可。 #include <bits/stdc.h>co…

Linux——编译器gcc/g++、调试器gdb以及自动化构建工具makefilemake详解

编译器—gcc/g、调试器—gdb以及自动化构建工具—makefile&&make 文章目录 编译器—gcc/g、调试器—gdb以及自动化构建工具—makefile&&make1. 编译器——gcc/g1.1 生成可执行文件与修改默认可执行文件1.2 程序的翻译过程以及对应的gcc选项1.2.1 预处理 gcc -E…

边缘计算是如何为元宇宙提供动力的?

构建元宇宙虚拟世界并不简单&#xff0c;也并不便宜&#xff0c;但是还是有许多大型公司正在转移大量资源来开发他们的元宇宙业务&#xff0c;当然大部分企业注意力都围绕着 VR 耳机、AR 眼镜、触觉手套和其他沉浸式虚拟现实体验所需的可穿戴硬件。虽然这种沉浸式的体验是最终结…

【Django使用】django经验md文档10大模块。第4期:Django数据库增删改查

Django的主要目的是简便、快速的开发数据库驱动的网站。它强调代码复用&#xff0c;多个组件可以很方便的以"插件"形式服务于整个框架&#xff0c;Django有许多功能强大的第三方插件&#xff0c;你甚至可以很方便的开发出自己的工具包。这使得Django具有很强的可扩展…