• C++多态_virtual


    前言:在此文章开始之前,我们要知道面向对象编程思想是什么?说白了就是节省代码,也就是所谓的"代码复用"

    代码复用的两种体现方式:1、继承;2、共用相同的函数

    本篇文章我们围绕着"共用相同的函数"展开多态的详解,文章使用到的工具是vs2010,文章中展现的汇编代码,我会给出解释,不用太在意。

    目录

    为什么父类的指针可以指向子类?

            那么派生类的指针能否指向基类呢?

    什么是多态? 

            virtual关键字 

            virtual实现多态的底层逻辑

            纯虚函数---抽象类

            纯虚函数的用途

    总结


    在多态的讲解开始之前,我们还应该知道一个概念:父类指针或引用可以指向子类

    为什么父类的指针可以指向子类?

            不懂继承的可以先看一下我的这篇文章《继承浅谈》,大概了解一下继承的基本概念即可。在继承中我们知道,假设父类A定义变量x,y;子类B定义变量z,那他们的关系如下:

            从上面的图中可以看出,派生类成员的范围是要比基类的更加广泛的,因为派生类不仅仅继承了基类的成员,同时也有自己定义的成员

            那么假设我们定义一个派生类对象B b;他的成员如下:

            然后我们定义一个A类的指针指向B类的对象A *a = &b;如下:

            因为A类成员只有x,y宽度总共是八字节,所以A类一级指针指向B类也只能访问八个字节的内容,因为A* a=&b;时,a是指向b类首地址的,所以这里的指针a只能访问到前八字节的内容,也就是变量x和变量y(派生类继承过来的成员),如下:

            当然这一切只是理论,实践如下:

    1. #include
    2. #include
    3. class Person{ // 基类Person:成员有a、b
    4. public:
    5. int a;
    6. int b;
    7. Person(){};
    8. Person(int a,int b){
    9. this->a = a; this->b = b;
    10. }
    11. };
    12. class Teacher:public Person{ // 派生类Teacher:成员有基类的a、b和自己的c
    13. public:
    14. int c;
    15. Teacher(){};
    16. Teacher(int a,int b,int c):Person(a,b)
    17. {
    18. this->c = c;
    19. }
    20. };
    21. int main()
    22. {
    23. Teacher t(10,20,30);
    24. Person *p = &t;
    25. system("pause");
    26. return 0;
    27. }

             当我们打出p->的时候,可以看出编译器已经给出优化了,p是只能访问到a和b的

             那么真的不能访问到成员c吗?其实是可以的,使用指针偏移:

            运行:

             可以看到,基类其实是可以访问到派生类定义的成员的,不过假如派生类中有c和d,那么基类访问派生类中的d成员就比较难了;所以总结就是:父类指针访可以访问子类自己定义的成员,但没必要。

            那么派生类的指针能否指向基类呢?

            可以,但没必要

            首先,还是先来一波推理,假设我们定义对象A a;那么他的成员如下:

            我们再使用派生类指针指向基类对象B* b = &a; 如下:

            从理论上来看,子类指针是不能指向父类对象的,但是为什么我说“可以,但没必要呢?”

            首先我们在编译器中定义父类的对象,用子类指针指向父类试试:

            编译是通过不了的,不过我们可以使用强制转换,如下:

             编译成功。

            我们使用t->看看会弹出来什么,如下:

            他竟然可以访问到c,但是我们并没有初始化c成员,所以打印出来的肯定是一个垃圾值,这也就应了我们那句:可以,但没必要。 

    什么是多态? 

            多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。

            C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

            我们知道,面向对象编程思想就是为了节省代码的重复编写,那么我会围绕节省代码编写的例子去讲多态,可能例子会有些莫名其妙,但是知道这个道理就行了。

            我们先准备一下代码:

    1. #include
    2. #include
    3. class Person{ // 基类Person:成员有a、b
    4. public:
    5. int a;
    6. int b;
    7. Person(){};
    8. Person(int a,int b){
    9. this->a = a; this->b = b;
    10. }
    11. void print() // 基类添加一个打印成员的函数,会继承到派生类
    12. {
    13. printf("%d %d\n",a,b);
    14. }
    15. };
    16. class Teacher:public Person{ // 派生类Teacher:成员有基类的a、b和自己的c
    17. public:
    18. int c;
    19. Teacher(){};
    20. Teacher(int a,int b,int c):Person(a,b)
    21. {
    22. this->c = c;
    23. }
    24. };
    25. int main()
    26. {
    27. Teacher t(10,20,30);
    28. Person *p = &t;
    29. system("pause");
    30. return 0;
    31. }

            我们在main函数上方定义两个函数,一个用来打印父类成员,一个用来打印子类的,如下:

            大家先不要管父类可以直接调用类成员print()来输出成员的这个问题,因为这里只是举个例子 

            从上面的图中可以看出,因为子类继承了父类的print,所以子类可以直接t->print();其实这也就是代码复用的第一种体现方式:继承;文章开头有说过。

            但是我们会发现,我们写了两个函数,一个用来打印父类一个用来打印子类,函数的大致内容都差不多,那么这是不是也算是代码的重复编写了?是不是也违背了面向对象的编程思想呢?

            是的。

            接下来我们就要解决这个问题。怎么解决呢?       

            首先我们上面讲过,父类的指针或引用是可以直接指向子类对象的,那么如果我们函数的形参设置成一个父类的指针,是不是就意味着实参不仅可以传递父类对象还可以传递子类对象?

            通过这一想法,我们可以把两个函数改成一个,并且可以传递父类或者子类的对象

            如下:

            验证一下,我们先传入父类对象看是否报错:

     

            没有问题,那么我们传入子类:

            也没有问题。

            那么这是不是就达到我们的目的了?减少了重复代码的编写;确实减少了代码的编写,但是还没有达到我们的目的,我们传入子类对象输出一下: 

            我们子类明明有三个成员,但是为什么只输出了两个,这很明显他调用的并不是子类继承过去的print,而是调用的父类的print; 

            解决方法如下:

            首先我们需要在子类中重写一个print函数,记住是重写!重写函数的要求就是函数的返回值类型和函数名、参数列表等都必须与父类一致,那么我们重写的函数如下:

            为什么一定要用同样的函数名呢?因为我们Myprint函数中调用的就是print(),为了实现统一的接口,所以我们必须使用同样的函数名。

            那么这样做是不是就可以传入父类对象就调用父类的print,传入子类的对象就调用子类的print呢?我们通过输出结果来看一下:

            子类依旧无法输出我们想要的结果,这是为什么呢?

            下面就要介绍一个关键字了。

            virtual关键字 

            Virtual是C++ OO机制中很重要的一个关键字。只要是学过C++的人都知道在类Base中加了Virtual关键字的函数就是虚拟函数。

            基类的函数调用如果有virtual则根据多态性调用派生类的,如果没有virtual则是正常的静态函数调用,还是调用基类的。

            那么我们在基类的print前面加上virtual关键字,如下:

            传入父类对象:

            没问题,传入子类对象:

            也没问题。

            通过上面的内容就真真正正的达到了我们的目的,不仅实现了代码的复用,同时也实现了所谓的多态。 

            那么我们如何知道加virtual关键字就实现了多态呢?virtual底层实现多态的思想又是什么呢?

            virtual实现多态的底层逻辑

            我们先把virtual关键字去掉,然后再如下地方下断点:

            CTRL+ALT+F7重新编译、F5调试、ALT+8转到反汇编,如下:

            可以看出,没有virtual关键字的时候,底层是直接指定了调用Person::print()函数,在编译的时候就指定了调用父类的print,那么运行结果肯定不会是子类print输出的内容啊。

            那么virtual关键字下又是什么样的呢?加上关键字,依旧这里下断点,汇编如下:

            这就是virtual函数实现多态的思想:传入一个没有指定的地址

            没有virtual时:指定调用父类的print

            有virtual时   : 调用一个地址,这个地址可以是父类的print也可以是子类的print,需要看你传入的是哪个类的对象。

            那么virtual关键字底层又是怎么确定调用父类还是子类的函数呢?

            这个问题与virtual底层实现的虚表有关,关于虚表的概念以后会细讲,这里我们暂且知道virtual底层是如何实现多态的就行了(传入一个没有指定的地址);

            纯虚函数---抽象类

            我们知道virtual关键字修饰的函数是虚函数,那么纯虚函数是什么样的呢?

            这就是纯虚函数。

            纯虚函数有几个需要注意的特点:

            1、含有纯虚函数的类被称为抽象类,不能创建对象;

            2、虚函数可以直接使用,也可以被子类重写之后以多态的形式调用,而纯虚函数必须在子类中实现该函数才能使用;

            我们先来看第一点,我们使用纯虚类Person创建一个对象试试:

            创建失败,不过我们可以创建父类指针:

            编译成功!

            因为我们知道父类的指针是可以指向子类的,所以纯虚类(抽象类)不能创建对象其实对我们的影响不大;但是我们也必须记住这一点 ;

            再来看第二点:虚函数可以直接使用,也可以被子类重写之后以多态的形式调用,而纯虚函数必须在子类中实现该函数才能使用;

            也就是说,如果父类定义了纯虚函数,那么子类是必须要去实现这个函数的

            先整理一下代码,如下:

    1. #include
    2. #include
    3. class Person{
    4. public:
    5. int a;
    6. int b;
    7. Person(){};
    8. Person(int a,int b){
    9. this->a = a; this->b = b;
    10. }
    11. virtual void print() = 0;
    12. };
    13. class Teacher:public Person{
    14. public:
    15. int c;
    16. Teacher(){};
    17. Teacher(int a,int b,int c):Person(a,b)
    18. {
    19. this->c = c;
    20. }
    21. };
    22. int main()
    23. {
    24. system("pause");
    25. return 0;
    26. }

            定义一个父类对象和子类对象:

            都不可以,都是说类是抽象的

            我们先写上父类的实现,在外部:

            没用,还是抽象类;

            我们在子类中实现一下,把父类的实现删掉:

     

            子类成功创建对象。

            因为父类创建纯虚函数后,不管父类写没写纯虚函数的实现,都不能创建对象,所以我们也就不管父类了;因此我们纯虚函数的特点二大致可以总结为:如果父类写了virtual ... =0;纯虚函数,那么父类无论如何都不能创建对象,并且子类必须实现这个纯虚函数,也就是重写;

            另外,如果有第二个派生类继承了Person,那么它也必须实现这个纯虚函数,否则不能创建对象,这里我就不验证了,小伙伴们可以自己测试一下;

            讲了这么多,纯虚函数的意义是什么呢?有什么用呢?

            纯虚函数的用途

            如果父类没有什么用,可以说是工具类,那么就可以在父类中定义纯虚函数,子类继承这个父类时必须去实现这个函数,这也就达到了目的,这也就是我们常说的接口类;父类中含有纯虚函数一般目的都是为了实现接口类;

            直接看一个案例代码:

    1. #include
    2. #include
    3. #define PI 3.14
    4. class Shape // 形状类
    5. {
    6. public:
    7. virtual double area() = 0;
    8. };
    9. class Circular:public Shape // 圆类继承形状类
    10. {
    11. private:
    12. double r;
    13. public:
    14. Circular(double r)
    15. {
    16. this->r = r;
    17. }
    18. double area()
    19. {
    20. return PI*r*r;
    21. }
    22. };
    23. void printArea(Shape* s) // 输出
    24. {
    25. printf("%lf\n",s->area());
    26. }
    27. int main()
    28. {
    29. Circular c(1.2);
    30. printArea(&c);
    31. system("pause");
    32. return 0;
    33. }

            在上面的代码中,我们只写了一个子类(圆),这很难体现出接口(抽象类)的重要性,如果有多个子类同时继承这一个抽象类的话,那么接口的重要性以及多态就会变得更加明显

            首先我们定义了一个形状类Shape,然后定义了一个圆类继承了这个形状类;

            形状类里有一个函数,是返回面积的,但是我们形状类,你知道他是个什么形状吗?Shape没有任何被指定的形状,所以Shape中的double area没有任何意义啊,那没有意义你还实现它干嘛?你不知道他的形状那么你的函数实现(返回面积)怎么写?

            所以我们把Shape类中的area声明为一个纯虚类;提供接口;

            我们看printArea函数;我们知道抽象类父类不能创建对象,但是能创建指针指向子类,所以父类给printArea函数提供了指针;我们printArea中调用了area函数,又因为父类的area是纯虚函数,那么所有的子类都必须实现area这个函数,这也就给printArea中调用area的输出实现了多种可能,也就是多态;

            我们可以简单总结一下:如果基类中的函数没有任何实现的意义,那么可以定义为纯虚函数

    总结

            1、父类指针或引用可以直接指向子类对象;子类指针也可以指向父类对象,但没必要;

            2、C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

            3、virtual关键字修饰的函数称为虚函数,virtual是实现多态的关键。

            4、虚函数的目的是提供一个统一的接口,被继承的子类重写,以多态的形式调用;

            5、如果基类函数没有任何实现的意义,那么可以定义为纯虚函数virtual ... = 0;

            6、含有纯虚函数的类被称为抽象类,不能创建对象;

            7、虚函数可以直接被使用,也可以被子类重写以后以多态的形式调用,而纯虚函数必须在子类中实现该函数才可以使用;

    以上便是文章的所有内容,有过有讲得不好的地方,还请大家指出,谢谢观看!

  • 相关阅读:
    【活动系列】那些年写的比较愚蠢的代码
    弱监督学习
    第10讲:Vue组件的定义与注册
    pytest脚本常用的执行命令
    shiro授权
    QMetaType和QVariant使用
    linux系统中wifi移植方法
    基于深度学习的目标检测和语义分割:机器视觉中的最新进展
    SpringBoot jackson 配置localDate、localDateTime日期序列化,反序列化
    《向量数据库指南》——TruLens 用于语言模型应用跟踪和评估
  • 原文地址:https://blog.csdn.net/qq_52572621/article/details/127798869