【c++随笔13】多态

news2024/11/14 3:05:57

【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++中使用纯虚函数定义接口)或者抽象方法(在其他语言中)。通过接口继承,一个类可以实现多个接口,从而表达出它具备了多个行为或功能。

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

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

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

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

相关文章

构造函数,原型对象,实例对象

1.构造函数、原型对象、实例对象三者分别是什么&#xff1f; 构造函数&#xff1a;用来创建对象的函数&#xff0c;创建实例对象的模板 。构造函数的函数名尽量首字母大写(为了区分普通函数和构造函数)原型对象&#xff1a;每一个函数在创建的时候&#xff0c;系统都会给分配一…

【Android Jetpack】DataStore的介绍

DataStore Jetpack DataStore是一种数据存储解决方案&#xff0c;允许您使用协议缓冲区存储键值对或类型化对象。DataStore使用Kotlin协程和Flow以异步、一致的事务方式存储数据。 注意&#xff1a;如果您需要支持大型或复杂数据集、部分更新或参照完整性&#xff0c;请考虑使…

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

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

【wp】2023第七届HECTF信息安全挑战赛 Web

伪装者 考点&#xff1a;http协议flask的session伪造ssrf读取文件 首先根据题目要求就行伪造HTTP 这里不多说&#xff0c;比较基础 然后下面得到是个登入 页面&#xff0c;我们输入zxk1ing 得到 说什么要白马王子 &#xff0c;一眼session伪造 看到ey开头感觉是jwt 输入看看 得…

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

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

深度学习中的图像融合:图像融合论文阅读与实战

个人博客:Sekyoro的博客小屋 个人网站:Proanimer的个人网站 abs 介绍图像融合概念&#xff0c;回顾sota模型&#xff0c;其中包括数字摄像图像融合&#xff0c;多模态图像融合&#xff0c; 接着评估一些代表方法 介绍一些常见应用&#xff0c;比如RGBT目标跟踪&#xff0c;…

PgSQL技术内幕-Bitmap Index Scan

PgSQL技术内幕-Bitmap Index Scan 1、简介 Bitmap索引扫描是对索引扫描的一个优化&#xff0c;通过建立位图的方式将原来的随机堆表访问转换成顺序堆表访问。主要分为两点&#xff1a;1&#xff09;管理每个Bitmap的hash slot没用完时&#xff0c;每个Bitmap代表每个heap页中满…

LeetCode【92】翻转链表II

题目&#xff1a; 思路&#xff1a; https://blog.csdn.net/DarkAndGrey/article/details/122146216 public ListNode reverseBetween(ListNode head, int left, int right) {if(head null || right left){return head;// 头节点为null&#xff0c;链表为空&#xff0c;反…

340条样本就能让GPT-4崩溃,输出有害内容高达95%?OpenAI的安全防护措施再次失效

仅需340个示例微调GPT-4&#xff0c;即可绕过安全限制&#xff0c;让模型说出“枪支改装方法”、“生化武器制作过程”等有害内容&#xff1f; OpenAI的安全防护措施再次失效&#xff0c;攻击的成功率高达95%&#xff01; 近日&#xff0c;美国顶尖大学UIUC与斯坦福联合对GPT…

华为麒麟服务器--硬盘问题

记录以下今天处理的服务器&#xff1a; 情况说明&#xff1a;linux 系统&#xff0c;不知道什么原因系统就突然不能用了&#xff08;据说是前段时间断电来着&#xff0c;但是机房有应急电源&#xff09;。 系统环境&#xff1a; 服务器&#xff1a;华为RH2288H V3 服务器 服…

(数据库管理系统)DBMS与(数据库系统)DBS的区别

数据库管理系统&#xff08;DBMS&#xff0c;Database Management System&#xff09;和数据库系统&#xff08;DBS&#xff0c;Database System&#xff09;是两个相关但不同的概念。 DBS是一个更广泛的概念&#xff0c;指的是计算机系统引入数据库后的系统&#xff0c;包括数…

vscode设置代码模板

一键生成vue3模板代码 效果演示 输入vue3 显示快捷键 按回车键 一键生成自定义模板 实现方法 进入用户代码片段设置 选择片段语言 vue.json输入自定义的代码片段 prefix是触发的内容&#xff0c;按自己的喜好来就行&#xff1b; body是模板代码&#xff0c;写入自己需要的…

SpringBoot常见注解

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a;每天一个知识点 ✨特色专栏&#xff1a…

leetcode刷题日志-68.文本左右对齐

给定一个单词数组 words 和一个长度 maxWidth &#xff0c;重新排版单词&#xff0c;使其成为每行恰好有 maxWidth 个字符&#xff0c;且左右两端对齐的文本。 你应该使用 “贪心算法” 来放置给定的单词&#xff1b;也就是说&#xff0c;尽可能多地往每行中放置单词。必要时可…

基于截图页面生成前端项目

前两天&#xff0c;在群里看见一个视频&#xff0c;视频中&#xff0c;作者截图twitter首页&#xff0c;然后使用截图直接生成与截图布局非常相近的前端项目&#xff0c;效果还是比较惊艳的。 今天陪老婆回老家&#xff0c;路上clone这个项目的代码到本地&#xff0c;学习了一下…

IDEA自动注解设置(中文版)

IDEA自动注解设置 1、添加类自动注释 文件 - 设置 - 编辑器 - 文件和代码模板 - Include - File Header /** *description&#xff1a;TODO *author&#xff1a; ${USER} *create&#xff1a; ${DATE} ${TIME} */2、添加类方法自动注释 文件 - 设置 - 编辑器 - 实时模版 - …

ARouter出现 there‘s no route matched in group问题排查

在使用ARouter时候会出现找不到映射路径的问题&#xff0c;这里给兄弟们总结下踩过的坑 所有用到的模块都要有填写依赖 android {defaultConfig {......javaCompileOptions {annotationProcessorOptions {arguments [AROUTER_MODULE_NAME: project.getName()]}}} } ... depe…

超详细~25考研规划~感恩现在努力的你!!!

25考研规划 俄语&#xff0c;翻译过来叫我爱你 考试时间 第一天 8.30-11.30政治——100分 2.00-5.00英语——100分 第二天 8.30-11.30数学——150分 2.00-5.00专业课——150分 1.什么是25考研 将在2024年12月参加考研&#xff0c;2025年本科毕业&#xff0c;9月读研究…

ANR问题分析的一般套路

目录 一.ANR初步了解1.发生原因2.ANR分类 二.ANR的Log解读1.Log获取2.案例一&#xff1a;sp耗时问题导致应用ANR 三.系统耗时分析方案1.binder_sample2.dvm_lock_sample3.binder starved4.案例二&#xff1a;疯狂Binder Call导致应用ANR5.案例三&#xff1a;广播超时导致App的A…

Django+Vue项目创建 跑通

参考链接&#xff1a; 【精选】DjangoVue项目构建_django vue-CSDN博客 一、背景 主要介绍如何使用后端Django 前端Vue 的技术栈快速地搭建起一套web项目的框架。 为什么使用Django和Vue? Django是Python体系下最成熟的web框架之一&#xff0c;由于Python语言的易用…