C++——多态

目录

引言

多态

1.多态的概念

[1.1 编译时多态(静态多态)](#1.1 编译时多态(静态多态))

[1.2 动态多态(运行时多态)](#1.2 动态多态(运行时多态))

2.多态的定义和实现

[2.1 多态构成的条件](#2.1 多态构成的条件)

[2.2 虚函数](#2.2 虚函数)

(1)虚函数的定义

(2)虚函数重写

(3)虚函数重写的例外

[(4)C++11 override和final](#(4)C++11 override和final)

(5)重载,重写(覆盖),隐藏(重定义)的对比

3.纯虚函数和抽象类

[3.1 概念](#3.1 概念)

[3.2 接口继承和实现继承](#3.2 接口继承和实现继承)


引言

C++------继承 中,我们学习了面向对象编程中的一个核心概念------继承,接下来我们接着学习另一重要概念------多态

多态

1.多态的概念

多态性是面向对象编程中的一个核心概念,它允许对象通过统一的接口实现不同的行为。在C++中,多态性指的是能够通过基类的指针或引用来调用派生类中的方法。这种能力使得程序在运行时能够根据对象的实际类型来确定调用哪个方法,从而实现动态绑定。

多态,通俗来讲就是多种形态。具体来说就是,要去完成某个特定的行为,当不同的对象去完成时会产生出不同的状态

举个简单的例子:买火车票这一行为,普通人买票时需要全价购买,而学生可以半价购买。

多态主要分为两种类型:静态多态 (编译时的多态)和动态多态(运行时多态)。

1.1 编译时多态(静态多态)

编译时多态主要通过函数重载函数模板来实现。

函数重载:在C++中,函数重载允许在同一个作用域内定义多个同名但参数列表不同的函数。当调用这些重载函数时,编译器会根据提供的参数类型和数量来决定调用哪个函数。这种多态性在编译时就已经确定,因此被称为编译时多态或静态多态。

函数模板:函数模板是一种泛型编程工具,它允许程序员编写与类型无关的代码。通过模板参数,函数模板可以在编译时根据提供的具体类型来生成相应的函数。这种多态性同样在编译时就已经确定。

编译时多态的优点是编译器可以在编译时优化代码,但由于多态性是在编译时确定的,因此它无法处理在运行时才确定的对象类型。

1.2 动态多态(运行时多态)

动态多态主要通过继承和虚函数来实现。

继承和虚函数:在C++中,运行时多态性通常通过继承和虚函数来实现。基类中的成员函数被声明为虚函数,派生类可以重写这些虚函数。当通过基类的指针或引用来调用虚函数时,程序会根据对象的实际类型来调用相应的派生类中的函数。这种多态性在运行时才确定,因此被称为运行时多态或动态多态。

动态多态的优点是可以在运行时根据对象的实际类型来调用不同的函数,从而增强了代码的灵活性和可扩展性。但缺点是相对于编译时多态,动态多态会带来一定的性能开销,因为需要在运行时进行类型检查和函数地址的解析。

2.多态的定义和实现

2.1 多态构成的条件

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

实现多态需要实现以下两个条件:

1.必须通过基类的指针或者引用调用虚函数。

2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。

我们根据上面提到的买票的例子来个简单的代码:

class Person
{
public:
	// 声明一个虚函数BuyTicket
	// 虚函数允许在派生类中被重写,以实现多态性
	virtual void BuyTicket() 
	{
		cout << "买票-全价" << endl;
	}
};
class Student : public Person
{
	// 重写基类中的虚函数BuyTicket
	// 这样,当通过基类指针或引用指向Student对象时
	// 调用BuyTicket会执行这里的实现
public:
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person P;
	Student S;
	Func(P);
	Func(S);
	return 0;
}
2.2 虚函数
(1)虚函数的定义

虚函数是指被virtual关键字修饰的类成员函数。这个virtual关键字的作用是使得函数在派生类中可以被重写,从而实现多态性。

这里的virtual与虚继承中的virtual是不同的概念,尽管它们都使用了virtual这个关键字,但用途和机制完全不同。

class Person
{
public:
	// 声明一个虚函数BuyTicket
	// 虚函数允许在派生类中被重写,以实现多态性
	virtual void BuyTicket()     // 虚函数
	{
		cout << "买票-全价" << endl;
	}
};
(2)虚函数重写

虚函数的重写(覆盖)派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型函数名字参数列表完全相同),称子类的虚函数重写了基类的虚函数。

class Student : public Person
{
	// 重写基类中的虚函数BuyTicket
	// 这样,当通过基类指针或引用指向Student对象时
	// 调用BuyTicket会执行这里的实现
public:
	virtual void BuyTicket()		// 虚函数重写
	{
		cout << "买票-半价" << endl;
	}
};
(3)虚函数重写的例外

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

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

class A
{
	// ...
};

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

class Person
{
public:
	virtual A* f()
	{
		return new A;
	}
};

class Student :public Person
{
public:
	virtual B* f()	// 协变
	{
		return new B;
	}
};

析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

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

class Student : public Person
{
public:
	virtual ~Student() // 构成重写
	{
		cout << "~Student()" << endl;
	}
};
int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1;
	delete p2;
	return 0;
}

输出结果为:

(4)C++11 override和final

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

class A
{
public:
	virtual void a(){}
};

class B :public A
{
public:
	virtual void a() override
	{
		cout << "a()" << endl;
	}
};

final:修饰虚函数,表示该虚函数不能再被重写

class A
{
public:
	virtual void a() final
	{
		// ...
	}
};

class B :public A
{
public:
	virtual void a()
	{
		cout << "a()" << endl;
	}
};

提示信息:

(5)重载,重写(覆盖),隐藏(重定义)的对比

3.纯虚函数和抽象类

3.1 概念

当一个函数在基类中声明为虚函数,并且其后紧跟= 0时,该函数就被定义为纯虚函数。纯虚函数没有函数体,它仅提供一个函数签名,要求派生类必须提供该函数的实现。

包含至少一个纯虚函数的类被称为抽象类(也称为接口类)。由于抽象类中存在未实现的纯虚函数,因此它不能被实例化。这是因为实例化一个抽象类对象会导致程序在尝试调用未实现的纯虚函数时崩溃。

当一个派生类从抽象类继承时,它必须实现所有从基类继承的纯虚函数。如果派生类没有实现所有纯虚函数,那么它自身也将成为一个抽象类,同样不能被实例化。

只有当派生类实现了所有从基类继承的纯虚函数后,它才能被实例化。这是因为此时派生类已经提供了所有必要的函数实现,可以创建一个完整的对象。

来看看下面这个简单的例子:

class Base
{
public:
	// 纯虚函数
	// 只要有一个纯虚函数,这个类称为抽象类
	// 抽象类特点:
	// 1.无法实例化对象
	// 2.抽象类的子类,必须要重写父类中的纯虚函数,
	//   否则也属于抽象类
	virtual void func() = 0;
};

class Son :public Base
{
public:
	virtual void func()
	{
		cout << "func()" << endl;
	}
};

void test1()
{
	//Base b;		// 不允许使用抽象类类型
	//new Base;		// 抽象类无法实例化对象

	//Son s;		// 子类必须重写父类中的纯虚函数
					// 否则无法实例化对象
	Base* base = new Son;
	base->func();
	delete base;
}

int main()
{
	test1();
	return 0;
}
3.2 接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。

虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。


以上是多态内容的上篇,后面还会继续深入学习。

求点赞收藏评论关注!!!
感谢大佬支持!!!

相关推荐
I_Am_Me_23 分钟前
【JavaEE初阶】线程安全问题
开发语言·python
运维&陈同学30 分钟前
【Elasticsearch05】企业级日志分析系统ELK之集群工作原理
运维·开发语言·后端·python·elasticsearch·自动化·jenkins·哈希算法
金士顿2 小时前
MFC 文档模板 每个文档模板需要实例化吧
c++·mfc
ZVAyIVqt0UFji3 小时前
go-zero负载均衡实现原理
运维·开发语言·后端·golang·负载均衡
loop lee3 小时前
Nginx - 负载均衡及其配置(Balance)
java·开发语言·github
SomeB1oody4 小时前
【Rust自学】4.1. 所有权:栈内存 vs. 堆内存
开发语言·后端·rust
toto4124 小时前
线程安全与线程不安全
java·开发语言·安全
水木流年追梦4 小时前
【python因果库实战10】为何需要因果分析
开发语言·python
人才程序员5 小时前
QML z轴(z-order)前后层级
c语言·前端·c++·qt·软件工程·用户界面·界面
w(゚Д゚)w吓洗宝宝了5 小时前
C vs C++: 一场编程语言的演变与对比
c语言·开发语言·c++