【c++随笔13】多态

【c++随笔13】多态

原创作者:郑同学的笔记

原创地址: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 修饰的类成员函数。
  • 指针调用
cpp 复制代码
#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;
}

输出

  • 引用调用
cpp 复制代码
/* 接收身份 */
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关键字声明为虚函数时,派生类可以对该虚函数进行重写,并且在派生类中返回类型可以是基类返回类型的子类型。这种返回类型的子类型关系称为协变。

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

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

cpp 复制代码
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、父虚子非虚------构成多态

现在来讲第二个例外。

  • 父类的虚函数没了无法构成多态:
  • 但是,子类的虚函数没了却能构成多态:
cpp 复制代码
#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. 直接定义对象

cpp 复制代码
#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操作符在堆上创建对象

cpp 复制代码
#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关键字修饰类时,表示该类是最终类,不能被其他类继承。例如:
cpp 复制代码
class Base final {
    // ...
};

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

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

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

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

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

  1. 修饰虚函数:与修饰普通成员函数类似,使用final关键字修饰虚函数时,表示该虚函数是最终版本,不能被派生类重写。例如:
cpp 复制代码
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关键字的示例:

cpp 复制代码
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来声明的,表示该函数没有实现,派生类必须重写它。

cpp 复制代码
virtual void pureVirtualFunction() = 0;

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

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

2、 抽象类(abstract class)

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

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

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

3、抽象类指针

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

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

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

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

  • 普通函数的继承是一种实现继承,子类继承了父类函数,可以使用函数,继承的是函数的实现。
  • 虚函数的继承是一种接口继承,子类继承的是父类虚函数的接口,目的是为了重写,
  • 达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
  • 出现虚函数就是为了提醒你重写的,以实现多态。如果虚函数不重写,那写成虚函数就没价值了。
相关推荐
cherub.3 分钟前
深入解析信号量:定义与环形队列生产消费模型剖析
linux·c++
I_Am_Me_15 分钟前
【JavaEE进阶】 JavaScript
开发语言·javascript·ecmascript
暮色_年华17 分钟前
Modern Effective C++item 9:优先考虑别名声明而非typedef
c++
重生之我是数学王子25 分钟前
QT基础 编码问题 定时器 事件 绘图事件 keyPressEvent QT5.12.3环境 C++实现
开发语言·c++·qt
Ai 编码助手27 分钟前
使用php和Xunsearch提升音乐网站的歌曲搜索效果
开发语言·php
学习前端的小z31 分钟前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
神仙别闹39 分钟前
基于C#和Sql Server 2008实现的(WinForm)订单生成系统
开发语言·c#
XINGTECODE40 分钟前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
我们的五年1 小时前
【Linux课程学习】:进程程序替换,execl,execv,execlp,execvp,execve,execle,execvpe函数
linux·c++·学习
zwjapple1 小时前
typescript里面正则的使用
开发语言·javascript·正则表达式