【C++】多态

举报
YIN_尹 发表于 2023/12/20 20:50:23 2023/12/20
【摘要】 @[TOC] 之前的文章我们学习了C++的继承,这篇文章我们来学习多态前言<font color = black>需要声明的,本文中的代码及解释都是在vs2022下的x86程序中,涉及的指针都是4bytes。 如果要其他平台下,部分代码需要改动。比如:如果是x64程序,则需要考虑指针是8bytes问题等等。1. 多态的概念多态的概念:<font color = blue>通俗来说,多态就是多...

@[TOC] 之前的文章我们学习了C++的继承,这篇文章我们来学习多态

前言

<font color = black>需要声明的,本文中的代码及解释都是在vs2022下的x86程序中,涉及的指针都是4bytes。 如果要其他平台下,部分代码需要改动。比如:如果是x64程序,则需要考虑指针是8bytes问题等等。

1. 多态的概念

多态的概念:

<font color = blue>通俗来说,多态就是多种形态;具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

举个栗子:

<font color = black>比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。 在这里插入图片描述

再举个例子:

<font color = black>为了争夺在线支付市场,支付宝经常会做诱人的扫红包-支付-给奖励金的活动。 那么大家想想为什么有人扫的红包又大又新鲜8块、10块...,而有人扫的红包都是1毛,5毛....。其实这背后也是一个多态行为。 支付宝首先会分析你的账户数据,如果你是新用户、没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额 = random()%99;如果你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你去使用支付宝,那么就你扫码金额 = random()%1;总结一下:同样是扫码动作,不同的用户扫得到的不一样的红包,这也是一种多态行为。 ps:支付宝红包问题纯属瞎编,大家仅供娱乐。 在这里插入图片描述

2. 多态的语法及实现

2.1 虚拟函数

要学习多态,首先我们要认识一下虚函数(虚拟函数)

<font color = blue>虚函数:即被virtual关键字修饰的类成员函数 在这里插入图片描述 <font color = black>这里的BuyTicket就是一个虚函数。

这里想告诉大家的是:

<font color = black>这里说的虚函数和我们之前学的虚拟继承是没什么关系的,只不过它们用了同一个关键字virtual罢了。

2.2 多态的构成条件

<font color = blue>具体来说,多态就是继承关系的不同类对象,去调用同一个函数,产生不同的行为。 <font color = black>比如Student继承了Person。Person对象买票全价,Student对象买票半价。

那要想实现多态,必须满足两个条件

2.2.1 条件1:虚函数的重写

第一个条件:

<font color = blue>被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

那什么是虚函数的重写呢?

<font color = blue>虚函数的重写(也可以叫覆盖): 派生类中有一个跟基类完全相同的==虚==函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。(可以认为子类继承了父类虚函数的接口,重写了实现,也称为接口继承) 注意与隐藏的区别。

比如,像这样:

<font color = black>在这里插入图片描述 这里子类Student的虚函数BuyTicket就对父类Person的虚函数BuyTicket进行了重写。

注意==⚠==:

<font color = "#000066">在重写基类虚函数时,派生类的虚函数不加virtual关键字,也可以构成重写(可以认为继承后基类的虚函数被继承下来了在派生类中依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用 但是父类的virtual是肯定不能省的,大家后面可以试一下,省略的话就不能构造多态了,当然不会报错。

那另一个条件呢?

2.2.2 条件2:基类的指针或者引用调用虚函数

第二个条件:

<font color = blue>必须通过基类的指针或者引用调用虚函数

我们先学语法,后面会给大家讲原理。

我们接着上面的代码给大家写,来完成第二个条件:

<font color = black>在这里插入图片描述 这里满足基类的引用调用虚函数

2.3 看看结果

现在多态的两个条件我们都满足了,我们看结果是怎样的:

<font color = black>在这里插入图片描述 此时已经成功实现了多态,Person类对象ps调用Func函数最终调的是Person里面的BuyTicket函数,而Student类对象st调用Func最终调到的是Student类里面的BuyTicket函数。 最终产生的是不同的结果。 在这里插入图片描述

当然我们也提到了子类的virtual可以省略:

<font color = black>在这里插入图片描述 但还是建议大家加上。

另外还可以是基类的指针去调用:

<font color = black>在这里插入图片描述

<font color = "#000066">我们说必须是基类的指针或者引用去调用虚函数,那就意味着用基类的对象是不行的

<font color = black>在这里插入图片描述 虽然没有报错,但是并没有实现多态。

3 虚函数重写的两个例外

3.1 析构函数的重写

我们来看这样一种场景:

<font color = black>在这里插入图片描述 还是Person和Student两个类,STUdent继承Person,它们都显示实现了自己的析构,我们运行程序 在这里插入图片描述 这没什么问题,结果我们也能理解,子类对象s先析构,子类的析构函数调再完成后自动调用父类的析构清理父类的部分。 最后p再调用自己的析构。

那我现在这样写:

<font color = black>两个类不动,我把main函数改成这样 在这里插入图片描述 注意两个指针都是Person*,一个指向父类对象,一个指向子类对象,这样赋值是没问题的,然后我们运行 在这里插入图片描述 大家看这次的析构调用的有没有问题。 🆗,s指向的是Student子类对象,但是析构的时候只调了父类的析构函数,这样有没有问题? 如果子类自己的成员存在资源管理,那只调父类析构的话,就只清理了子类里面父类的部分,那是不是就内存泄漏了啊。

那这里为什么是这样的结果呢?

<font color = black>大家回忆一下delete会做哪些事情? 🆗,对于自定义类型delete会做的是: <font color = black>1. 在要释放的对象空间上执行析构函数,完成对象中资源的清理工作 <font color = black>2. 调用operator delete函数,operator delete实际调free释放对象的空间 那它这里第一步执行析构是它这个指针是什么类型,它执行的就是那个类的析构,所以这里delete s只会执行父类的析构。

但是在这个地方,我们期望它是这样正常的只调父类的析构吗?

<font color = black>是不是不期望啊,因为如果父类的指针指向的是子类对象,在delete的时候还是只调父类的析构,那是不是就可能会有内存泄漏的风险啊。

我们期望他怎么做,按照什么去调析构?

<font color = black>是不是期望它按照指向的对象的类型去调啊。 指向的是父类对象,就按父类的析构去走;指向的是子类的对象,就按子类的析构去走。

那我们可以怎么做?

<font color = black>🆗,是不是多态就上场了。 用多态是不是就可以达到这样的效果啊。 那现在怎么实现多态,是不是把父类的析构变成虚函数,然后子类重写就行了啊。 因为现在的情况就是基类的指针,delete的时候又会自动调用析构

所以:

<font color = black>在这里插入图片描述 然后我们再看结果 在这里插入图片描述 这样是不是就行了啊。

但是:

<font color = "#000066">重写不是要求基类和派生类的虚函数名字一样吗,可是它们两个的析构函数名字并不一样啊。 🆗,大家还记不记得,我们讲继承的时候说过的,当时我们是这样说的 由于后面多态里面的一些原因,编译器会对析构函数名进行特殊处理,都会被处理成destrutor() 那其实就是为这种场景做准备的!!! 只有派生类Student的析构函数重写了Person的析构函数,delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。

所以:

<font color = blue>虽然函数名不相同,看起来违背了重写的规则,其实不然,这里编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

3.2 协变

虚函数重写的第二个例外:

<font color = blue>派生类重写基类虚函数时,与基类虚函数返回值类型可以不同。 但是是有要求的: 基类虚函数必须返回基类对象的指针或引用,派生类虚函数必须返回派生类对象的指针或引用,我们把这种情况称为协变。 另外,这里这里返回的基类派生类的指针或引用也可以是其它继承体系中的基类和派生类。

举个栗子:

<font color = black>像这样在这里插入图片描述 父类的虚函数返回父类的指针,子类重写的虚函数返回子类的指针 在这里插入图片描述 这样也是可以实现多态的。

另外上面说返回的基类派生类的指针或引用也可以是其它继承体系中的基类和派生类,什么意思呢?

<font color = black>那就是这样 在这里插入图片描述 这样也是可以的 在这里插入图片描述

不过这个协变可能在实际中应用的场景不多。

3. 做一道笔试题

下面我们看一道题目

问:以下程序输出结果是什么() 在这里插入图片描述 在这里插入图片描述 大家先自己看看题,认真分析一下。

我们来一起分析一下:

<font color = black>首先,父类里面有两个虚函数:func和test。 但是子类只对func进行了重写。 然后main函数里面用了一个子类对象的指针去调用test函数,能调到吗? 当然可以,虽然子类里面看起来没有test函数,但是它继承了父类的test函数啊,所以可以调,这里调到的就是子类继承下来的那个test函数。 然后在test函数里面,又去调了func函数。 那现在大家思考一个问题,test函数里面调用func,是否构成多态 那我们就看它是否满足多态的两个条件嘛,首先虚函数的重写,这里是满足的,子类对父类的虚函数func进行了重写。 那第二个条件:必须是基类的指针或引用去调用,满足吗 🆗,其实也是满足的。 虽然子类的test是继承下来的,但是继承下来test函数中this指针的类型是不会变的,还是父类指针A*(继承下来函数的参数类型是不会变的)。 所以这里相当于把子类对象p的指针赋给了父类的指针(this指针),然后,通过这个父类的指针去调用被重写的虚函数func。 所以这里是满足多态的 那这里调到的func就是子类对象p对应的func。 在这里插入图片描述 所以结果应该是B->0,因为子类给val的缺省值是0 对吗??? 我们验证一下 在这里插入图片描述 哎呀,错了,是B->1 为啥呢? 🆗,我们上面说了,虚函数的重写只是重写了函数的实现,而继承了接口,所以父类Func中给val的缺省值也继承了下来 在这里插入图片描述 所以,结果才是B->1 <font color = blue>因此这道题答案是B

变个型

我修改一下,大家再来看:

<font color = black>在这里插入图片描述 现在我把test放到B里面 在这里插入图片描述 main函数我不动。那现在结果是啥? 在这里插入图片描述 这次是B->0

为什么呢?我们来分析一下

<font color = black>大家看这次func里面调用test还是多态吗? 🆗,这种情况下是不是没有构成多态啊。 因此此时test是只属于B的,所以test的this指针是B*的,这次是子类的指针去调用func的,所以没有构成多态。 没有构成多态,就可以认为没用进行接口继承,或者认为没用多态虚函数的重写就没有作用。 所以这里就是正常的调用B里面的func,缺省值就是自己里面的0。 因此结果是B->0

那说到这里我们也顺便来区分一组概念:

4. 接口继承和实现继承

<font color = blue>普通函数的继承是一种实现继承,派生类继承了基类的成员函数,可以使用该函数,继承的是函数的实现。 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。 所以如果不实现多态,最后不要把函数定义成虚函数。

5. C++11 override 和 final

接下来我们来了解两个C++11中的关键字

5.1 final

首先我们来看final关键字,这个关键字有什么作用呢?

<font color = black>我们在之前继承的那篇文章里其实又提到过final,我们说如果详设计一个不能被继承的类,就可以用final。 <font color = blue>即用final修饰一个类,可使该类变为最终类,即不能被继承的类 <font color = black>在这里插入图片描述

那还有其它作用吗?

<font color = blue>final修饰一个虚函数,该虚函数将不能再被重写 <font color = black>在这里插入图片描述 但是感觉这个作用意义不大,因为虚函数一般就是为了被重写,然后实现多态的。

5.2 override

然后再看一个关键字叫做override 他有什么作用呢?

<font color = blue>override:检查派生类是否对基类的虚函数进行了重写,如果没有重写编译报错。 <font color = black>在这里插入图片描述 这里是重写了的,所以没报错。 我们知道虚函数重写的话基类是必须加virtual的,子类可以不加,但建议加上 所以如果把父类的virtual去掉就会报错 在这里插入图片描述 因为子类里加了override,它必须对基类的虚函数进行重写。 如果重写不正确也能检查出来 在这里插入图片描述

6. 重载、覆盖(重写)、隐藏(重定义)的对比

在这里插入图片描述

7. 抽象类

接下来我们再来学一个东西叫做抽象类,先来了解一下它的概念:

<font color = blue>在虚函数的后面写上 =0 ,则这个函数为纯虚函数(不需要函数体)。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象。 派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。 纯虚函数规范了派生类必须重写(不重写就不能实例化),另外纯虚函数更体现出了接口继承。

下面通过一些例子帮助大家理解理解:

<font color = black>在这里插入图片描述 这里的Car这个类就是一个抽象类,因为它包含纯虚函数。 所以,Car这个类不能实例化出对象 在这里插入图片描述

那该怎么感性的理解一下这个抽象类呢?

<font color = black>它定义了一组方法和行为,但并没有具体的实现细节。你可以把它看作是一个规范,告诉其他类应该有哪些方法,并且如何使用这些方法。 举个例子来说明抽象类的概念: 假设我们有一个抽象类叫做"动物",其中有一个纯虚函数"发出声音"。我们知道每种动物都会发出声音,但是具体的声音是不同的。那么我们可以定义一个"狗"类和一个"猫"类,它们都继承自"动物"类,并实现了"发出声音"方法。这样,无论我们有一只狗还是一只猫,我们都可以使用"动物"类的指针或引用来调用"发出声音"方法,而不需要关心具体是哪种动物。 可以认为如果一个类在现实中没有对应的实体,我们就可以把它定义成一个抽象类。

我们继续:

<font color = black>另外上面还说到抽象类被继承后的子类也不能实例化出对象:在这里插入图片描述 因为这里的子类还是一个抽象类,它也包含纯虚函数,是它继承下来的嘛。 只有重写纯虚函数,派生类才能实例化出对象 在这里插入图片描述 这下就可以了。 因为重写之后,它就不包含纯虚函数了,所以他就不是抽象类,那就可以实例化出对象了。

那这篇文章就先到这里,下一篇文章,我们将重点讲解一下多态的底层原理...

8. 用到的代码

我的Gitee

//多态
//class Person 
//{
//public:
//  virtual void BuyTicket() 
//  { 
//      cout << "买票-全价" << endl; 
//  }
//};
//class Student : public Person
//{
//public:
//  virtual void BuyTicket()
//  {
//      cout << "买票-半价" << endl;
//  }
//};
//void Func(Person& p)
//{
//  p.BuyTicket();
//}
//int main()
//{
//  Person ps;
//  Student st;
//
//  Func(ps);
//  Func(st);
//
//  return 0;
//}
​
//析构函数的虚重写
//class Person 
//{
//public:
//  virtual ~Person() 
//  { 
//      cout << "~Person()" << endl; 
//  }
//};
//class Student : public Person 
//{
//public:
//  virtual ~Student()
//  { 
//      cout << "~Student()" << endl; 
//  }
//};
//int main()
//{
//  Person* p = new Person;
//  Person* s = new Student;
//
//  delete p;
//  delete s;
//
//  return 0;
//}
​
//协变
//class A {};
//class B : public A {};
//class Person 
//{
//public:
//  virtual A* BuyTicket() 
//  { 
//      cout << "买票-全价" << endl; 
//      return new A;
//  }
//};
//class Student : public Person
//{
//public:
//  virtual B* BuyTicket()
//  {
//      cout << "买票-半价" << endl;
//      return new B;
//  }
//};
//class Person
//{
//public:
//  virtual Person* BuyTicket()
//  {
//      cout << "买票-全价" << endl;
//      return this;
//  }
//};
//class Student : public Person
//{
//public:
//  virtual Student* BuyTicket()
//  {
//      cout << "买票-半价" << endl;
//      return this;
//  }
//};
//int main()
//{
//  Person p;
//  Student s;
//  Person* pp = &p;
//  Person* ps = &s;
//
//  pp->BuyTicket();
//  ps->BuyTicket();
//
//  return 0;
//}
​
//class A
//{
//public:
//  virtual void func(int val = 1) 
//  { 
//      std::cout << "A->" << val << std::endl; 
//  }
//};
//
//class B : public A
//{
//public:
//  void func(int val = 0) 
//  { 
//      std::cout << "B->" << val << std::endl; 
//  }
//  virtual void test()
//  {
//      func();
//  }
//};
//
//int main()
//{
//  B* p = new B;
//  p->test();
//  return 0;
//}
​
//class Base final { // Base类被声明为最终类
//    // 类定义...
//};
//
//class Derived : public Base { // 错误,Derived无法继承自Base类
//    // 类定义...
//};
​
//class Car
//{
//public:
//  virtual void Drive() final {}
//};
//class Benz :public Car
//{
//public:
//  virtual void Drive() 
//  {
//      cout << "Benz-舒适" << endl; 
//  }
//};
​
//class Car {
//public:
//  virtual void Drive() {}
//};
//class Benz :public Car {
//public:
//  virtual void Drive() override { cout << "Benz-舒适" << endl; }
//};
//int main()
//{
//  return 0;
//}
​
class Car
{
public:
    virtual void Drive() = 0;
};
class Benz :public Car
{
public:
    virtual void Drive()
    {
        cout << "Benz-舒适" << endl;
    }
};
int main()
{
    Benz b;
    return 0;
}

在这里插入图片描述

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。