• 【C++】多态


    通俗来说,多态就是多种形态,当不同的对象去完成时某个行为时会产生出不同的状态。


    1.多态的定义及实现

    1.1多态的构成条件

    多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了 Person。Person对象买票全价,Student对象买票半价。

    那么在继承中要构成多态还有两个条件:

    1. 必须通过基类的指针或者引用调用虚函数

    2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

    1.2虚函数 

    1. class Person {
    2. public:
    3. virtual void BuyTicket() { cout << "买票-全价" << endl;}
    4. };

    1.3虚函数的重写

    虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的 返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

    在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用

    虚函数重写的两个例外:

    1. 协变(基类与派生类虚函数返回值类型不同)

    派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

    1. class A{};
    2. class B : public A {};
    3. class Person {
    4. public:
    5. virtual A* f() {return new A;}
    6. };
    7. class Student : public Person {
    8. public:
    9. virtual B* f() {return new B;}
    10. };

    2. 析构函数的重写(基类与派生类析构函数的名字不同)

    如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写。虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

    如果不将基类的析构函数设为虚函数反而会遇到问题。

    比如:

    1. #include<iostream>
    2. using namespace std;
    3. class Person
    4. {
    5. public:
    6. ~Person()
    7. {
    8. cout << "~Person" << endl;
    9. }
    10. };
    11. class Student :public Person
    12. {
    13. public:
    14. ~Student()
    15. {
    16. cout << "~Student" << endl;
    17. }
    18. };
    19. int main()
    20. {
    21. Person* p2 = new Student();
    22. delete p2;
    23. return 0;
    24. }

    这段代码明明new的是Student对象,但是却没有调用Student的析构函数,会造成内存泄漏 。

     当把基类的析构函数变成虚函数后,满足了多态的条件(父类的指针,指向子类的对象。调用虚函数)就完成了析构函数的重写。

    1.4override 和 final

    1. final:修饰虚函数,表示该虚函数不能再被重写

    当出现下面这种情况就有可能用到final

     B继承A,C继承B,B重写了func函数,但不想它的孩子重写这个函数

     2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

     1.5重载、覆盖(重写)、隐藏(重定义)的对比

    重载:

    1、两个函数在同一作用域

    2、函数名相同,参数不同

    重写(覆盖):

    1、两个函数分别在基类和派生类的作用域

    2、函数名/参数/返回值必须想用(协变除外)

    3、函数必须是虚函数

    重定义(隐藏):

    1、两个函数分别在基类和派生类的作用域

    2、函数名同名

    3、两个基类和派生类的同名函数不构成重写就是重定义

    2、抽象类

    2.1概念

    在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

    2.2 接口继承和实现继承

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

    下面这段代码会输出什么呢?

    1. class A
    2. {
    3. public:
    4. virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
    5. virtual void test() { func(); }
    6. };
    7. class B : public A
    8. {
    9. public:
    10. void func(int val = 0) { std::cout << "B->" << val << std::endl; }
    11. };
    12. int main(int argc, char* argv[])
    13. {
    14. B* p = new B;
    15. p->test();
    16. return 0;
    17. }

     3.多态的原理

     3.1虚函数表

    1. //sizeof(Base)是多少?
    2. class Base
    3. {
    4. public:
    5. virtual void Func1()
    6. {
    7. cout << "Func1()" << endl;
    8. }
    9. private:
    10. int _b = 1;
    11. };

     通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些 平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代 表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

     对上面代码进行一个改造,增加了一个子类Derive,在子类中重写了Func1,在Base类中增加了虚函数Func2、和普通函数Func3。

    1. class Base
    2. {
    3. public:
    4. virtual void Func1()
    5. {
    6. cout << "Base::Func1()" << endl;
    7. }
    8. virtual void Func2()
    9. {
    10. cout << "Base::Func2()" << endl;
    11. }
    12. void Func3()
    13. {
    14. cout << "Base::Func3()" << endl;
    15. }
    16. private:
    17. int _b = 1;
    18. };
    19. class Derive : public Base
    20. {
    21. public:
    22. virtual void Func1()
    23. {
    24. cout << "Derive::Func1()" << endl;
    25. }
    26. private:
    27. int _d = 2;
    28. };
    29. int main()
    30. {
    31. Base b;
    32. Derive d;
    33. return 0;
    34. }

     1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。

    2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

    3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。

    4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

    5. 总结一下派生类的虚表生成:

    a.先将基类中的虚表内容拷贝一份到派生类虚表中

    b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数

    c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

    6. 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。同一个类型的对象虚表是一样的,所以虚表并不所随着对象的创建而创建,对象的销毁而销毁,所以虚表只能存在静态区(数据段)和常量区(代码段),是静态区还是常量区在讲完多态原理再说。

    3.2多态的原理

    普通调用是在编译是确定函数调用的地址,而多态调用是在运行是通过查虚函数表来确定函数调用的地址.

     注意只有父类的指针或者引用才能触发多态,父类的对象是不行的,对象的拷贝是不拷贝虚表的。

     VS编译器的监视窗口有可能显示不完全

    比如:

    我们在子类中加了virtual void Func4()这个虚函数,但是在虚表中它是没有显示出来的。

     

     在内存窗口中可以看到有两个地址和虚函数表中的地址是可以对应上的,我们接下来看一下内中中的第三个地址是不是virtual void Func4()这函数

     可以发现00e8106e这个地址存放的确实是虚函数Fucn4()

     知道了如何找到虚表后,我们就可以看看虚表存在什么地方了

    虚表和常量区挨得近,并且虚函数地址和test()地址的挨得近的,所以虚表中存的是虚函数的地址,而虚函数和其他函数存在一起

    3.2多继承中的虚函数表

    1. class Base1 {
    2. public:
    3. virtual void func1() { cout << "Base1::func1" << endl; }
    4. virtual void func2() { cout << "Base1::func2" << endl; }
    5. private:
    6. int b1;
    7. };
    8. class Base2 {
    9. public:
    10. virtual void func1() { cout << "Base2::func1" << endl; }
    11. virtual void func2() { cout << "Base2::func2" << endl; }
    12. private:
    13. int b2;
    14. };
    15. class Derive : public Base1, public Base2 {
    16. public:
    17. virtual void func1() { cout << "Derive::func1" << endl; }
    18. virtual void func3() { cout << "Derive::func3" << endl; }
    19. private:
    20. int d1;
    21. };
    22. typedef void(*VFPTR) ();
    23. void PrintVTable(VFPTR vTable[])
    24. {
    25. cout << " 虚表地址>" << vTable << endl;
    26. for (int i = 0; vTable[i] != nullptr; ++i)
    27. {
    28. printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
    29. VFPTR f = vTable[i];
    30. f();
    31. }
    32. cout << endl;
    33. }
    34. int main()
    35. {
    36. Derive d;
    37. VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
    38. PrintVTable(vTableb1);
    39. VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
    40. PrintVTable(vTableb2);
    41. return 0;
    42. }

    ​​​​​​​

     

  • 相关阅读:
    Springboot自定义@Import自动装配
    【JavaScript 19】Array对象
    什么是响应式设计?响应式设计的基本原理是什么?如何实现
    Unity URP14.0 自定义后处理框架
    【星海出品】操作系统C语言小例子
    [附源码]java毕业设计英语知识竞赛报名系统
    微信小程序云开发教程——墨刀原型工具入门(常用组件)
    设计模式之单例模式
    运维:记一次寻找定时任务并删除的经历
    SAP ABAP OData 服务如何支持 $filter (过滤)操作试读版
  • 原文地址:https://blog.csdn.net/holle_world_ldx/article/details/126810027