C++类 多态(笔记)

写在前面

最近一直想着要把最近学的C++的多态整理出来,可是每次想写的时候,总是发现写不出来,可能是理解还不够,现在感觉终于比较深入的理解多态,于是把笔记整理出来

 


目录

多态

定义

多态实现要素

多态案例:计算器

 原理:

1

2

3

纯虚函数

虚析构和纯虚析构


多态

定义

同一种方法的调用,针对不同的对象有不同的效果

为什么要学习多态?因为多态非常符合面向对象的编程思维,就像现实生活中任意两个人去做同一件事结果可能是不同的

想要更好的认识多态,先要了解一下静态联编和动态联编

静态联编:地址早绑定——在编译阶段就已经知道这一句函数调用是执行哪一段特定的函数代码

动态联编:地址晚绑定——在序运行阶段才知道这一句函数调用是执行的哪一段特定的函数代码

这样说有点干燥,就像下面的代码段一样:

//动物类
class Animal
{
public:
    void speak()
    {
        cout<<"动物在说话"<<endl;
    }
    
};
//小狗类,继承动物类
class Dog:public Animal
{
public:
    void speak()
    {
        cout<<"小狗在说话"<<endl;
    }

};

//测试函数
void test(Animal& animal)
{

    animal.speak();

}
int main()
{

    Animal animal;
    Dog dog;
    test(animal);
    test(dog);

    return 0;
}

静态联编

这段代码定义了两个类:动物类和小狗类,在test函数里面的animal.spesk()就是静态联编,因为无论传入的是动物类还是小狗类结果都是调用动物类中的speak函数(这里假定你已经知道了,子类可以转换为父类,但是父类对象不能转换为子类)这就是静态联编。在编译阶段就已经知道了,这个函数调用是执行哪一段特定的函数代码。

运行结果

那么动态联编又是怎样的呢?

就拿上面的那个例子来说,就是向test函数里面传递的对象不同就会调用不同对象的speak函数,比如向test函数里面传递的是animal对象,那么调用的就是animal对象里面的speak函数,当传递的参数是dog对象的时候,调用的就是dog对象的speak函数

这就是动态联编

具体实现就是在父类和子类的同名函数前面加上virtual修饰符构成虚函数,然后在子类中重写虚函数

具体实现代码如下:

 

//动物类
class Animal
{
public:
   virtual void speak()
    {
        cout<<"动物在说话"<<endl;
    }
    
};
//小狗类,继承动物类
class Dog:public Animal
{
public:
   virtula void speak()
    {
        cout<<"小狗在说话"<<endl;
    }

};

//测试函数
void test(Animal& animal)
{

    animal.speak();

}
int main()
{

    Animal animal;
    Dog dog;
    test(animal);
    test(dog);

    return 0;
}

可以发现这一段代码和上一段代码只有一点区别,就是在父类和子类的speak函数前面都加上了一个virtual关键字进行修饰,这样之后,再次运行代码就会发现当传进test函数是animal对像时,调用的是animal对象的speak函数,当传入的参数为dog对象时,调用的就是dog对象的speak函数。

运行结果如下

 

多态实现要素

 1、有继承关系。只有父类和子类,才会有多态这种现象

2、父类指针或者父类引用指向子类对象

3、父类和子类中有同名的虚函数,并且在子类中重写同名虚函数

多态案例:计算器

 

#include<iostream>
using namespace std;

//定义计算器的基类
class calculator
{
    
public:
    
   virtual void calc(int num1,int num2)
    {}
    
};
//计算器的子类 :加法
class add:public calculator
{
public:    
  virtual void calc(int num1,int num2)
    {
        cout<<"num1 + num2 = "<<num1+num2<<endl;
    }  
    
};
//计算器的子类:减法
class sub:public calculator
{
public:
    virtual void calc(int num1,int num2)
    {
        cout<<"num1 - num2 = "<<num1-num2<<endl;
    }
};

int main()
{
    calculator *Calc=new add;
    Calc->calc(5,6);//计算5 + 6
    delete Calc;
    Calc =new sub;
    Calc->calc(5,6);//计算5 - 6
    delete Calc;
    
    return 0;

}

可以发现,在main函数中调用了两次calc函数,但是两次调用的是不同的函数。

运行结果如下

 

 

 那多态实现的原理是什么呢?死记语法吗?

当然不是。

多态实现的底层如下所述

 原理:

1

当一个类中存在虚函数,那么在实例化对象的时候,编译器会自动向这个对象中添加一个隐藏成员——一个指向保存虚函数地址的虚函数表的指针——虚函数指针(或者也叫做虚函数表指针),当这个对象调用自己的虚函数的时候就会先到虚函数表中找到这个虚函数的地址,然后准确的调用在虚函数表中保存的地址的虚函数。

还是上面的那个动物类,动物类中只有一个speak函数,但是执行cout<<sizeof(Animal)<<endl;语句的时候,会输出4,这就是一个隐藏成员虚函数指针

2

接下来进一步分析多态的原理,首先分析父类和子类的结构,如图所示:

 

首先,看父类(Animal)的内部结构虚函数表中存放Animal::speak()函数的地址,当子类继承的时候会把父类的所有成员都继承下来,那么子类中也有从父类中继承来的虚函数,如果没有重写子类中的虚函数,那么子类中的虚函数表里面存放的父类的虚函数地址,

接下来,看在子类中重写父类中的同名虚函数会怎么样。

在子类中重写父类中的同名虚函数

由于在子类中重写了父类中的虚函数,所以在子类的虚函数表中将继承下来的父类虚函数地址覆盖了,当调用speak函数的时候,会先到虚函数表里面找到speak函数的地址,虚函数表中储存的是Dog类中虚函数speak的地址,那么接下来调用的就是Dog类的speak。这就实现了多态

3

最后再讨论为什么一定要用父类的指针或者引用指向子类的对象

 还是举动物类和小狗类的例子,如果将上面的test测试函数改为下面的这种形式,那么无论是否重写虚函数,最后调用的speak()函数还会是Animal类的speak函数,因为在传参的时候,编译器会自动的把子类对象转换为父类对象。但是如果定义的是一个父类引用或者父类指针,那么就不是简单的自动转换了,父类引用或者指针指向子类对象,那么调用speak函数的时候就会调用子类的speak函数

void test(Animal animal)
{
    anmal.speak();
}

/修改之前的版本
void test(Animal & animal)
{
    animal.speak();
}

纯虚函数

最后来讨论一下纯虚函数,纯虚函数的形式:virtual 函数返回值类型 函数名 (参数列表)=0;

总结来说,就是在一个虚函数的后面加上一个等号,然后分号结束

纯虚函数的作用:当一个类中只要存在一个纯虚函数那么这个类就会成为一个抽象类,这个类不能实例化对象,但是可以定义指向这个类的指针或者引用

抽象类特点:1、不能实例化对象 

                      2、由于子类继承的是父类中的所有成员,所以子类必须要重写父类中的纯虚函数,否则子类也属于抽象类,也无                               法实例化对象

为什么要使用纯函数?

在上面的计算器案例中我们会发现,其实基类calculator中的calc函数什么都没有干,事实上在写多态的时候,父类中的虚函数基本上什么事情都不做,并且我们也不需要父类来实例化对象,都是定义一个父类的引用或者父类指针指向子类,来实现多态

所以,我们通常把父类中的虚函数改为纯虚函数

虚析构和纯虚析构

当父类指向子类的指针或者引用“死亡”时,并不会调用子类中的析构函数,当子类中有向堆区申请内存的操作时,那么就会造成内存泄漏,怎么解决这个问题呢?

答案就是虚析构或者纯虚析构

首先,虚析构:

虚析构的写法就是在父类的析构函数前面加上一个virtual关键字就好了,这样当父类指针死亡的时候就会调用子类的析构函数,

其次,纯虚析构

纯虚析构的写法就是在父类的虚析构的基础上,改为纯虚析构,~类名()=0;

但是,要注意两点 :1、如果类中有纯虚析构了,那么这个类就是一个抽象类,

                                  2、当在类中声明了一个纯虚函数的时候还要在类外实现,否则就会报错,因为在一个对象死亡的时候,一定要调用析构函数的,如果只是声明了析构函数,而没有实现,编译器会因为没有析构函数进行调用而报错


写在后面

越来越发现写博客记笔记是多么的锻炼人了,没有深入理解根本就不知道怎么写,

前天晚上,坐在电脑前,准备总结最近学的多态,打开CSDN,……,半个小时后,打开CSDN,…………

昨天晚上吃完饭,兴冲冲地坐在电脑前,打开CSDN,然后…………

一直在纠结,到底怎样讲给别人听(虽然不一定有人看,但是咸鱼也要有梦想,万一实现了呢。。),

要想说服别人首先要说服自己,于是我一遍又一遍地百度,CSDN,b站,终于写出来了,,,

只学语法和用法,而不运用就相当于纸上谈兵看,所以附上实战项目代码:

文字PK小游戏

链接:https://pan.baidu.com/s/1AqtkvdodhcrOudU55XxeRA 
提取码:jzrr

 

 

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值