• 【C++】多态/虚函数/虚表指针,虚表


    多态

    多态分为

    1. 静态联编(早绑定)------编译时的多态:函数重载(名字粉碎技术)和运算符重载
    2. 动态联编(晚绑定)------运行时多态:简单来说,就是程序在运行前不能根据函数名和参数确定调用那个函数,必须在程序执行过程中,根据执行的具体情况来动态确定,它是通过类公有继承关系中的虚函数来实现的。目的也是为了建立一种通用程序,通用性是程序追求的目标之一。

    虚函数

    定义:虚函数是一个类的成员函数。
    语法:virtual 返回类型 函数名参数列表()
    关键virtual指明该成员为虚函数,仅用在类的定义中,如果虚函数在类外定义可以不加virtual

    虚函数的特点:

    当某一个类的成员函数被定义为虚函数,那么由该类派生出来的所有派生类,该函数都保持虚的特征。

    class Animal
    {
    private:
        string name;
    public:
        Animal(const string& na) : name(na) {}
    public:
        virtual void eat() {}
        virtual void walk() {}
        virtual void PrintInfo() {}
    
        string& get_name() { return name; }
        const string& get_name()const { return name; }
    };
    
    class Dog : public Animal
    {
    private:
        string owner;
    public:
        Dog(const string& ow, const string& na) : Animal(na), owner(ow) {}
    
        virtual void eat() { cout << "Dog eat: bone" << endl; }
        virtual void walk() { cout << "Dog walk: run" << endl; }
        virtual void PrintInfo()
        {
            cout << "Dog owner 's name: " << owner << endl;
            cout << "Dog name: " << get_name() << endl;
        }
    };
    
    class Cat : public Animal
    {
    private:
        string owner;
    public:
        Cat(const string& ow, const string& na) : Animal(na), owner(ow) {}
    
        virtual void eat() { cout << "Cat eat: fish" << endl; }
        virtual void walk() { cout << "Cat walk: silent" << endl; }
        virtual void PrintInfo()
        {
            cout << "Cat owner 's name: " << owner << endl;
            cout << "Cat name: " << get_name() << endl;
        }
    };
    //如果派生类传递基类的引用或指针,再以基类指针调用虚函数
    void fun(Animal& animal)
    {          //指针*animal
        animal.eat();
        animal.walk();
        animal.PrintInfo();
    }
    int main(void)
    {
        Dog dog("嘟嘟", "哈士奇");
        Cat cat("baiU", "喵酱");
    
        fun(dog);
        fun(cat);
    
        return 0;
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    实现动态联编的注意点:

    1.必须使用公有继承
    2.类中的类方法必须是虚函数
    3.要实现运行时的多态,必须用指针或者引用来调用虚函数。

    为什么要是用公有继承?

    原因:因为私有继承不代表是一个的意思)就是外部函数无法访问派生类的私有和保护属性。比如:猫是动物的一种。

    使用虚函数的注意事项:

    1. 派生类中定义的虚函数必须和基类中定义的虚函数同名,同返回类型,同参数列表,否则会被认为是重载,而不是虚函数。
      如:基类返回,基类指针,派生类返回,派生类指针是允许的。
    //这是函数的重载。
    class Object
    {
    public:
    virtual Object*fun() {}
    };
    class Base
    {
      public:
      virtual Base*fun(){}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    1. 只有类的成员函数才能被说明为虚函数,因为虚函数仅适用于有继承关系的类对象。
    2. 静态成员,是所有同一类对象公有,不能作为虚函数。
    3. 内联函数不能作为虚函数,每个对象一个拷贝,没有映射关系。
    4. 实现动态多态性时,必须使用基类类型的,指针或引用,使该指针指向该基类的不同派生类的对象,并通过该指针指向虚函数,才能实现动态多态。
      如:
    void fun(Animal& animal)
    {          //指针*animal
        animal.eat();  //&amimal就是基类引用
        animal.walk();
        animal.PrintInfo();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1. 析构函数可以被定义为虚函数,构造函数不可以被定义为虚函数,因为调用构造函数时,对象还没有完成实例化(也就是对象没有内存空间,如何保存访问虚表的指针?)
      在基类和派生类中动态分配内存空间,必须把析构函数定义为虚函数,实现撤销对象时的多态性。
    2. 函数执行速度要稍慢一点,为了实现多态性,每一个派生类均要保存相应的虚函数的入口地址表,函数的调用机制是间接实现的,所以多态性总要付出一定的代价,但是通用性是一个更高的目标。
    3. 如果定义在类外,virtual只能加载函数声明前面,不能(再)再函数定义前面,正确的定义必须不包括virtual。

    虚函数表,和虚表指针的概念

    实际上,运行时的多态是因为虚函数表的存在,如果设计的类中有虚函数,那么在编译阶段就会生成虚函数指针和虚函数表,里面存放了各个虚函数的函数指针。
    如果派生类重写了基类的虚函数,那么派生类的虚函数就覆盖虚函数表里的基类虚函数。

    class Object
    {
    private:
        int value;
    public:
        Object(int x = 0) :value(x) {}
        virtual void add() { cout << "object::add" << endl; }
        virtual void fun() { cout << "object::fun" << endl; }
    };
    class Base :public Object
    {
    private:
        int sum;
    public:
        Base(int x = 0) :Object(x + 10), sum(x) {}
        virtual void add() { cout << "Base::add" << endl; }
        virtual void fun() { cout << "Base::fun" << endl; }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    int main()
    {
        Base base(10);
        Object* op = &base;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    1.首先base调用构造函数,发现继承了Object,因此会先调用基类的构造函数,构造隐藏基类对象。
    2.在构造时发现,基类中有虚函数,就会在隐藏基类对象中生成指向基类中虚函数的指针(vfptr)。如下图:
    隐藏基类对象的虚表指针vfptr指向的基类的虚表

    请添加图片描述

    请添加图片描述
    3.接下来返回Base类中,调用构造函数,构建base对象,发现Base类中也有虚函数。
    4.接下来就会让隐藏基类对象中的vfptr指针指向派生类的虚函数表。(虚函数函数在数据区只有一份)
    如下图:
    隐藏基类对象的虚表指针指向派生类的虚表
    如果重写基类的虚函数,就会覆盖基类的虚函数。
    请添加图片描述

    请添加图片描述

    注意:(只有基类的对象中有虚表指针(只有一个))

    1. 如果使用"对象.函数名()",不管方法是否是虚函数,都调用该对象的方法。

    2. 如果使用基类的指针或者引用指向派生类,那么调用的虚方法时,会采用动态联编,调用派生类的虚方法。
      换句话说,基类指针和引用是通过基类的虚表指针查虚表的方式,实现动态绑定

    Object* op = &base;
    
    • 1

    基类指针指向base对象,Base类中有隐藏基类对象和base对象两个成员,隐藏基类对象内存中的虚表指针,指向Base类中的虚表。
    请添加图片描述

    构造函数为什么不能作为虚函数?

    1. 存储角度,虚函数都对应一个指向vtable虚表的指针,这个指向虚表的指针实际上存储在对象的内存空间中,而对象是通过调用构造函数进行实例化后才有内存空间,那构造函数如果是虚函数,对象如何保存指向虚表的指针。
    2. 使用角度,虚函数通常都是通过父类的指针或者引用来调用子类的成员函数,而构造函数是在创建对象的时候自动调用。不可能通过父类的指针或者引用来调动

    从汇编角度来看动态联编过程

    int main()
    {
        Base base(10);
        Object* op = &base;
        op->add();
        op->fun();
        op->printf();![请添加图片描述](https://img-blog.csdnimg.cn/cb13a15f698b417ead5a4ea206cd039f.png)
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    虚表:存放的是虚函数的指针。

    请添加图片描述

     op->add(); 
    
    • 1
    1. 先把op的地址放入eax,
    2. 再从eax中取出虚表的地址放入edx
    3. 再从edx中取出虚表的第一个地址,这个地址就是第一个虚函数的地址。
      请添加图片描述

    执行这一步时:
    请添加图片描述
    会跳转到这:
    请添加图片描述

     op->fun();
    
    • 1

    前两步操作和op->fun()相同,从edx中取地址,要加4个字节,取的是虚表中第二个函数的地址。
    (edx+4),因为在虚表中存放的是虚函数指针。
    请添加图片描述

    三层继承时的虚表:

    class Object
    {
    private:
        int value;
    public:
        Object(int x = 0) : value(x) {}
    
        virtual void add() { cout << "Object::add" << endl; }
        virtual void fun() { cout << "Object::fun" << endl; }
        virtual void Print() { cout << "Object::Print" << endl; }
    };
    
    class Base : public Object
    {
    private:
        int sum;
    public:
        Base(int x = 0) : Object(x + 10), sum(x) {}
        virtual void add() { cout << "Base::add" << endl; }
        virtual void fun() { cout << "Base::fun" << endl; }
        virtual void show() { cout << "Base::show" << endl; }
    };
    class test :public Base
    {
    private:
        int num;
    public:
        test(int x = 0) : Base(x + 10), num(x) {}
        virtual void add() { cout << "Base::add" << endl; }
        virtual void fun() { cout << "Base::fun" << endl; }
        virtual void show() { cout << "Base::show" << endl; }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    请添加图片描述
    文字描述:
    在Object类中可以看见三个虚函数:

     virtual void add() 
     virtual void fun() 
     virtual void Print() 
    
    • 1
    • 2
    • 3

    在Base中可以看到四个虚函数

    virtual void add() 
    virtual void fun() 
    virtual void Print() 
    virtual  void show()
    
    • 1
    • 2
    • 3
    • 4

    在Test中可以看到四个虚函数

    virtual void add() 
    virtual void fun() //是继承Base的,Object的被覆盖了
    virtual void Print() 
    virtual  void show()
    
    • 1
    • 2
    • 3
    • 4

    而虚表指针是在Object类的隐藏基类对象的内存空间中。指向的是Test类中的虚表。
    创建基类指针:

    int main()
    {
    Test t1(10); 
    Object*op;
    Base*bp;
    Test*tp;
    op = &t1;
    bp =&t1;
    tp = &t1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    请添加图片描述
    请添加图片描述
    均能访问到。

    使用对象调用普通方法时产生的动态联编:

    class Object
    {
    private:
    	int value;
    public:
    	Object(int x = 0) : value(x) {}
    
    	virtual void add() { cout << "Object::add" << endl; }
    	virtual void fun() { cout << "Object::fun" << endl; }
    	virtual void Print() { cout << "Object::Print" << endl; }
    
    	void function()
    	{
    		fun();
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    int main(void)
    {
        Test t1;
        Base b1;
        Object obj;
    
        t1.function();
        b1.function();
        obj.function();
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    执行完:

     Test t1;
        Base b1;
        Object obj;
    
    • 1
    • 2
    • 3

    t1, b1, obj的虚表状态。

    请添加图片描述

    1. 当执行 t1.function();,this指针里的虚表指针指向的是Test类中的虚表,查询虚表,调用虚函数fun(),如下图:
      请添加图片描述
      请添加图片描述

    2. 执行b1.function();和1一样。

    3. 请添加图片描述

    4. 执行obj.function()也和1一样。

    5. 请添加图片描述

    结果
    请添加图片描述

  • 相关阅读:
    net-java-php-python-网络安全教育学习网站计算机毕业设计程序
    SpringMVC自定义注解
    R语言dplyr包select函数筛选dataframe数据中以指定字符串结尾(end with)的数据列(变量)
    【实践篇】教你玩转JWT认证---从一个优惠券聊起
    便携式电热水壶外贸出口欧洲CE认证准备资料
    安防视频平台EasyCVR视频调阅全屏播放显示异常是什么原因?
    wsl端口转发远程链接
    23种设计模式
    论文阅读:攻击者的《银河系漫游指南》:开展对互联网资源供应商的路径外攻击以劫持互联网资源 the Hijackers‘ Guide To The Galaxy
    神经网络(十七)RCNN及其变体的概述
  • 原文地址:https://blog.csdn.net/weixin_52958292/article/details/127545179