C++类的三大特性之多态

一:多态

<1> 多态的概念

  • 多态是一个比较抽象的概念,通俗的说,就是不同对象完成同一行为时表现出不同的状态
  • 比如儿童和成人去游乐园玩买票,都是买票这一行为,但是儿童买票半价,成人买票全价,这就是多态的一种体现
  • 换成编程的说法就是,多态是在不同继承关系的类对象,去调用同一个函数,产生了不同的行为
  • 我们可以理解成有两个类,一个类为Person,一个类为Children,并且Children继承了Person类,它们都有Buyticket这个函数,但是Person类调用这个函数时和Children类调用这个函数是发生的行为不同
    在这里插入图片描述

<2> 多态的构成条件

  • 如上图,这就是一个多态的实现方法,要构成多态必须满足以下两个条件
    1. 必须通过基类的指针或引用调用虚函数
    2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数完成重写

    在这里插入图片描述
  • 上图说明通过基类对象调用虚函数是不构成多态的,必须通过基类的指针或引用
  • 而我们把virtual关键字删去,使其不为虚函数,也就不构成多态,无论是使用基类的对象,指针,还是引用,都是调用的基类的函数
#include"Blog.h"
class Person
{
public:
     void Buyticket()
    {
        cout << "全价" << endl;
    }
};
class Student :public Person
{
public:
     void Buyticket()
    {
        cout << "半价" << endl;
    }
};
void Fun1(Person p)
{
    p.Buyticket();
}
void Fun2(Person& p)
{
    p.Buyticket();
}
void Fun3(Person* p)
{
    p->Buyticket();
}
int main()
{
    Person p;
    Student s;
    Fun1(p);
    Fun1(s);
    cout << endl;
    Fun2(p);
    Fun2(s);
    cout << endl;
    Fun3(&p);
    Fun3(&s);
    return 0;
}

在这里插入图片描述

二:虚函数

<1> 虚函数的概念

  • 前面多态的构成条件里提到了虚函数,那么什么是虚函数?
  • 很简单,我们只要在函数之前加上virtual关键字,那么这个函数就变成了虚函数
  • 像这样:virtual void Buyticket()

<2> 虚函数的重写

  • 我们要实现多态,除了将函数改为虚函数之后,我们还需要对虚函数进行重写,那么虚函数重写是什么?
  • 派生类有一个和基类完全相同的虚函数(即函数名,参数类型,返回类型完全相同),那么我们就说派生类重写了基类的虚函数
  • 这么一说,是不是觉得和重定义(隐藏),和函数重载很相似?但是它们三者直接是不同的
    在这里插入图片描述

<3> 虚函数重写的两个例外

  • 前文强调完成虚函数重写必须保证函数名,参数,返回类型均相同,但是有两种情况我们可以不满足这一点

1.协变

  • 当基类虚函数返回另一个基类的对象或指针,派生类也返回所对应基类的派生类的对象或指针时,虽然基类和派生类的返回类型不同,但是仍构成虚函数重写,我们把这种情况叫做协变
  • 比如A是一个基类,B是A的派生类,Person类的虚函数返回A&或A时,Children类返回B&或B,这就构成了协变
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;
    }
};
void Fun1(Person p)
{
    p.Buyticket();
}
void Fun2(Person& p)
{
    p.Buyticket();
}
void Fun3(Person* p)
{
    p->Buyticket();
}
int main()
{
    Person p;
    Student s;
    Fun1(p);
    Fun1(s);
    cout << endl;
    Fun2(p);
    Fun2(s);
    cout << endl;
    Fun3(&p);
    Fun3(&s);
    return 0;
}

在这里插入图片描述

2.析构函数的重写

  • 基类和派生类的析构函数的函数名不同,但是也构成虚函数的重写
  • 并且只要基类的析构函数前加了virtual,即基类的析构函数为虚函数,那么不管派生类的析构函数前是否加上virtual,都与基类的析构函数构成重写
  • 这么做的原因是保证派生类的析构函数一定会完成重写,因为派生类的析构函数如果不构成重写,在某些场景可能会出问题
    在这里插入图片描述
  • 这里函数名不同还是构成重写看起来有些不河里
  • 但其实我们可以理解为编译器之后会把这两个函数都处理为destructor来保证它们两个的函数名相同

<4> override和final

  • final加在虚函数后表示这个虚函数不能再重写,加在类后表明这个类不能再被继承
    在这里插入图片描述
    在这里插入图片描述
  • override检查派生类函数是否完成重写,没有完成则编译报错
    在这里插入图片描述

三:抽象类

<1> 抽象类的概念

  • 在虚函数后加上=0,则这个函数我们称为纯虚函数
  • 我们把包含了纯虚函数的类叫做抽象类,也叫做接口类,抽象类是不能实例化出对象的
  • 抽象类的派生类也无法实例化出对象,只有重写虚函数,才能实例化出对象
    在这里插入图片描述
    在这里插入图片描述

<2> 抽象类的作用

  • 可以更好的表示现实世界中,没有实例对象对应的抽象类型
  • 体现了接口继承,强制子类去重写虚函数(不重写,子类也是抽象类,无法实例化出对象)

<3> 接口继承和实现继承

  • 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现
  • 虚函数的继承是基类虚函数的接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态。所以不实现多态,不要把函数定义成虚函数

四:多态的原理

<1> 虚函数表

class Person 
{
public:
    virtual void Buyticket() 
    {
        cout << "全价" << endl;
    }
private:
    int _a;
};
int main()
{
    cout << sizeof(Person) << endl;
    return 0;
}
  • 先来做个题,sizeof(Person)是多少?
  • 也许很多人都觉得是4,只有一个成员_a的大小,但是很遗憾,在32位平台下,答案是8,在64位平台下,答案是16
    在这里插入图片描述
    在这里插入图片描述
  • ???发生什么事了,怎么会是这个答案
  • 因为Person类除了有成员变量_a,还有一个指针,我们成它为虚函数表指针,简称虚表指针
    在这里插入图片描述
  • 这个虚表指针指向的是一个函数指针数组,而那个数组里存的就是一个个函数指针,也代表Person类中的虚函数
  • 这里注意与前面菱形继承中的虚基表作区分,虚基表是指向一块空间,那块空间里存的是距离基类的偏移量
  • 现在为了搞清楚虚表里面究竟会存哪些函数,我们对上述代码作些许改变
  • Person类一共有三个函数Fun1,Fun2,Fun3,我们将Fun1,Fun2置为虚函数,Fun3为普通函数
  • 构造Children类继承Person类,并重写Person类中的Fun1
class Person 
{
public:
    virtual void Fun1() 
    {
        cout << "Person::Fun1" << endl;
    }
    virtual void Fun2()
    {
        cout << "Person::Fun2" << endl;
    }
    void Fun3()
    {
        cout << "Person::Fun3" << endl;
    }
private:
    int _a;
};
class Student :public Person
{
public:
    virtual void  Fun1() 
    {
        cout << "Student::Fun1" << endl;
    }
};
int main()
{
    Person p;
    Student s;
    return 0;
}

在这里插入图片描述

  • 通过观察两个对象中虚表存储的值,我们可以发现
  1. 派生类对象s中也有一个虚表指针,s对象由两部分构成,一部分是父类继承下来的成员,一部分是自己的成员
  2. 基类p对象和派生类s对象的虚表不同,因为派生类完成了对Fun1的重写,所以s的虚表中存储的是重写的Children::Fun1,所以虚函数的重写我们也称为覆盖,指的就是派生类重写虚函数后对虚表的的覆盖
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
  5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  6. 这里还有一个很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。但是很多人都是这样深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际验证一下会发现vs下是存在代码段的

<2> 原理

  • 将这么久的虚函数表,所以多态原理究竟是个啥?
  • 别着急,将虚函数表就是为了将多态原理做铺垫的
    在这里插入图片描述
  1. 观察上图的橘色箭头我们看到,p是指向Person对象时,p->Fun1在Person的虚表中找到虚函数是
    Person::Fun1。
  2. 观察上图的红色箭头我们看到,p是指向Children对象时,p->Fun1在Children的虚表中找到虚函数
    是Children::Fun1。
  3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
  4. 满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。
  • 这时我们再想想,达成多态的两个条件,一个是虚函数重写,一个是基类对象的指针或引用调用虚函数,而对象不行,为什么?
  • 父类指针和引用,在切片时指向或者引用父类和子类对象中切出来的一部分,这部分是包括虚表指针的,因为虚表指针在父类中
  • 而为父类对象时,切片只会拷贝成员变量过去,不会拷贝虚表指针过去,所以调用虚函数时无法通过虚表指针找到对应派生类的虚函数,只能直接调用基类的虚函数,就无法形成多态

<3> 动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

五:单继承和多继承的虚函数表

<1> vs下对虚表的隐藏

  • 肿么回事?前面不是已经讲过虚函数表了么,怎么又梅开二度?
  • 主要目的是为了讲讲多继承的虚函数表,但是首先我们需要注意,在VS2019下调试时看到的虚表可能并不真实
#define _CRT_SECURE_NO_WARNINGS
#include"Blog.h"
class Person 
{
public:
    virtual void Fun1() 
    {
        cout << "Person::Fun1" << endl;
    }
    virtual void Fun2()
    {
        cout << "Person::Fun2" << endl;
    }
private:
    int _a;
};
class Childeren :public Person
{
public:
    virtual void  Fun1() 
    {
        cout << "Children::Fun1" << endl;
    }
    virtual void  Fun3()
    {
        cout << "Children::Fun3" << endl;
    }
    virtual void  Fun4()
    {
        cout << "Children::Fun4" << endl;
    }
private:
    int _b;
};
void Fun(Person* p)
{
    p->Fun1();
}
int main()
{
    Person p;
    Childeren s;
   
    return 0;
}

在这里插入图片描述

  • 可以看到,s对象的虚表中只有重写了的Fun1和从Person类中继承下来的Fun2,那么还有Fun3和Fun4呢,是不存在虚表里面么,其实不是,这是因为vs对我们隐藏了
  • 那么我们该如何看到真实的虚表呢?
  • 我们首先先定义一个函数指针typedef void (*VFPTR) (),然后我们对对象p取地址再强制类型转换为int*,再对齐解引用,我们就得到了p对象头4bytes的值,这个也就是我们的虚表指针
  • 然后我们利用虚表指针对虚表进行一个遍历,就能看到我们所有的虚函数了
#define _CRT_SECURE_NO_WARNINGS
#include"Blog.h"
class Person 
{
public:
    virtual void Fun1() 
    {
        cout << "Person::Fun1" << endl;
    }
    virtual void Fun2()
    {
        cout << "Person::Fun2" << endl;
    }
private:
    int _a;
};
class Childeren :public Person
{
public:
    virtual void  Fun1() 
    {
        cout << "Children::Fun1" << endl;
    }
    virtual void  Fun3()
    {
        cout << "Children::Fun3" << endl;
    }
    virtual void  Fun4()
    {
        cout << "Children::Fun4" << endl;
    }
private:
    int _b;
};
void Fun(Person* p)
{
    p->Fun1();
}
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
    cout << " 虚表地址" << vTable << endl;
    for (int i = 0; vTable[i] != nullptr; i++)
    {
        printf("第%d个虚函数地址 :0X%x,->", i+1, vTable[i]);
        VFPTR f = vTable[i];
        f();
    }
    cout << endl;
}
int main()
{
    Person p;
    Childeren s;
    VFPTR* vTablep = (VFPTR*)(*(int*)&p);
    PrintVTable(vTablep);
    VFPTR* vTables = (VFPTR*)(*(int*)&s);
    PrintVTable(vTables);
    return 0;
}

在这里插入图片描述

<2> 多继承情况下的虚函数表

  • 我们新增一个Student类,让Children类同时继承Person和Student类,并让Children只重写Fun1
  • 由监视窗口我们可以知道,现在s对象里面有两个虚表了,一个是继承的Person类的,一个是继承的Student类的,而Children重写了Fun1,那么Person类和Student类里面的虚表都会被覆盖
  • 这时候我们又发现监视窗口不对劲了,Children自己的虚函数呢?
  • 于是我们通过之前的方法自己打印虚表,可知Children类自己的虚函数只存在于第一张虚表里面
#define _CRT_SECURE_NO_WARNINGS
#include"Blog.h"
class Person 
{
public:
    virtual void Fun1() 
    {
        cout << "Person::Fun1" << endl;
    }
    virtual void Fun2()
    {
        cout << "Person::Fun2" << endl;
    }
private:
    int _a;
};
class Student
{
public:
    virtual void Fun1()
    {
        cout << "Student::Fun1" << endl;
    }
    virtual void Fun2()
    {
        cout << "Student::Fun2" << endl;
    }
private:
    int _b;
};
class Childeren :public Person,public Student
{
public:
    virtual void  Fun1() 
    {
        cout << "Children::Fun1" << endl;
    }
    virtual void  Fun3()
    {
        cout << "Children::Fun3" << endl;
    }
    virtual void  Fun4()
    {
        cout << "Children::Fun4" << endl;
    }
private:
    int _c;
};
void Fun(Person* p)
{
    p->Fun1();
}
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
    cout << " 虚表地址" << vTable << endl;
    for (int i = 0; vTable[i] != nullptr; i++)
    {
        printf("第%d个虚函数地址 :0X%x,->", i+1, vTable[i]);
        VFPTR f = vTable[i];
        f();
    }
    cout << endl;
}
int main()
{
    Person p;
    Childeren s;
    VFPTR* vTable1 = (VFPTR*)(*(int*)&s);
    PrintVTable(vTable1);
    VFPTR* vTable2 = (VFPTR*)(*(int*)((char*)&s+sizeof(Person)));
    PrintVTable(vTable2);
    return 0;
}

在这里插入图片描述

  • 在这里插入图片描述

六:菱形虚拟继承中的虚函数表和虚基表

  • 在菱形虚拟继承中虚函数表就更复杂了,这里只讨论最简单情况下虚基表和虚表的关系
  • 我们有ABCD四个类,A为基类,BC都虚继承A,D再继承B和C,这就构成了一个菱形虚拟继承
  • 在这样的继承中,如果B,C有自己的虚函数的话,那么B,C不仅会有自己的虚表,并且B.C的虚基表中还会存储有关虚表的偏移量
#define _CRT_SECURE_NO_WARNINGS
#include"Blog.h"
class A 
{
public:         
    virtual void Fun() 
    {
        cout << "A::Fun" << endl;
    }

    int _a;
};
class B :virtual public A
{
public:
    virtual void Fun()
    {
        cout << "B::Fun" << endl;
    }
    virtual void Fun1()
    {
        cout << "B::Fun1" << endl;
    }

    int _b;
};
class C :virtual public A
{
public:
    virtual void  Fun() 
    {
        cout << "C::Fun" << endl;
    }
    virtual void  Fun1()
    {
        cout << "C::Fun1" << endl;
    }

    int _c;
};
class D :public B,public C
{
public:
    virtual void  Fun()
    {
        cout << "D::Fun" << endl;
    }
    virtual void  Fun1()
    {
        cout << "D::Fun1" << endl;
    }

    int _d;
};
int main()
{
    D tmp;
    tmp._a = 1;
    tmp._b = 2;
    tmp._c = 3;
    tmp._d = 4;
    return 0;
}

在这里插入图片描述

在这里插入图片描述

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dhdw

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值