• C++进阶篇2---多态


    1.多态的概念

    多态的概念:通俗来说,就是多种形态,具体点就是当不同的对象,去完成某个行为,会产生不同的状态

    举个例子:同样是吃饭,狗吃狗粮,猫吃猫粮,不同的对象,对于同一个行为会有不同的状态

    2.多态的定义和实现

    2.1虚函数

    虚函数:即被virtual修饰的类成员函数,注意是类成员函数,其他函数不能被virtual修饰

    1. class A {
    2. public:
    3. virtual void func() {
    4. cout << "hello C++" << endl;
    5. }
    6. };

    2.2虚函数的重写

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

    1. class A {
    2. public:
    3. virtual void func() {
    4. cout << "hello C++" << endl;
    5. }
    6. };
    7. class B : public A{
    8. public:
    9. //虚函数重写
    10. virtual void func() {
    11. cout << "hello ZXWS" << endl;
    12. }
    13. };

    2.3多态的构成条件

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

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

    1. class A {
    2. public:
    3. virtual void func() {
    4. cout << "hello C++" << endl;
    5. }
    6. };
    7. class B : public A{
    8. public:
    9. virtual void func() {
    10. cout << "hello ZXWS" << endl;
    11. }
    12. };
    13. void test(A& a)//必须是基类的引用/指针
    14. {
    15. a.func();//该函数是虚函数且被重写
    16. }
    17. int main()
    18. {
    19. A a;
    20. B b;
    21. test(a);
    22. test(b);
    23. return 0;
    24. }

     总结:多态的实现本质就是由虚函数的重写实现的,再次强调一下

    虚函数重写规则的总结和补充

    1.virtual关键字

    2.三同(返回值类型/函数参数/函数名相同)

    注意:

    1.返回值不同也能构成虚函数重写,但是返回值的类型必须是引用/指针,称为协变,(返回值得全是指针/全是引用,且得是父子关系---父对父,子对子【基类中的虚函数返回值为父类,派生类中虚函数返回值为子类】,这里的父子关系可以是任意一对父子关系)

    【注释】:基类、派生类和父类、子类是一个意思,上面的表述是为了表示两对父子关系

    1. class A {
    2. };
    3. class B : public A {
    4. };
    5. class Person {
    6. public:
    7. //父对父,子对子,对
    8. virtual A* func() {
    9. cout << "A* func()" << endl;
    10. return nullptr;
    11. }
    12. //父对子,子对父,错
    13. //virtual B* func() {
    14. // cout << "A* func()" << endl;
    15. // return nullptr;
    16. //}
    17. };
    18. class Student : public Person {
    19. public:
    20. virtual B* func() {
    21. cout << "B* func()" << endl;
    22. return nullptr;
    23. }
    24. //virtual A* func() {
    25. // cout << "B* func()" << endl;
    26. // return nullptr;
    27. //}
    28. };

    2.析构函数的重写(正常来看基类和派生类的名字不同)

    如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
    1. class Person {
    2. public:
    3. virtual ~Person(){
    4. cout << "~Person()" << endl;
    5. }
    6. };
    7. class Student : public Person {
    8. public:
    9. virtual ~Student() {
    10. cout << "~Student()" << endl;
    11. }
    12. };
    13. int main()
    14. {
    15. Person* p = new Person;
    16. Person* s = new Student;
    17. //如果析构函数不能实现虚函数重写,这里的空间释放就会出现问题
    18. //所以编辑器将析构函数的名字做了特殊处理
    19. delete p;
    20. delete s;
    21. return 0;
    22. }
    3.只要基类中的虚函数加了关键字virtual,派生类中可以不加,(推荐都加上)

    2.4 C++11 override 和 final 

    从上面的虚函数重写的规则,我们不难看出C++对虚函数的重写比较严格,这就导致我们在写的时候容易出错,且一些种错误在编译期间不会报错,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写

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

     2.override: 检查派生类虚函数是否重写了基类某个虚函数。

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

     3.抽象类

    3.1概念

    在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生 类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承
    1. class A {
    2. public:
    3. virtual void func() = 0;
    4. };
    5. class B : public A {
    6. public:
    7. virtual void func() {
    8. cout << "hello C++" << endl;
    9. };
    10. };
    11. class C : public A {
    12. public:
    13. virtual void func() {
    14. cout << "hello ZXWS" << endl;
    15. };
    16. };
    17. int main()
    18. {
    19. //A a;//抽象类不能实例化对象
    20. B b;
    21. C c;
    22. A* pb = &b;
    23. A* pc = &c;
    24. pb->func();
    25. pc->func();
    26. return 0;
    27. }

     3.2接口继承和实现继承

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

    大家来猜猜下面代码的打印结果

    1. class A {
    2. public:
    3. virtual void test() {
    4. func();
    5. }
    6. virtual void func(int x = 0) {
    7. cout << "A->" << x << endl;
    8. }
    9. };
    10. class B : public A {
    11. public:
    12. virtual void func(int x = 1) {
    13. cout << "B->" << x << endl;
    14. }
    15. };
    16. int main()
    17. {
    18. B b;
    19. A* pb = &b;
    20. pb->test();
    21. return 0;
    22. }

    代码分析:

    首先pb调用A类中的test函数,因为B类中没有重写该虚函数,所以还是调用A类中的test。然后test函数里调用func函数,func函数实现了重写,所以调用B类中的,但是注意,这里的重写只是重写函数的内部实现,函数接口(函数的声明)还是A类的函数接口,所以x的缺省值为0,而不是1

    大家要清楚普通继承和虚函数继承的差别!!!

    4.多态的原理

    在讲多态的原理之前,我们先来看看下面代码的运行结果

    1. class A {
    2. public:
    3. virtual void func(int x = 0) {
    4. cout << "A->" << x << endl;
    5. }
    6. private:
    7. int _a;
    8. char ch;
    9. };
    10. int main()
    11. {
    12. cout << sizeof(A) << endl;
    13. return 0;
    14. }

     哎?为什么这是12呢,正常我们用内存对齐来算的结果应该是8才对呀,这个类和之前的类的唯一区别在于它多了一个虚函数,我们就猜测类里面来应该存了func这个函数的相关信息。

    下面我们来调试看看

    显然,a对象中确实多了一个指针,所以对象a的大小为12(用的是32位的编译环境,地址是4字节),那么这个指针有什么用呢?

    这个指针指向的空间里存放了虚函数的地址,我们称这块空间为虚函数表,简称虚表,所以这个指针叫做虚函数表指针 (一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中)

    那么多态是如何通过虚函数实现的呢?

    通过上面的图,我们就应该能理解多态的实现原理了,本质就是在虚表里查找函数,里面放的啥函数就调用啥函数,而虚表里存放的虚函数,根据子类对父类的虚函数有没有重写,来决定存放哪个函数的地址

    同时这里也能解释为什么多态的实现需要调用父类的引用/指针,因为父类对子类对象的引用并没有生成临时变量,而是直接引用的子类继承来的父类部分,那么当然也能通过虚函数表指针调用对应的函数实现多态,指针同理

    (注意:有些平台可能会将虚函数表指针放到对象的最后面,这个跟平台有关)

     还有一点:同一个类的对象公用同一张虚函数表

     4.3动态绑定和静态绑定

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

    5.单继承和多继承关系的虚函数表

    虚函数表存在哪个区域?栈?堆?还是什么?

    1. class A {
    2. public:
    3. virtual void func() { cout << "A" << endl; }
    4. };
    5. void test()
    6. {
    7. A aa;
    8. int a=0;
    9. int* p = new int;
    10. const char* str = "hello ZXWS";
    11. static int c = 0;
    12. printf("代码区:%p\n",str);
    13. printf("静态区:%p\n", &c);
    14. printf("堆区:%p\n", p);
    15. printf("栈区:%p\n", &a);
    16. printf("虚表:%p\n", *((int*)&aa));//VS中虚函数表指针的地址一般在对象的起始位置
    17. printf("虚函数:%p\n", &A::func);
    18. return 0;
    19. }

    很显然,虚表的地址离代码区比较近,其实虚表就在代码区,这也符合我们的认识,因为虚表的内容是不能被修改的,当然虚函数其实和正常函数没什么区别,都在代码区

    5.1单继承

    1. class A {
    2. public:
    3. virtual void func1() { cout << "A::func1" << endl; }
    4. virtual void func2() { cout << "A::func2" << endl; }
    5. private:
    6. int _a;
    7. };
    8. class B {
    9. public:
    10. virtual void func1() { cout << "B::func1" << endl; }
    11. virtual void func3() { cout << "B::func3" << endl; }
    12. virtual void func4() { cout << "B::func4" << endl; }
    13. private:
    14. int _b;
    15. };
    16. int main()
    17. {
    18. A a;
    19. B b;
    20. return 0;
    21. }

    当我们看到调试窗口,就会发现B类的虚函数表中只有两个虚函数,而B类中应该有4个虚函数才对,那么会为什么呢?

    这里解释一下,这是编辑器自己做了处理,我们要想看清,还是得看地址空间,如下

     (VS编辑器会在虚函数表的结尾放一个空指针)从内存窗口来看,b的虚函数表确实是存了4个地址,但是我们不能确定,下面我们来验证一下

    1. typedef void(*VF)();//重定义函数指针
    2. void PrintVF(VF v[]) {
    3. for (int i = 0; v[i]; i++) {
    4. printf("[%d]:%p-> ", i, v[i]);
    5. v[i]();//用函数指针调用函数
    6. printf("\n");
    7. }
    8. }
    9. int main()
    10. {
    11. A a;
    12. B b;
    13. PrintVF((VF*)(*(int*)&b));//这里传的是虚函数表指针
    14. //VS中虚函数表指针的地址一般在对象的起始位置,这里注意32位地址4字节,64位地址8字节
    15. //这里是32位下,用int*,64位下要用long long*
    16. return 0;
    17. }

    通过验证,单继承中虚函数确实都在虚函数表中(验证时要注意,编辑器有时候对虚表处理不干净,需要我们重新生成解决方案,重新编译一下)

    5.2多继承中的虚函数表

    1. class A1 {
    2. public:
    3. virtual void func1() { cout << "A1::func1" << endl; }
    4. virtual void func2() { cout << "A1::func2" << endl; }
    5. private:
    6. int a1;
    7. };
    8. class A2 {
    9. public:
    10. virtual void func1() { cout << "A2::func1" << endl; }
    11. virtual void func2() { cout << "A2::func2" << endl; }
    12. private:
    13. int a2;
    14. };
    15. class B : public A1, public A2 {
    16. public:
    17. virtual void func1() { cout << "B::func1" << endl; }
    18. virtual void func3() { cout << "B::func3" << endl; }
    19. private:
    20. int b;
    21. };
    22. int main()
    23. {
    24. B b;
    25. printf("虚表地址:%p\n",* ((int*)&b));
    26. PrintVF((VF*)(*((int*)&b)));
    27. A2* p = &b;
    28. printf("虚表地址:%p\n", *((int*)p));
    29. PrintVF((VF*)(*((int*)p)));
    30. return 0;
    31. }

    可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中,且子类会继承父类的虚函数表

    这里简单说明一下,为什么两个虚表中B::func1函数的地址不同的问题,其实这是为了修正this指针,我们知道多态的实现离不开指针(引用),当我们用p去调用func1函数时,显然传过去的this指针是不对的,p仅仅指向A2的部分,所以编辑器会在调用B::func1这个函数之前,将this指针修正成b对象的起始地址,而继承来的A1成员的起始地址恰好和b对象的起始地址相同,不需要修正,所以直接填了函数地址,所以两者地址不同,但是最终调用的函数一样

    5.3菱形继承、菱形虚拟继承

    这里只要了解即可

    菱形进程和多继承一样,无非是消耗了空间,这里就不多讲了

    我们来看看菱形虚拟继承

    1. class A {
    2. public:
    3. virtual void func1() { cout << "A1::func1" << endl; }
    4. private:
    5. int a=1;
    6. };
    7. class A1 : virtual public A{
    8. public:
    9. virtual void func1() { cout << "A1::func1" << endl; }
    10. virtual void func2() { cout << "A1::func2" << endl; }
    11. private:
    12. int a1=2;
    13. };
    14. class A2 : virtual public A {
    15. public:
    16. virtual void func1() { cout << "A2::func1" << endl; }
    17. virtual void func2() { cout << "A2::func2" << endl; }
    18. private:
    19. int a2=3;
    20. };
    21. class B : public A1, public A2 {
    22. public:
    23. virtual void func1() { cout << "B::func1" << endl; }
    24. virtual void func3() { cout << "B::func3" << endl; }
    25. private:
    26. int b=4;
    27. };

    (上面两个图是分开调试的,需要单独分析,仅仅当长个见识,不会也没关系)

    六、一些小点

    1.inline函数可以是虚函数吗?

    可以,不过编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中
    1. class A {
    2. public:
    3. inline virtual void func() { cout << "A::func()"; }
    4. };
    5. int main()
    6. {
    7. A a;
    8. A* p = new A;
    9. a.func();
    10. p->func();
    11. return 0;
    12. }

    (上面的调试内容需要将相关优化打开,正常调试看不到,这里也就是给大家看看)

     2.静态成员可以是虚函数吗?

    不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
    3.构造函数可以是虚函数吗?
    不可以,因为虚表指针是在构造函数初始化列表阶段才初始化的
    4.对象访问普通函数快还是虚函数快?
    首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
  • 相关阅读:
    LeetCode·707.设计链表·架构题
    android开发中常用的Eclipse快捷键详细整理
    微信小程序Day5笔记
    Web前端和Java选哪个?哪个就业形势更好?
    centos7停服之后换阿里云的源
    记录一次生产环境MySQL死锁以及解决思路
    安卓游戏开发之图形渲染技术优劣分析
    bug场景记录
    ninja编译方法介绍
    Fisco开发第一个区块链应用
  • 原文地址:https://blog.csdn.net/V_zjs/article/details/133934072