【C++】多态

在这里插入图片描述

多态的概念

多态(polymorphism)的概念:通俗来说,就是多种形态。多态分为编译时多态(静态多态)运⾏时多态(动态多态),这⾥我们重点讲运⾏时多态。

**编译时多态(静态多态)**主要就是我们前⾯讲的函数重载和函数模板,他们传不同类型的参数就可以调⽤不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时⼀般归为静态,运⾏时归为动态。

运⾏时多态,具体点就是去完成某个⾏为(函数),可以传不同的对象就会完成不同的⾏为,就达到多种形态。⽐如买票这个⾏为,当普通⼈买票时,是全价买票;学⽣买票时,是优惠买票(5折或75折);军⼈买票时是优先买票。

多态的定义及实现

多态的构成条件

多态是⼀个继承关系下的类对象,去调⽤同⼀函数,产⽣了不同的⾏为。⽐如Student继承了 Person。Person对象买票全价,Student对象优惠买票。

实现多态的两个必须重要条件

• 必须基类的指针或者引⽤调⽤虚函数

• 被调⽤的函数必须是虚函数。

说明:要实现多态效果,第⼀必须是基类的指针或引⽤,因为只有基类的指针或引⽤才能既指向基类对象又指向派生类对象(/只有基类的指针或引⽤才能既可以传基类对象,也可以传派生类对象); 第二,派⽣类必须对基类的虚函数重写/覆盖,重写或者覆盖了,派⽣类才能有不同的函数,多态的不同形态效果才能达到。

虚函数

(在菱形继承时我们的虚继承用到了virtual这个关键字,在多态中我们的虚函数也使用virtual这个关键字,但这二者并没有什么关系。)

成员函数前⾯加virtual修饰,那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修饰。

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

虚函数的重写/覆盖

虚函数的重写/覆盖:派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数类型完全相同),称派⽣类的虚函数重写了基类的虚函数。

注意:在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使⽤,不过在考试选择题中,经常会故意买这个坑,让你判断是否构成多态。

多态示例1:
class Person 
{
public:
 	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student : public Person 
{
public:
 	virtual void BuyTicket() { cout << "买票-打折" << endl; }
};

void Func(Person* ptr)
{
     // 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket 
     // 但是跟ptr没关系,⽽是由ptr指向的对象决定的。 
     ptr->BuyTicket();
}

int main()
{
     Person ps;
     Student st;
     Func(&ps);
     Func(&st);
     return 0;
}

我们可以梳理一下这个逻辑,我们给Func这个函数传两个实参,为不同对象的地址(前者为Person类,后者为Student类)时,形参Person* ptr都能接收,这是因为我们在继承中学过:public继承的派生类对象可以赋值给基类的指针/基类的引⽤,也就是我们所说的“切片”;

所以ptr->BuyTicket();调用的并不会就是Person中的BuyTicket函数,而是指向谁调用谁。

上面那个多态是在父类和子类之间,那么现在我们来看看另一个多个子类之间实现多态的例子:

多态示例2:
class Animal
{
public:
	virtual void talk() const
	{}
};

class Dog : public Animal
{
public:
	virtual void talk() const
	{
		std::cout << "汪汪" << std::endl;
	}
};

class Cat : public Animal
{
public:
	virtual void talk() const
	{
		std::cout << "(>^ω^<)喵" << std::endl;
	}
};

void letsHear(const Animal& animal)
{
	animal.talk();
}

int main()
{
	Cat cat;
	Dog dog;
	letsHear(cat);
	letsHear(dog);
	return 0;
}

同一个函数,不同的对象达到不同结果,我们的代码更显灵活。

接下来我们看一个“老六”题目:

多态场景的⼀个选择题

以下程序输出结果是什么()

A:A->0 B:B->1 C:A->1 D:B->0 E:编译出错 F:以上都不正确

分析:

(虚函数不一定重写。B完成了重写,因为参数名和缺省值不是影响因素,参数类型、函数名、返回值类型都相同,达到了三同,所以完成了重写。)

首先我们创建B类对象的指针p,调用test(),这里p能够调用到test()是因为它被继承下来到B类了。但是!继承并不是真的在B里面拷贝了一份test()。所以我们test()里的是A * this,而不是B * this。

所以this->func();满足多态的必要条件(1 必须基类的指针或者引用调用虚函数

2 被调⽤的函数必须是虚函数),构成多态。

而指向的对象是B类的,所以调用B的test()。

但是!重写的本质是重写虚函数的实现部分,也就是基类的声明部分加上派生类的实现部分,会重写为:

virtual void func(int val = 1)
{
    std::cout<<"B->"<<val<<std::endl;
}

所以最后的答案是B->1

绕了很多个弯。

我们可以通过调试来看看:

可以看到,p->test();来到A类中,而不是B中因为继承并不是真的在B中有一份,再看窗口的this,类型是A*,是基类的指针,满足了多态的必要条件;B中的func是重写,即使没写virtual。所以多态,p是B类的,所以调用的是B里的func(),但是val根据重写的本质(只重写实现部分),缺省值为A中的1.

虚函数重写的一些其他问题

协变

(下段解释来源于豆包)

在 C++ 中,协变(covariance)主要是指在类的继承关系中,函数返回类型的一种特殊关系。
一、概念阐述
如果派生类中的成员函数与基类中的成员函数具有相同的函数名和参数列表,但返回类型是基类中相应函数返回类型的派生类类型,那么就称这个返回类型是协变的。
例如:

class Base {
public:
    virtual Base* clone() const {
        return new Base(*this);
    }
};

class Derived : public Base {
public:
    virtual Derived* clone() const override {
        return new Derived(*this);
    }
};

在这个例子中,Derived类重写了Base类的clone函数,返回类型从Base*变为Derived*,这就是协变的一种体现。
二、协变的作用
1.增强类型安全性
使得代码在处理对象的继承关系时更加明确和安全。通过协变的返回类型,你可以确保在调用函数时得到正确类型的对象指针,避免了不必要的类型转换和潜在的错误。
2.支持多态性
协变与 C++ 的多态性紧密结合,允许在运行时根据对象的实际类型来决定调用哪个具体的函数实现。这对于实现灵活的软件设计非常重要,特别是在面向对象编程中,多态性是实现代码可扩展性和可维护性的关键。
三、注意事项
协变只适用于指针和引用类型的返回值。对于值类型的返回值,C++ 不支持协变。
在使用协变时,需要确保重写的函数实现与基类中的函数具有相同的行为和语义。否则,可能会导致意外的结果和错误。
总的来说,协变是 C++ 中一个比较高级的特性,它在处理类的继承关系和多态性时提供了更多的灵活性和安全性。但在使用协变时,需要谨慎考虑其适用范围和潜在的风险,以确保代码的正确性和可靠性。

派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。协变的实际意义并不⼤,所以我们了解⼀下即可。

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

class Person {
public:
	virtual A* BuyTicket()
	{
		cout << "买票-全价" << endl;
		return nullptr;
	}
};

class Student : public Person {
public:
	virtual B* BuyTicket()
	{
		cout << "买票-打折" << endl;
		return nullptr;
	}
};

void Func(Person* ptr)
{
	ptr->BuyTicket();
}

int main()
{
	Person ps;
	Student st;
	Func(&ps);
	Func(&st);

	return 0;
}

这里返回值的父子可以是自己类,也可以是其他的父子。

析构函数的重写(面试出现多)

详见该文:https://blog.csdn.net/2301_82135086/article/details/143454841?fromshare=blogdetail&sharetype=blogdetail&sharerId=143454841&sharerefer=PC&sharesource=2301_82135086&sharefrom=from_link

override和final关键字

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

// error C3668: “Benz::Drive”: 包含重写说明符“override”的⽅法没有重写任何基类⽅法 
class Car {

public:
	virtual void Dirve()
	{}
};

class Benz :public Car {

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

int main()
{
	return 0;
}

这里的错误在于函数名写错了。编译的时候是检查不出来这种错误的。assert的检查是运行时的,这个是编译时的。

如果我们不想让派⽣类重写这个虚函数,那么可以⽤final去修饰。

final这个关键字我们在继承的时候遇到过,一个类我们不想它被继承就可以用final。

这里,不想被重写,我们也用final。

重载/重写/隐藏的对比

这个也是一个在面试中常考察的题。

这三个概念都是(函数名相同的)函数之间的关系。

这三者相同的点在于函数名相同。

重写和隐藏的关系尤为紧密,重写的条件比隐藏的更严苛(参数和返回值也得相同,得构成“三同”)。可以这样说,不在同一作用域的函数名相同的函数,如果无法达到重写的条件构成条件,那就是隐藏。

纯虚函数和抽象类

在虚函数的后⾯写上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派⽣类重写,但是语法上可以实现),只要声明即可。

包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。

纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写实例化不出对象。

可以看到,我们把Benz类的Drive()的定义屏蔽掉后,Benz类继承了Car类的纯虚函数,于是也变成了一个抽象类,也就无法实例化出对象。

class Car
{

public:
 virtual void Drive() = 0;
};

class Benz :public Car
{

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

class BMW :public Car
{

public:
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};

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

	Car* pBMW = new BMW;
	pBMW->Drive();

	return 0;
}

本文到此结束。下篇文章讲解多态的原理

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部