• 【C++】-- 多态


    目录

    一、多态定义

    1.静态多态

    2.动态多态

    (1)虚函数 

    (2)虚函数的重写 

    (3)虚函数重写的两个例外

    (4)C++11的final和override

    (5)重载、重写与隐藏

     二、抽象类

    1.纯虚函数

    (1)纯虚函数

    (2)抽象类(接口类):

    2.接口继承和实现继承 

    (1)实现继承

    (2)接口继承 

    三、多态原理

    1.虚函数表

    2.原理

    (1)构成多态 

    (2)不构成多态

    (3)汇编层面看多态

    四、单继承和多继承关系的虚函数表

    1.单继承的虚函数表

    (1)虚表初始化的时机

    (2)子类虚表的生成过程

    2.多继承的虚函数表 


    一、多态定义

     多态是函数调用的多种形态,使我们调用函数更加灵活。多态分为两种:静态多态和动态多态

    1.静态多态

    静态多态即函数重载,这里的静态是指编译时:

    1. #include
    2. using namespace std;
    3. void Swap(int& s1, int& s2)
    4. {
    5. int temp = s1;
    6. s1 = s2;
    7. s2 = temp;
    8. }
    9. void Swap(float& s1, float& s2)
    10. {
    11. float temp = s1;
    12. s1 = s2;
    13. s2 = temp;
    14. }
    15. void Swap(char& s1, char& s2)
    16. {
    17. char temp = s1;
    18. s1 = s2;
    19. s2 = temp;
    20. }
    21. int main()
    22. {
    23. int a = 1, b = 2;
    24. float c = 3.0, d = 4.0;
    25. char e = 'z', f = 'Z';
    26. Swap(a, b);
    27. Swap(c, d);
    28. Swap(e, f);
    29. return 0;
    30. }

    看起来我们用的是一个函数,实际上这就是静态多态 

    2.动态多态

    动态多态是指不同类型对象完成一件事的时候产生的动作不一样,那么结果也不一样。 

    在继承中要构成多态有两个条件,缺一不可:

    (1)必须通过父类的指针或者引用调用虚函数

    (2)被调用的函数必须是虚函数,且子类必须对父类的虚函数进行重写

    动态多态父类指针或引用的指向:

    (1)父类指针或引用指向父类,调用的就是父类虚函数

    (2)父类指针或引用指向哪个子类,调用的就是哪个子类重写的虚函数

    根据切片规则,父类的指针既可以指向父类,又可以指向子类,如果有多个子类,就可以指向不同类型。 

    (1)虚函数 

    被virtual修饰的成员函数叫做虚函数。

    注意:

    (1)只有类的非静态成员函数才可以被 virtual修饰,普通函数不可以。

    (2)虽然虚函数的virtual和虚继承中的virtual是同一个关键字,但是它们之间没有关系。虚函数的vitual是为了实现多态,虚继承中的virtual是为了解决菱形继承的数据冗余和二义性。

    (2)虚函数的重写 

     虚函数重写也叫做覆盖,在子类中重写了一个和父类中的虚函数完全相同的虚函数:包括函数名、返回值、参数列表都相同,这时候子类就重写了父类的虚函数。

    注意:

    子类重写的虚函数的函数名、返回值、参数列表和父类一定要完全相同,否则就变成了函数重载,和继承无关。

    如下,Bird类和Dog类就重写了父类的虚函数:

    1. #include
    2. using namespace std;
    3. class Animal
    4. {
    5. public:
    6. virtual void Speak()//父类虚函数
    7. {
    8. cout << "speak" << endl;
    9. }
    10. };
    11. class Bird:public Animal
    12. {
    13. public:
    14. virtual void Speak()//子类重写父类虚函数
    15. {
    16. cout << "chirp" << endl;
    17. }
    18. };
    19. class Dog :public Animal
    20. {
    21. public:
    22. virtual void Speak()//子类重写父类虚函数
    23. {
    24. cout << "bark" << endl;
    25. }
    26. };
    27. //父类对象:会破坏多态条件,不构成多态
    28. void fun(Animal a)
    29. {
    30. //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
    31. a.Speak();
    32. }
    33. //父类引用-构成多态
    34. void fun1(Animal& a)
    35. {
    36. //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
    37. a.Speak();
    38. }
    39. //父类指针-构成多态
    40. void fun2(Animal* pa)
    41. {
    42. //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
    43. pa->Speak();
    44. }
    45. int main()
    46. {
    47. Animal a;
    48. Bird b;
    49. Dog d;
    50. Animal *pa = &a;
    51. Bird *pb = &b;
    52. Dog *pd = &d;
    53. fun(a);
    54. fun(b);
    55. fun(d);
    56. cout << endl;
    57. fun1(a);
    58. fun1(b);
    59. fun1(d);
    60. cout << endl;
    61. fun2(pa);
    62. fun2(pb);
    63. fun2(pd);
    64. return 0;
    65. }

     当父类对象调用虚函数时,不构成多态,当父类引用或指针调用虚函数时,构成多态:

     如果去掉父类的虚函数关键字virtual,子类就没有重写父类的虚函数,那么就算传父类指针或父类引用也不会构成多态:

    1. #include
    2. using namespace std;
    3. class Animal
    4. {
    5. public:
    6. void Speak()//父类普通函数
    7. {
    8. cout << "speak" << endl;
    9. }
    10. };
    11. class Bird:public Animal
    12. {
    13. public:
    14. void Speak()//子类普通函数
    15. {
    16. cout << "chirp" << endl;
    17. }
    18. };
    19. class Dog :public Animal
    20. {
    21. public:
    22. void Speak()//子类普通函数
    23. {
    24. cout << "bark" << endl;
    25. }
    26. };
    27. //父类对象:会破坏多态条件,不构成多态
    28. void fun(Animal a)
    29. {
    30. //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
    31. a.Speak();
    32. }
    33. //父类引用-构成多态
    34. void fun1(Animal& a)
    35. {
    36. //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
    37. a.Speak();
    38. }
    39. //父类指针-构成多态
    40. void fun2(Animal* pa)
    41. {
    42. //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
    43. pa->Speak();
    44. }
    45. int main()
    46. {
    47. Animal a;
    48. Bird b;
    49. Dog d;
    50. Animal *pa = &a;
    51. Bird *pb = &b;
    52. Dog *pd = &d;
    53. fun(a);
    54. fun(b);
    55. fun(d);
    56. cout << endl;
    57. fun1(a);
    58. fun1(b);
    59. fun1(d);
    60. cout << endl;
    61. fun2(pa);
    62. fun2(pb);
    63. fun2(pd);
    64. return 0;
    65. }

     不构成多态:

    (3)虚函数重写的两个例外

    ① 协变(返回值类型是父子关系)

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

    1. #include
    2. using namespace std;
    3. class Animal
    4. {
    5. public:
    6. virtual Animal* Speak()//父类虚函数
    7. {
    8. cout << "Animal* Animal::Speak()" << endl;
    9. return new Animal;//返回父类指针
    10. }
    11. };
    12. class Bird :public Animal
    13. {
    14. public:
    15. virtual Bird* Speak()//子类重写父类虚函数
    16. {
    17. cout << "Bird* Bird::Speak()" << endl;
    18. return new Bird;//返回子类指针
    19. }
    20. };
    21. int main()
    22. {
    23. Animal a;
    24. Bird b;
    25. Animal* pa = &a;
    26. pa->Speak();
    27. Bird* pb = &b;
    28. pb->Speak();
    29. return 0;
    30. }

    父类返回父类指针,子类返回子类指针:

    所以虚函数重写后的返回值不一定相同 ,因为有协变的存在。

     ②析构函数的重写(子类与父类析构函数的名字不同)

    如果父类的析构函数为虚函数,此时子类析构函数只要定义,无论是否加virtual关键字,都与父类的析构函数构成重写,虽然父类与子类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。可参考【C++】-- 继承第四节的第4小节。

    把父类析构函数定义为虚函数,子类就可以重写父类的虚函数:

    1. #include
    2. using namespace std;
    3. class Animal
    4. {
    5. public:
    6. virtual ~Animal()
    7. {
    8. cout << "~Animal()" << endl;
    9. }
    10. };
    11. class Bird :public Animal
    12. {
    13. public:
    14. virtual ~Bird()//Bird类和Animal类的析构函数名看起来不同,但是他们构成虚构函数重写
    15. {
    16. cout << "~Bird()" << endl;
    17. }
    18. };
    19. int main()
    20. {
    21. Animal a;
    22. Bird b;
    23. return 0;
    24. }

    由于析构时,子类对象先调用自己的析构函数进行清理,清理完后再自动调用父类的析构函数,所以打印的前两行是Bird类对象调用的析构函数,第3行调用的是Animal类对象的析构函数。

    但是发现把

        virtual ~Animal()

        virtual ~Bird()

    中的virtual都去掉,运行结果还是一样。说明在普通场景下,父类和子类的析构函数是否是虚函数,是否构成重写,没什么影响。

    那在什么场景下才有影响呢?

    1. #include
    2. using namespace std;
    3. class Animal
    4. {
    5. public:
    6. ~Animal()
    7. {
    8. cout << "~Animal()" << endl;
    9. }
    10. };
    11. class Bird :public Animal
    12. {
    13. public:
    14. ~Bird()//Bird类和Animal类的析构函数名看起来不同,但是他们构成虚构函数重写
    15. {
    16. cout << "~Bird()" << endl;
    17. }
    18. };
    19. int main()
    20. {
    21. Animal* pa = new Animal;
    22. Animal* pb = new Bird;
    23. //多态行为
    24. delete pa;//pa->析构函数() + operator delete(pa)
    25. delete pb;//pb->析构函数() + operator delete(pb)
    26. return 0;
    27. }

    delete在释放空间的同时要调用析构函数,需要做两步操作:

    (1)先调用析构函数

    (2)再释放空间

    pa和pb的空间都会被释放,pa指向父类对象,期望调用父类的析构函数,pb指向子类对象,期望调用子类的析构函数,指向父类调父类,指向子类调子类,期望这里达到多态行为,虽然没有明显的函数调用,但是delete操作调了析构函数。

    pb指向子类对象,但是发现没有调用子类析构函数,可能存在内存泄漏:

    当子类析构函数不需要清理资源时也就没什么问题,但是当子类析构函数需要清理时,这样做会存在内存泄漏 。因此多态场景下子类和父类的析构函数最好加上virtual关键字完成虚函数重写就不会导致内存泄漏了。所以上面的代码最好不要删掉析构函数前面的virtual。

    另外在继承一文中,说过子类和父类的析构函数构成隐藏。原因就是表面上子类的析构函数个父类的析构函数名不同,但是为了构成重写,编译器会对析构函数名调用时,统一将父类和子类的析构函数名改成destructor( )。统一改成destructor( )构成隐藏的目的就是在这里能够调用同一个函数,达到多态指向父类对象就调父类对象,指向子类对象就调子类对象的的目的。

    因此,父类函数中的virtual不能省,否则子类继承不了父类的virtual属性,无法重写父类的虚函数,如果这个函数是析构函数,那么还会造成内存泄漏。为了保持统一,父类和子类虚函数前面的virtual都不要省。

    (4)C++11的final和override

    ①final:如果一个虚函数不想被重写,可以在虚函数后面加final

    1. #include
    2. using namespace std;
    3. class Animal
    4. {
    5. public:
    6. virtual ~Animal() final//虚函数不想被重写
    7. {
    8. cout << "~Animal()" << endl;
    9. }
    10. };
    11. class Bird :public Animal
    12. {
    13. public:
    14. virtual ~Bird()
    15. {
    16. cout << "~Bird()" << endl;
    17. }
    18. };

     一旦重写final修饰的虚函数,就会报错:

    如果一个类不想被继承,可以在这个类后面加final

    1. class Animal final//Animal类不想被继承
    2. {};
    3. class Bird :public Animal
    4. {};

     编译报错:final类无法被继承:

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

    1. #include
    2. using namespace std;
    3. class Animal
    4. {
    5. public:
    6. virtual void Speak()
    7. {
    8. cout << "speak" << endl;
    9. }
    10. };
    11. class Bird :public Animal
    12. {
    13. public:
    14. virtual void Speak(int i) override//用来检查子类是否完成父类虚函数的重写
    15. {
    16. cout << "chirp" << endl;
    17. }
    18. };

    由于子类重写的虚函数的参数列表和父类虚函数的参数列表不同,导致子类没有成功完成重写父类的虚函数,override检查会报错:

    (5)重载、重写与隐藏

     重载、重写和隐藏的对比:

     二、抽象类

    1.纯虚函数

    (1)纯虚函数

    定义:在虚函数的后面写上 =0

    (2)抽象类(接口类):

    定义:包含纯虚函数的类

    性质:抽象类不能实例化出对象。子类继承抽象类后也不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象。

    意义:

    ① 能够更好地去表示现实世界中没有实例对象是我抽象类型,如:植物、人、动物

    ② 体现接口继承,强制子类去重写虚函数(就算不重写,子类也是抽象类)

    1. #include
    2. using namespace std;
    3. class Animal//抽象类
    4. {
    5. public:
    6. virtual void Speak() = 0;//纯虚函数
    7. };
    8. class Bird :public Animal
    9. {
    10. public://没有重写纯虚函数
    11. };
    12. class Dog :public Animal
    13. {
    14. public:
    15. virtual void Speak()//子类重写父类虚函数
    16. {
    17. cout << "bark" << endl;
    18. }
    19. };

    抽象类不能实例化出对象:

    1. int main()
    2. {
    3. Animal a;
    4. return 0;
    5. }

     报错:

     当子类没有重写父类的纯虚函数时,直接把父类的虚函数继承下来了,这个虚函数也是纯虚函数,那么这个子类就是抽象类,不能实例化出对象:

    1. int main()
    2. {
    3. Bird b;
    4. return 0;
    5. }

    报错: 

     当子类重写了父类虚函数:

    1. #include
    2. using namespace std;
    3. class Animal//抽象类
    4. {
    5. public:
    6. virtual void Speak() = 0;//纯虚函数
    7. };
    8. class Bird :public Animal
    9. {
    10. public:
    11. virtual void Speak()//子类重写父类纯虚函数
    12. {
    13. cout << "chirp" << endl;
    14. }
    15. };
    16. class Dog :public Animal
    17. {
    18. public:
    19. virtual void Speak()//子类重写父类纯虚函数
    20. {
    21. cout << "bark" << endl;
    22. }
    23. };
    24. int main()
    25. {
    26. Animal* pBird = new Bird;
    27. pBird->Speak();
    28. Animal* pDog = new Dog;
    29. pDog->Speak();
    30. return 0;
    31. }

     pBird 和pDog是指向父类的指针,调用了子类虚函数,看起来像是调用了同一个虚函数Speak( )。

    2.接口继承和实现继承 

    (1)实现继承

    普通函数的继承是实现继承,不是接口继承,继承的是函数的实现,可以直接使用这个函数,也是一种复用。

    (2)接口继承 

    虚函数包括纯虚函数的继承是接口继承,子类仅仅只继承了父类接口 ,父类没有实现这个接口函数,子类要对纯虚函数进行重写达到多态的目的。

    注意: 

    如果为了达到多态目的,那可以把父类接口定义成虚函数,并且定义了后,子类必须要重写父类的虚函数,否则就不要把普通函数定义成虚函数。

    三、多态原理

    1.虚函数表

    了解多态原理前需要了解虚函数表。 

    【C++】-- 类和对象一文中,讲过类的大小如何计算,只包含成员变量的大小,不会包含成员函数的大小,那么下面的代码应该打印4

    1. #include
    2. using namespace std;
    3. class Animal
    4. {
    5. public:
    6. virtual void Speak()//父类虚函数
    7. {
    8. cout << "speak" << endl;
    9. }
    10. public:
    11. int legs;
    12. };
    13. int main()
    14. {
    15. Animal a;
    16. cout << "sizeof(a) = " << sizeof(a) << endl;//Animal类对象的大小
    17. return 0;
    18. }

    但是结果却打印8:

     

     对象a只有一个成员变量legs,占4个字节。通过监视看到,对象a里面包含两个成员 ,那么另外4个字节一定是_vfptr占用的,且_vfptr里面存放的是一个地址,那么_vfptr一定是个指针:

     _vfptr叫做虚函数表指针,其中v是virtual的缩写,f是function的缩写。

    虚函数表也简称虚表。

    由于虚函数的地址要被放到虚函数表中,因此一个含有虚函数的类中都至少有一个虚函数表指针,这个虚函数表指针指向一个虚函数。虚函数表指针用来实现多态。

    那么子类的虚表中都存放了什么呢?对于如下代码

    1. #include
    2. using namespace std;
    3. class Animal
    4. {
    5. public:
    6. virtual void Speak()//父类虚函数
    7. {
    8. cout << "speak" << endl;
    9. }
    10. virtual void run()//父类虚函数
    11. {
    12. cout << "run" << endl;
    13. }
    14. void jump()//父类普通函数
    15. {
    16. cout << "jump" << endl;
    17. }
    18. public:
    19. int legs;
    20. };
    21. class Bird :public Animal
    22. {
    23. public:
    24. virtual void Speak()//子类重写父类虚函数
    25. {
    26. cout << "chirp" << endl;
    27. }
    28. public:
    29. string color;
    30. };
    31. int main()
    32. {
    33. Animal a;
    34. Bird b;
    35. return 0;
    36. }

     监视:

     数组也叫做表

    从监视可以发现: 

    1.子类对象b中也有一个虚表指针,b对象由两部分构成,一部分是父类继承下来的成员,另一部分是自己的成员。
    2.父类a对象和子类对象b虚表是不一样的,Speak完成了重写,所以b的虚表中存的是重写的Bird::Speak,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
    3. Run继承下来后是虚函数,所以放进了虚表,Jump也继承下来了,但是不是虚函数,所以不会放进虚表。
    4.虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
    5.总结一下派生类的虚表生成:
            a.先将基类中的虚表内容拷贝一份到派生类虚表中 
            b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 
            c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
    6.虚函数存在哪的?虚表存在哪的?
            虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是它的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。虚表在vs下存在代码段里。

    2.原理

    (1)构成多态 

    对于如下代码,子类重写了父类虚函数,且通过父类指针调用虚函数,这就满足了多态的两个条件

    1. #include
    2. using namespace std;
    3. class Animal
    4. {
    5. public:
    6. virtual void Speak()//父类虚函数
    7. {
    8. cout << "speak" << endl;
    9. }
    10. };
    11. class Bird :public Animal
    12. {
    13. public:
    14. virtual void Speak()//子类重写父类虚函数
    15. {
    16. cout << "chirp" << endl;
    17. }
    18. };
    19. void func(Animal* pa)
    20. {
    21. pa->Speak();//通过父类指针调用虚函数
    22. }
    23. int main()
    24. {
    25. Animal a;
    26. func(&a);
    27. Bird b;
    28. func(&b);
    29. return 0;
    30. }

     打印结果:

    为什么引用是父类就调父类的Speak,是子类就调子类的Speak呢?

     对象a和对象b里面都没有其他成员,只有虚表指针,都是4字节。子类完成父类虚函数重写以后,子类的虚表指针指向的是重写了的子类虚函数:

     指针或引用调用虚函数是怎么调的呢?

    指针或引用调用虚函数时,不是编译时确定,而是运行时才到指向的对象的虚表中找对应的虚函数调用,当指针或引用指向父类对象时,调用的就是父类的虚表中的虚函数,当指针或引用指向子类对象时,调用的就是子类虚表中的虚函数

     

    (2)不构成多态

     ① 如果子类没有重写父类虚函数:

    1. #include
    2. using namespace std;
    3. class Animal
    4. {
    5. public:
    6. virtual void Speak()//父类虚函数
    7. {
    8. cout << "speak" << endl;
    9. }
    10. };
    11. class Bird :public Animal
    12. {
    13. public:
    14. };
    15. void func(Animal* pa)
    16. {
    17. pa->Speak();
    18. }
    19. int main()
    20. {
    21. Animal a;
    22. func(&a);
    23. Bird b;
    24. func(&b);
    25. return 0;
    26. }

     这时就破坏了多态的条件,那么子类也有虚表,但是子类虚表里的指针存的是Animal的虚函数,而不是Bird的虚函数。不满足多态条件时(子类没有重写父类虚函数/通过父类指针或引用调用虚函数),就不会到虚表里面去找,决定调哪个函数是在编译时确定这个函数的形参是哪个类型,而跟对象没有关系。

     总结:

    (1)构成多态,指向谁就调用谁的虚函数,跟对象有关

    (2)当子类没有重写父类虚函数时,不构成多态,调用函数的入参类型是什么,调用的就是哪个的虚函数,跟对象无关,跟入参类型有关

     ② 不是通过父类指针或引用,而是通过父类对象调用虚函数

    1. #include
    2. using namespace std;
    3. class Animal
    4. {
    5. public:
    6. virtual void Speak()//父类虚函数
    7. {
    8. cout << "speak" << endl;
    9. }
    10. public:
    11. int legs = 4;
    12. };
    13. class Bird :public Animal
    14. {
    15. public:
    16. virtual void Speak()//子类重写父类虚函数
    17. {
    18. cout << "chirp" << endl;
    19. }
    20. public:
    21. string color;
    22. };
    23. void func(Animal pa)//入参类型为父类对象
    24. {
    25. pa.Speak();
    26. }
    27. int main()
    28. {
    29. Animal a;
    30. func(a);
    31. Bird b;
    32. b.legs = 2;
    33. func(b);
    34. return 0;
    35. }

    对象是无法实现出多态的,因为如果入参是子类对象,那么指针和引用会把父类那部分切出来,切出来后不是赋值,而是让指针指向子类里面父类的那部分,这个指针无论指向的是父类还是子类,看到的都是父类对象,给父类引用的就是父类对象,给子类引用的是切片出来的父类对象。

    而构成多态时,引用和指针本身并不知道自己指向或引用的是父类对象还是子类对象,指向父类对象,那就指向或引用整个父类对象,指向子类对象,那就那看到的就是子类对象中父类那一部分,对于

        a.Speak();

     编译完成的指令是一样的,虽然传入的实参不同,但是看到的都是父类部分或者子类切片出来的父类部分,都是一样的动作,到对应的地方去找。

    如果是对象的时候为什么不行,如果是对象涉及到切片问题,这个时候的切片不是让我窒息那个你,你给我的是一个父类对析那个,那就把这个父类对象给你,你给我的是个子类对象,就把子类对象中的父类部分切片后给你,调用拷贝构造函数把父类部分切片出来,把父类成员给你;父类对象不会把虚表指针给过去,两者的虚表指针是一样的

    父类对象调用完func后,pa的虚表指针存放的是父类的虚函数地址:

    子类对象调用完func后,pa的虚表指针存放的还是父类的虚函数地址,但是成员变量被修改了:

     这是因为多个同类型对象,只有一份虚表,因此虚表当中的内容是一样的,它们的虚表指针都指向这个虚表。

    当是子类切片的时候,会把子类切出来的成员变量给func的形参(即父类对象),但不会把_vfptr给过去,因为只有一份虚表,假如切片后把虚表指针也给过去了,会出现混乱,它的虚表指针到底是父类的还是子类的,如果是直接定义出来的,那就是父类的,如果经过子类赋值,那就是子类的,但是父类对象的虚表里面怎么会有子类的虚函数呢?这显然不合理,因此不会把虚表给func的形参(父类对象)。

    指针和引用是指向的,指向父类就是父类对象,指向子类就是子类当中切片出来的父类部分,让指针和引用去指向。而对象要拷贝构造,只是把值给过去。

    总结:当通过父类对象调用虚函数,切片只会拷贝成员变量,不会拷贝虚表指针

    (3)汇编层面看多态

     不构成多态时,编译时直接调用函数的地址

    构成多态时,运行时到指向的对象的虚表中找到要调用的虚函数

    四、单继承和多继承关系的虚函数表

    1.单继承的虚函数表

    (1)虚表初始化的时机

    对象中虚表指针是在什么阶段初始化的?虚表在哪个阶段生成? 

    对于如下代码:

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include
    3. using namespace std;
    4. class Animal
    5. {
    6. public:
    7. virtual void Speak()//父类虚函数
    8. {
    9. cout << "Animal::speak" << endl;
    10. }
    11. virtual void Run()//父类虚函数
    12. {
    13. cout << "Animal::run" << endl;
    14. }
    15. void Jump()//父类普通函数
    16. {
    17. cout << "Animal::jump" << endl;
    18. }
    19. public:
    20. int legs = 4;
    21. };
    22. class Bird :public Animal
    23. {
    24. public:
    25. virtual void Speak()//子类重写父类虚函数
    26. {
    27. cout << "Bird::chirp" << endl;
    28. }
    29. public:
    30. string color = "Yellow";
    31. };
    32. int main()
    33. {
    34. Animal a;
    35. Bird b;
    36. return 0;
    37. }

    通过监视F11逐语句查看执行过程发现,定义对象a时,执行步骤如下:

    (1)开始执行Animal的构造函数

    (2)初始化Animal的成员

    (3)将Animal构造函数执行完毕

    发现执行完以上3步之后,虚表指针已经初始化了: 

     因此,虚表指针是在构造函数初始化列表阶段初始化的,虚表在编译时就已经生成了。

    一个类中所有的虚函数地址,都会放到虚表中。虚表里面存放的是虚函数地址,虚函数和普通函数一样, 编译完成后,都放在代码段。

    (2)子类虚表的生成过程

    子类的虚表是如何生成的呢?

    父类的虚表中存的是Aniaml的Speak( )和Run( )的地址。生成子类虚表时,会单独开辟一块空间,拷贝一份父类虚表过程中,会将对应虚函数位置覆盖成子类重写了父类的虚函数,如果子类没有重写,那么父类的虚函数就不会被覆盖,保留。所以子类虚表的生成过程是一个拷贝+覆盖的过程。

    监视如上代码:

    (1)子类重写了父类的Speak( )虚函数,所以子类会覆盖父类Speak( )位置;

    (2)子类没有重写父类的Run( )虚函数,子类不会覆盖父类Run( )位置;

    (3)父类的Jump( )不是虚函数, 不会出现在虚表中:

    虚函数的重写是语法层的概念,覆盖是虚表实现层的概念。

    在内存窗口输入虚表地址,发现里面存的是虚函数的地址,虚表作为数组,是如何知道数组结束的呢?VS在虚表结束位置放空指针,表示虚表结束了:

    假如子类还有虚函数:

    1. virtual void Fly()//飞
    2. {
    3. cout << "virtual Bird::fly" << endl;
    4. }
    5. virtual void Sing()//唱歌
    6. {
    7. cout << "virtual Bird::sing" << endl;
    8. }

     这两个虚函数既不是继承父类虚函数,也没有重写父类虚函数,通过监视看不到子类的这两个虚函数,但是通过内存可以看到:

    也可以打印一下虚表中调用的函数:

    1. typedef void(*VFunc)();//为虚表指针定义简洁的名称
    2. void PrintVFT(VFunc* ptr)//传参虚函数指针数组
    3. {
    4. printf("虚表地址:%p\n",ptr);
    5. for (int i = 0; ptr[i] != nullptr; i++)
    6. {
    7. printf("VFT[%d] : %p->", i,ptr[i]);
    8. ptr[i]();
    9. }
    10. printf("\n");
    11. }
    12. int main()
    13. {
    14. Animal a;
    15. PrintVFT((VFunc*)(*((int*)&a)));
    16. Bird b;
    17. PrintVFT((VFunc*)(*((int*)&b)));
    18. //(int*)&a -- 将a的虚表指针强转为int型
    19. //*((int*)&a)) -- 解引用得到虚表指针指向的第一个虚函数地址
    20. //(VFunc*)(*((int*)&a)) -- 将第一个虚函数地址强转为(VFunc*)
    21. return 0;
    22. }

     

    2.多继承的虚函数表 

     以上是单继承,对于多继承,如何打印虚表函数:

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include
    3. using namespace std;
    4. class Animal
    5. {
    6. public:
    7. virtual void Color()//颜色
    8. {
    9. cout << "virtual Animal::color" << endl;
    10. }
    11. virtual void Name()//名称
    12. {
    13. cout << "virtual Animal::name" << endl;
    14. }
    15. };
    16. class Plant
    17. {
    18. public:
    19. virtual void Color()//颜色
    20. {
    21. cout << "virtual Plant::color" << endl;
    22. }
    23. virtual void Name()//名称
    24. {
    25. cout << "virtual Plant::name" << endl;
    26. }
    27. };
    28. class Coral :public Animal, public Plant
    29. {
    30. public:
    31. virtual void Color()//子类重写Animal类虚函数
    32. {
    33. cout << "virtual Coral::color" << endl;
    34. }
    35. virtual void Shape()//子类重写Plant类虚函数
    36. {
    37. cout << "virtual Coral::shape" << endl;
    38. }
    39. };
    40. typedef void(*VFunc)();//为虚表指针定义简洁的名称
    41. void PrintVFT(VFunc* ptr)//传参虚函数指针数组
    42. {
    43. printf("虚表地址:%p\n", ptr);
    44. for (int i = 0; ptr[i] != nullptr; i++)
    45. {
    46. printf("VFT[%d] : %p->", i, ptr[i]);
    47. ptr[i]();
    48. }
    49. printf("\n");
    50. }
    51. int main()
    52. {
    53. //c继承了两个类,有两个虚表
    54. //c的两张虚表,先继承了Animal,Animal在前面,正好Animal的头4个字节是虚表指针,Plant挨着Animal,Animal完了就是Plant
    55. Coral c;
    56. PrintVFT((VFunc*)(*((int*)&c)));
    57. PrintVFT((VFunc*)(*(int*)((char*)&c + sizeof(Animal))));
    58. //(char*)&c -- 取c的地址,强转成char*
    59. //(char*)&c + sizeof(Animal) -- 取c的地址,强转成char*,再跨越一个Animal类的大小
    60. return 0;
    61. }

    打印发现:
    (1)两张虚表都重写了Color( )函数

    (2)但两张虚表都没有重写Name( )函数,都直接继承了Name( )函数

    (3)Shape( )虚函数只放在了第一张虚表中,第二张虚表没有放

     

     

  • 相关阅读:
    【机器学习】支持向量机【下】软间隔与核函数
    教育案例分享 | 安全狗云安全体系为高校提升立体化纵深防御能力
    vue3中插槽,计算属性,路由(addRoutes()白屏),表单,vuex 笔记,localStorage和sessionStorage语法
    【pip command】之 options 汇总
    Scala基础【正则表达式、框架式开发原则】
    docker 实战
    2.19每日一题(分段函数求定积分)
    【To .NET】C#集合类源码解析
    【Verilog】布斯算法(Booth Algorithm)乘法器的 Verilog 实现
    计算机网络——b站王道考研笔记
  • 原文地址:https://blog.csdn.net/gx714433461/article/details/125987314