C++笔记7:多态笔记再整理、多态原理,关于虚函数表,C++对象的内存布局

本文深入探讨C++中的多态性,包括静态多态与动态多态的概念,重点讲解动态多态的实现机制——虚函数表。解析虚函数表如何支持父类指针调用子类成员函数,实现多态效果。同时,讨论虚函数覆盖、多重继承下虚函数表的变化,以及虚函数表在对象内存布局中的位置。

笔记6中继承与多态都是比较基础的,也并不清楚很多原理,觉得深一点可能就不会了,可是多态又很重要,所以二一下,之后上完课再看看要不要补充个3,加油

关于多态,就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议


多态,顾名思义就是同一个事物在不同场景下的多种形态
多态有静态多态和动态多态之分,函数重载,泛型编程是静态多态,虚函数是动态多态。
静态多态是编译器在编译期间完成的,编译器会根据实参类型来选择调用合适的函数,如果有合适的函数可以调用就调,没有的ua就发出警告或者报错

下面具体讲动态多态:
动态多态是在程序运行时根据基类的引用(指针)指向的对象来确定自己具体调用哪一个类的虚函数。

动态多态的条件:
1,基类中必须包含虚函数,并且派生类中一定要对基类中的虚函数进行重写
2,通过基类对象的指针或者引用调用虚函数
3,基类和派生类中虚函数的原型必须保持一致(返回值类型,函数名称以及参数列表)。协变和析构函数除外,基类和派生类的析构函数是不一样的。

总结一题面试题:哪些函数不能定义为虚函数?

  1. 友元函数,他不是类的成员函数
  2. 全局函数
  3. 静态成员函数,它没有this指针
  4. 构造函数,拷贝构造函数,以及赋值运算符重载(可以但是一般不建议作为虚函数)

纯虚函数的形参列表后面要写上=0,包含纯虚函数的类叫做抽象类,也叫接口类,抽象类不能被实例化出对象,纯虚函数在派生类中重新定义以后,派生类才能实例化出对象,纯虚函数是一定要被继承的,否则它存在没有任何意义。

多态原理:

是要涉及到多态虚表之类的,
派生类虚表:
1,先将基类的虚表中的内容拷贝一份
2,如果派生类对基类中的函数进行重写,使用派生类的虚函数替换相同偏移量位置的基类虚函数
3,如果派生类中新增加自己的虚函数,按照其在派生类中的声明次序,放在上述虚函数之后

多态的缺陷:
1,降低了程序运行效率,因为多态需要去找虚表的地址
2,空间浪费


具体展开:

关于多态,就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议

虚函数表
一般继承,无虚函数覆盖
一般继承,有虚函数覆盖
多重继承,无虚函数覆盖
多重继承,有虚函数覆盖
C++对象的内存布局

虚函数表

虚函数(virtual function)是通过一张虚函数表(virtual table)来实现的,简称为V-table,在虚函数表中就是类的虚函数的地址表,这张表解决了继承、覆盖等问题。在有虚函数的类的实例中这个表被分配在了这个实例的内存中,当我们用父类的指针来操作一个子类的时候,虚函数表就像一个地图一样,指明了实际所应该调用的函数。

C++的编译器保证虚函数表的指针存在于对象实例中组前面的位置,这意味着我们通过对象实例的地址得到这张虚函数表然后就可以遍历其中函数指针,并调用相应的函数。

简单实例👇

    class Base {
         public:
                virtual void f() { cout << "Base::f" << endl; }
                virtual void g() { cout << "Base::g" << endl; }
                virtual void h() { cout << "Base::h" << endl; }
    };

通过Base实例得到虚函数表Base b;
在这里插入图片描述
最后虚函数表中的一个结点是虚函数表的结束标志,不同编译器不同的结束标志。

一般继承,无虚函数覆盖

看看继承时的虚函数表:
case1:
在这里插入图片描述子类没有重载任何父类的函数,那么在派生类的实例中,其虚函数表如下所示:
对于实例:Derive d; 的虚函数表如下
在这里插入图片描述1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。

一般继承,有虚函数覆盖

子类中有虚函数重载了父类的虚函数的情况:
在这里插入图片描述
其虚函数表:
在这里插入图片描述1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。

这样,对于下面的程序:

    Base *b = new Derive();
    b->f();

由于b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了,这就实现了多态。

多重继承,无虚函数覆盖

子类不覆盖父类函数的情况:
在这里插入图片描述
对于子类实例中的虚函数表:
在这里插入图片描述

我们可以看到:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
这样做是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数

多重继承,有虚函数覆盖

如果发生虚函数覆盖:

在这里插入图片描述
在这里插入图片描述
三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。

    Derive d;
    Base1 *b1 = &d;
    Base2 *b2 = &d;
    Base3 *b3 = &d;
    b1->f(); //Derive::f()
    b2->f(); //Derive::f()
    b3->f(); //Derive::f()
    b1->g(); //Base1::g()
    b2->g(); //Base2::g()
    b3->g(); //Base3::g()

安全性

  1. 如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。
    class Base {
        private:
                virtual void f() { cout << "Base::f" << endl; }
    };
    class Derive : public Base{
    };
    typedef void(*Fun)(void);
    void main() {
        Derive d;
        Fun  pFun = (Fun)*((int*)*(int*)(&d)+0);
        pFun();
    }
  1. 另外,任何妄图使用父类指针去调用子类中未覆盖父类的成员函数的行为都会被编译器视为非法,无法编译通过。但是却可以通过指针的方法访问虚函数表达到违法语义的行为。

===》

Base b;
Fun pFun = NULL;
cout << "虚函数表地址:" << (int*)(&b) << endl;
cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;
// Invoke the first virtual function
pFun = (Fun)*((int*)*(int*)(&b));
pFun();

运行结果:

在这里插入图片描述

通过这个示例,我们可以看到,我们可以通过强行把&b转成int *,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了通过这个示例,我们就可以知道如果要调用Base::g()和Base::h(),其代码如下:

    (Fun)*((int*)*(int*)(&b)+0);  // Base::f()
    (Fun)*((int*)*(int*)(&b)+1);  // Base::g()
    (Fun)*((int*)*(int*)(&b)+2);  // Base::h()

C++对象的内存布局

  1. 单一继承
    在这里插入图片描述在这里插入图片描述
    1)虚函数表在最前面的位置。
    2)成员变量根据其继承和声明顺序依次放在后面。
    3)在单一的继承中,被overwrite的虚函数在虚函数表中得到了更新。

  2. 多重继承在这里插入图片描述
    在这里插入图片描述1) 每个父类都有自己的虚表。
    2) 子类的成员函数被放到了第一个父类的表中
    3) 内存布局中,其父类布局依次按声明顺序排列。
    4) 每个父类的虚表中的f()函数都被overwrite成了子类的f()。这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

  3. 重复继承
    在这里插入图片描述
    在这里插入图片描述
    B在D中有两个实例,造成了数据的重复,于是引入虚基类概念

  4. 多重虚拟继承
    在这里插入图片描述只需要把B1和B2继承B的语法中加上virtual 关键,就成了虚拟继承:

    class B {……};
    class B1 : virtual public B{……};
    class B2: virtual public B{……};
    class D : public B1, public B2{ …… };
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值