• C++:多态 详解


    目录

    一.多态的概念

    二.多态的定义及实现

    1.重写/覆盖 的要求

    2.多态两个要求:

    3.多态的切片示意图

    4.多态演示:

    买票场景下的多态 完整代码

    5.虚函数重写的例外:

    协变(父类与子类虚函数返回值类型不同)

    6.接口继承和实现继承

    多态的坑题目(考接口继承)

    7.析构函数的重写-析构函数名统一会被处理成destructor()

    8.C++11 override 和 final

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

    10.抽象类

    三.多态的原理

    1.虚函数介绍

    2.虚函数表

    3.虚表存储

    (1)虚函数重写/覆盖 语法与原理层解释

    (2)虚表存储解释

    多态调用和普通调用底层解释(编译时多态/运行时多态)

    (3)父类赋值给子类对象,也可以切片。为什么实现不了多态?

    (4)静态和动态的多态(了解)

    (5)非多态的虚函数Func4在监视窗口被隐藏了,看不到,只能通过内存看到

    (6)同一类型对象,共用一个虚表

    (7)虚表存在常量区/代码段

    4.多继承,虚表的存储(一个子类继承两个父亲时)

    四.虚函数使用规则

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


    一.多态的概念

    多态的概念:通俗来说,就是多种形态, 具体点就是去完成某个行为,当不同的对象去完成时会
    产生出不同的状态
    举个例子:比如 买票这个行为 ,当 普通人 买票时,是全价买票; 学生 买票时,是半价买票; 军人
    买票时是优先买票。
    再举个栗子: 最近为了 争夺在线支付市场 ,支付宝年底经常会做诱人的 扫红包 - 支付 - 给奖励金
    活动。那么大家想想为什么有人扫的红包又大又新鲜 8 块、 10 ... ,而有人扫的红包都是 1 毛, 5
    .... 。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如
    你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额 =
    random()%99 ;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你
    去使用支付宝,那么就你扫码金额 = random()%1 ;总结一下: 同样是扫码动作,不同的用户扫
    得到的不一样的红包,这也是一种多态行为。 ps :支付宝红包问题纯属瞎编,大家仅供娱乐。
    总结:多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如 Student 继承了 Person。 Person 对象买票全价, Student 对象买票半价。

    二.多态的定义及实现

    1.重写/覆盖 的要求

    重写/覆盖: 子类中有一个跟父类完全相同的虚函数,子类的虚函数重写了基类的虚函数

    即:子类父类都有这个虚函数 + 子类的虚函数与父类虚函数的 函数名/参数/返回值 都相同 -> 重写/覆盖(注意:参数只看类型是否相同,不看缺省值)

    2.多态两个要求:

     1、被调用的函数必须是虚函数,子类对父类的虚函数进行重写 (重写:三同(函数名/参数/返回值)+虚函数)
     2、父类指针或者引用去调用虚函数。

    3.多态的切片示意图

    (1)示例1:给一个student的子类对象(临时对象也行),然后把这个对象赋给一个父类指针,通过这个父类指针就可以访问student子类的虚拟函数

    (2)示例2:假设B是子类,A是父类,new一个B类的临时对象,然后把这个临时对象赋给一个父类指针A* p2,通过这个父类指针p2就可以访问子类B的虚拟函数func

     

    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*p1 = new B;
    15. //p1->test(); 这个是多态调用,下有讲解 二->6
    16. p1->func(); //普通调用
    17. A*p2 = new B;
    18. p2->func(); //多态调用
    19. return 0;
    20. }

    4.多态演示:

    1. class Person {
    2. public:
    3. Person(const char* name)
    4. :_name(name)
    5. {}
    6. // 虚函数
    7. virtual void BuyTicket()
    8. {
    9. cout << _name << "Person:买票-全价 100¥" << endl;
    10. }
    11. protected:
    12. string _name;
    13. //int _id;
    14. };
    15. class Student : public Person {
    16. public:
    17. Student(const char* name)
    18. :Person(name)
    19. {}
    20. // 虚函数 + 函数名/参数/返回值 -》 重写/覆盖
    21. virtual void BuyTicket()
    22. {
    23. cout << _name << " Student:买票-半价 50 ¥" << endl;
    24. }
    25. };
    26. void Pay(Person& ptr)
    27. {
    28. ptr.BuyTicket();
    29. }
    30. int main()
    31. {
    32. string name;
    33. cin >> name;
    34. Student s(name.c_str());
    35. Pay(s);
    36. }

    买票场景下的多态 完整代码

    普通人 买票时,是全价买票; 学生 买票时,是半价买票; 军人 买票时是优先买票。
    1. class Person {
    2. public:
    3. Person(const char* name)
    4. :_name(name)
    5. {}
    6. // 虚函数
    7. virtual void BuyTicket() { cout << _name << "Person:买票-全价 100¥" << endl; }
    8. protected:
    9. string _name;
    10. //int _id;
    11. };
    12. class Student : public Person {
    13. public:
    14. Student(const char* name)
    15. :Person(name)
    16. {}
    17. // 虚函数 + 函数名/参数/返回值 -》 重写/覆盖
    18. virtual void BuyTicket() { cout << _name << " Student:买票-半价 50 ¥" << endl; }
    19. };
    20. class Soldier : public Person {
    21. public:
    22. Soldier(const char* name)
    23. :Person(name)
    24. {}
    25. // 虚函数 + 函数名/参数/返回值 -》 重写/覆盖
    26. virtual void BuyTicket() { cout << _name << " Soldier:优先买预留票-88折 88 ¥" << endl; }
    27. };
    28. // 多态两个要求:
    29. // 1、子类虚函数重写的父类虚函数 (重写:三同(函数名/参数/返回值)+虚函数)
    30. // 2、父类指针或者引用去调用虚函数。
    31. //void Pay(Person* ptr)
    32. //{
    33. // ptr->BuyTicket();
    34. //}
    35. void Pay(Person& ptr)
    36. {
    37. ptr.BuyTicket();
    38. }
    39. // 不能构成多态
    40. //void Pay(Person ptr)
    41. //{
    42. // ptr.BuyTicket();
    43. //}
    44. int main()
    45. {
    46. int option = 0;
    47. cout << "=======================================" << endl;
    48. do
    49. {
    50. cout << "请选择身份:";
    51. cout << "1、普通人 2、学生 3、军人" << endl;
    52. cin >> option;
    53. cout << "请输入名字:";
    54. string name;
    55. cin >> name;
    56. switch (option)
    57. {
    58. case 1:
    59. {
    60. Person p(name.c_str());
    61. Pay(p);
    62. break;
    63. }
    64. case 2:
    65. {
    66. Student s(name.c_str());
    67. Pay(s);
    68. break;
    69. }
    70. case 3:
    71. {
    72. Soldier s(name.c_str());
    73. Pay(s);
    74. break;
    75. }
    76. default:
    77. cout << "输入错误,请重新输入" << endl;
    78. break;
    79. }
    80. cout << "=======================================" << endl;
    81. } while (option != -1);
    82. return 0;
    83. }
    解释 2、父类指针或者引用去调用虚函数,传值调用不构成多态。
    用子类也不行,必须用父类,比如你用个student,那么你的Person或者Soldier就传不进形参
    1. void Pay(Person* ptr) //指针调用可以
    2. {
    3. ptr->BuyTicket();
    4. }
    5. void Pay(Person& ptr) //引用调用可以
    6. {
    7. ptr.BuyTicket();
    8. }
    9. // 不能构成多态
    10. //void Pay(Person ptr) //传值调用不可以
    11. //{
    12. // ptr.BuyTicket();
    13. //}

    5.虚函数重写的例外:

    协变(父类与子类虚函数返回值类型不同)

    子类重写父类虚函数时,与父类虚函数返回值类型不同 称为协变。

    虚函数重写对返回值要求有一个例外:协变,协变是子类虚函数与父类虚函数返回值类型不同,但子类和父类的返回值类型也必须是父子关系指针和引用。

    子类虚函数没有写virtual,f依旧是虚函数,因为子类先继承了父类函数接口声明(接口部分是virtual A* f()  ),重写是重写父类虚函数的实现部分( 重写函数实现部分是用子类虚函数的{ }里面的函数实现替代父类虚函数的{ }里面的函数实现 )        ps:我们自己写的时候子类虚函数也写上virtual

    1. class A{};
    2. class B : public A {};
    3. // 虚函数重写对返回值要求有一个例外:协变,父子关系指针和引用
    4. //
    5. class Person {
    6. public:
    7. virtual A* f() {
    8. cout << "virtual A* Person::f()" << endl;
    9. return nullptr;
    10. }
    11. };
    12. class Student : public Person {
    13. public:
    14. // 子类虚函数没有写virtual,f依旧时虚函数,因为先继承了父类函数接口声明
    15. // 重写父类虚函数实现
    16. // ps:我们自己写的时候子类虚函数也写上virtual
    17. // B& f() {
    18. virtual B* f() {
    19. cout << "virtual B* Student::f()" << endl;
    20. return nullptr;
    21. }
    22. };
    23. int main()
    24. {
    25. Person p;
    26. Student s;
    27. Person* ptr = &p;
    28. ptr->f();
    29. ptr = &s;
    30. ptr->f();
    31. return 0;
    32. }

    6.接口继承和实现继承

    普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实
    现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成
    多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
    所以就有了 子类虚函数没有写virtual,依旧是虚函数;子类虚函数使用的是父类虚函数的缺省参数,只是重写了实现

    多态的坑题目(考接口继承)

    子类虚函数没有写virtual,func 依旧是虚函数,因为子类先继承了父类函数接口声明(接口部分是virtual A* f()  ),重写是重写父类虚函数的实现部分( 重写函数实现部分是用子类虚函数的{ }里面的函数实现替代父类虚函数的{ }里面的函数实现 )        ps:我们自己写的时候子类虚函数也写上virtual

    p->test(),调用test中的this指针类型是A*,但指向的是对象B* p中的内容,类B中继承的test函数中又调用func函数,func函数没有写virtual 但依旧是虚函数,只要是虚函数重写就是接口继承,子类先继承了父类函数接口声明(父类接口部分是virtual void func(int va1=1) ),重写是重写父类虚函数的实现部分( 即使用子类的函数的实现部分{}内容 ),所以缺省函数用的是父类的1,实现用的子类的函数实现,打印结果是 B->1

    7.析构函数的重写-析构函数名统一会被处理成destructor()

    只有派生类 Student 的析构函数重写了 Person 的析构函数,下面的 delete 对象调用析构函
    数,才能构成多态,才能保证 p1 p2 指向的对象正确的调用析构函数。
    函数名处理成destructor() 才能满足多态:
    如果父类的析构函数为虚函数,此时子类析构函数只要定义,无论是否加virtual关键字,
    都与父类的析构函数构成重写,虽然父类与子类析构函数名字不同。虽然函数名不相同,
    看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处
    理,编译后析构函数的名称统一处理成destructor。
    1. class Person {
    2. public:
    3. virtual ~Person() {cout << "~Person()" << endl;}
    4. };
    5. class Student : public Person {
    6. public:
    7. virtual ~Student() { cout << "~Student()" << endl; }
    8. };
    9. int main()
    10. {
    11. Person* p1 = new Person;
    12. Person* p2 = new Student;
    13. delete p1;
    14. delete p2;
    15. return 0;
    16. }

     2.注意:期望delete ptr调用析构函数是一个多态调用, 如果设计一个类,可能会作为基类,其次析构函数最好定义为虚函数
    1. class Person {
    2. public:
    3. virtual ~Person()
    4. {
    5. cout << "~Person()" << endl;
    6. }
    7. };
    8. class Student : public Person {
    9. public:
    10. // Person析构函数加了virtual,关系就变了
    11. // 重定义(隐藏)关系 -> 重写(覆盖)关系
    12. virtual ~Student() //这里virtual加不加都行
    13. {
    14. cout << "~Student()" << endl;
    15. delete[] _name;
    16. cout << "delete:" << (void*)_name << endl;
    17. }
    18. private:
    19. char* _name = new char[10]{ 'j','a','c','k' };
    20. };
    21. int main()
    22. {
    23. // 对于普通对象是没有影响的
    24. //Person p;
    25. //Student s;
    26. // 期望delete ptr调用析构函数是一个多态调用
    27. // 如果设计一个类,可能会作为基类,其次析构函数最好定义为虚函数
    28. Person* ptr = new Person;
    29. delete ptr; // ptr->destructor() + operator delete(ptr)
    30. ptr = new Student;
    31. delete ptr; // ptr->destructor() + operator delete(ptr)
    32. return 0;
    33. }

    8.C++11 override final

    (1)final :修饰虚函数,表示该虚函数不能再被重写;修饰类,该类不能被继承

     (2)override:override写在子类中,要求严格检查是否完成重写,如果没有完成重写就报错

     示例:如果父类没写virtual能检查出来并报错

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

    (只有重写要求原型相同,原型相同就是指 函数名/参数/返回值都相同)

    函数重载:在同一个作用域中,两个函数的函数名相同,参数个数,参数类型,参数顺序至少有一个不同,函数返回值的类型可以相同,也可以不相同。

    重定义(也叫做隐藏)是指在继承体系中,子类重新定义父类中有相同名称的非虚函数 ( 参数列表可以不同 ) ,此时子类的函数会屏蔽掉父类的那个同名函数。

    重写(也叫做覆盖)是指在继承体系中子类定义了和父类函数名,函数参数,函数返回值完全相同的虚函数。此时构成多态,根据对象去调用对应的函数。

    10.抽象类

    在虚函数的后面写上 =0 ,则这个函数为纯虚函数包含纯虚函数的类叫做抽象类(也叫接口
    类),抽象类不能实例化出对象,但可以new别的对象来定义指针,例如Car* pBMW = new BMW;
    (1)子类继承后也不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象。
    (2)父类的纯虚函数强制了派生类必须重写,才能实例化出对象(跟override异曲同工,override是放在子类虛函数,检查重写。功能有一些重叠和相似
    另外纯虚函数更体现出了接口继承。
    (3)纯虚函数也可以写实现{ },但没有意义,因为是接口继承,{ }中的实现会被重写;父类没有对象,所以无法调用纯虚函数

    1. 抽象类 -- 在现实一般没有具体对应实体
    2. 不能实例化出对象
    3. 间接功能:要求子类需要重写,才能实例化出对象
    4. class Car
    5. {
    6. public:
    7. virtual void Drive() = 0;
    8. // // 实现没有价值,因为没有对象会调用他
    9. // /*virtual void Drive() = 0
    10. // {
    11. // cout << " Drive()" << endl;
    12. // }*/
    13. };
    14. class Benz :public Car
    15. {
    16. public:
    17. virtual void Drive()
    18. {
    19. cout << "Benz-舒适" << endl;
    20. }
    21. };
    22. class BMW :public Car
    23. {
    24. public:
    25. virtual void Drive()
    26. {
    27. cout << "BMW-操控" << endl;
    28. }
    29. };
    30. void Test()
    31. {
    32. Car* pBenz = new Benz;
    33. pBenz->Drive();
    34. Car* pBMW = new BMW;
    35. pBMW->Drive();
    36. }

    .多态的原理

    1.虚函数介绍

    被virtual修饰的成员函数称为虚函数,虚函数的作用是用来实现多态,只有在需要实现多态时,才需要将成员函数设置成虚函数,否则没有必要

    2.虚函数表

    和菱形虚拟继承的虚基表不一样,那个存的是偏移量

    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)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数
    的地址要被放到虚函数表中,虚函数表也简称虚表,。那么派生类中这个表放了些什么呢?我们
    接着往下分析

    3.虚表存储

    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. void Func3()
    27. {
    28. cout << "Derive::Func3()" << endl;
    29. }
    30. private:
    31. int _d = 2;
    32. };
    33. int main()
    34. {
    35. cout << sizeof(Base) << endl;
    36. Base b;
    37. cout << sizeof(Derive) << endl;
    38. Derive d;
    39. Base* p = &b;
    40. p->Func1();
    41. p->Func3();
    42. p = &d;
    43. p->Func1();
    44. p->Func3();
    45. // /*Base& r1 = b; 引用也是多态调用
    46. // r1.Func1();
    47. // r1.Func3();
    48. //
    49. // Base& r2 = d;
    50. // r2.Func1();
    51. // r2.Func3();*/
    52. }

    (1)虚函数重写/覆盖 语法与原理层解释

    --语法层的概念: 派生类对继承基类虚函数实现进行了重写
    --原理层的概念: 子类的虚表,拷贝父类虚表进行了修改,覆盖重写那个虚函数

    (2)虚表存储解释

    无论是子类还是父类中只要有虚函数都会多存一个指针,这个指针叫虚表指针,他指向一个指针数组,指针数组中存着各个虚函数的地址。

    Func1是重写的函数,Base[0]中存的地址并非真正的Derive中Func1的地址,而是通过call这个地址,找到这个地址的内容,这个地址的内容指令又是jump到地址2,地址2存的才是真正的Derive中Func1的地址

    多态调用和普通调用底层解释(编译时多态/运行时多态)

    C++语言的多态性分为编译时的多态性和运行时的多态性

    ①运行时多态是动态绑定,也叫晚期绑定;运行时的多态性可通过虚函数实现。

    ②编译时多态是静态绑定,也叫早期绑定,主要通过重载实现;编译时的多态性可通过函数重载模板实现。

    -在运行期间,通过传递不同类的对象,编译器选择调用不同类的虚函数:编译期间,编译器主要检测代码是否违反语法规则,此时无法知道基类的指针或者引用到底引用那个类的对象,也就无法知道调用哪个类的虚函数。在程序运行时,才知道具体指向那个类的对象,然后通过虚表调用对应的虚函数,从而实现多态。

    1. class Person {
    2. public:
    3. virtual void BuyTicket() { cout << "买票-全价" << endl; }
    4. void Buy() { cout << "Person::Buy()" << endl; }
    5. };
    6. class Student : public Person {
    7. public:
    8. virtual void BuyTicket() { cout << "买票-半价" << endl; }
    9. void Buy() { cout << "Student::Buy()" << endl; }
    10. };
    11. void Func1(Person* p)
    12. {
    13. 跟对象有关,指向谁调用谁 -- 运行时确定函数地址
    14. p->BuyTicket();
    15. 跟类型有关,p类型是谁,调用就是谁的虚函数 -- 编译时确定函数地址
    16. p->Buy();
    17. }
    18. int main()
    19. {
    20. Person p;
    21. Student s;
    22. Func1(&p);
    23. Func1(&s);
    24. return 0;
    25. }

     

     

    重点总结:
    多态调用:运行时决议-- 运行时确定调用函数的地址(不管对象类型,查对应的虚函数表,如果是父类的对象,就查看父类对象中存的虚表;如果是子类切片后的对象,就查看子类切片后对象中存的虚表)
    普通调用:编译时决议-- 编译时确定调用函数的地址(只看对象类型去确定调用哪个对象中的函数)

    (3)父类赋值给子类对象,也可以切片。为什么实现不了多态?

    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. void Func3()
    27. {
    28. cout << "Derive::Func3()" << endl;
    29. }
    30. private:
    31. int _d = 2;
    32. };
    33. int main()
    34. {
    35. cout << sizeof(Base) << endl;
    36. Base b;
    37. cout << sizeof(Derive) << endl;
    38. Derive d;
    39. // 父类赋值给子类对象,也可以切片。为什么实现不了多态?
    40. Base r1 = b;
    41. r1.Func1();
    42. r1.Func3();
    43. Base r2 = d;
    44. r2.Func1();
    45. r2.Func3();
    46. return 0;
    47. }

    我们发现r2没有拷贝子类d的虚表,则r2虚表中存的还是父类的虚表,调用时还是调用父类的func1,而不是子类切片后的func1

    (4)静态和动态的多态(了解)

    (5)非多态的虚函数Func4在监视窗口被隐藏了,看不到,只能通过内存看到

    1. class Derive : public Base
    2. {
    3. public:
    4. // 重写
    5. virtual void Func1()
    6. {
    7. cout << "Derive::Func1()" << endl;
    8. }
    9. void Func3()
    10. {
    11. cout << "Derive::Func3()" << endl;
    12. }
    13. virtual void Func4()
    14. {
    15. cout << "Derive::Func4()" << endl;
    16. }
    17. private:
    18. int _d = 2;
    19. };
    20. // 取内存值,打印并调用,确认是否是func4
    21. //typedef void(*)() V_FUNC; // 不支持这种写法
    22. typedef void(*V_FUNC)(); // 只能这样定义函数指针
    23. // 打印虚表
    24. //void PrintVFTable(V_FUNC a[])
    25. void PrintVFTable(V_FUNC* a)
    26. {
    27. printf("vfptr:%p\n", a);
    28. for (size_t i = 0; a[i] != nullptr; ++i) //VS下的虚表以空指针结束
    29. {
    30. printf("[%d]:%p->", i, a[i]); //打印虚表中的所有函数的地址
    31. V_FUNC f = a[i]; //调用函数中打印函数,可以知道是哪个func函数
    32. f();
    33. }
    34. }
    35. int c = 2;
    36. int main()
    37. {
    38. Base b;
    39. Derive d;
    40. PrintVFTable((V_FUNC*)(*((int*)&d))); //下有解释
    41. }

     PrintVFTable((V_FUNC*)(*((int*)&d))); 解释:

    因为对象中存虚表指针,虚表指针中存的是虚表(一个指针数组),则需要先解引用访问到这个对象的前四个字节内容(存的就是虚表指针),此时的虚表指针 *((int*)&d)是一个int类型,再把虚表指针类型强转成指针数组类型才能传参

    监视窗口和内存窗口:

    (6)同一类型对象,共用一个虚表

    一个类型公共一个虚表,所有这个类型对象都存这个虚表指针

    (7)虚表存在常量区/代码段

    不可能存在栈,栈区是建立栈帧,出作用域栈帧销毁, 虚表是一个永久的存在,排除栈。

    不可能存在堆区,堆区是动态申请,最后动态释放

    可以存在静态区或者常量区,最可能存在常量区。通过下面打印地址可见虚表存储地址离常量区地址最近 

     

    4.多继承,虚表的存储(一个子类继承两个父亲时)

    大体的结论就是:func1是重写的函数,在子类的两个父类的虚表中存储的func1地址不相同,但是通过一系列的call这个地址,这个地址的内容又是jump到另一个指令,最终都会跳到子类重写的func1地址上

    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. // 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
    26. cout << " 虚表地址>" << vTable << endl;
    27. for (int i = 0; vTable[i] != nullptr; ++i)
    28. {
    29. printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
    30. VFPTR f = vTable[i];
    31. f();
    32. }
    33. cout << endl;
    34. }
    35. int main()
    36. {
    37. printf("%p\n", &Derive::func1);
    38. Derive d;
    39. //PrintVTable((VFPTR*)(*(int*)&d));
    40. PrintVTable((VFPTR*)(*(int*)&d));
    41. PrintVTable((VFPTR*)(*(int*)((char*)&d+sizeof(Base1))));
    42. }

     PrintVTable((VFPTR*)(*(int*)&d)); 

    因为对象中存虚表指针,虚表指针中存的是虚表(一个指针数组),则需要先解引用访问到这个对象的前四个字节内容(存的就是虚表指针),此时的虚表指针 *((int*)&d)是一个int类型,再把虚表指针类型强转成指针数组类型才能传参

    PrintVTable((VFPTR*)(*(int*)((char*)&d+sizeof(Base1)))); 是找到Base2的虚表地址后再解引用找到虚表(直接加2个int字节也能找到base2,考虑Base1可能不单单是2个int大小,这里建议用sizeof(Base1) )

     

    结论: Derive对象Base2虚表中func1时,是Base2指针ptr2去调用。但是这时ptr2发生切片指针偏移,需要修正。中途就需要修正存储this指针ecx的值
     

    四.虚函数使用规则

    1. 什么是多态?答:参考本节内容
    2. 什么是重载、重写(覆盖)、重定义(隐藏)?答:参考本节内容
    3. 多态的实现原理?答:参考本节内容
    虚函数使用规则:

    (1)虚函数在类中声明和类外定义的时候,virtual关键字只在声明时加上,而不能加在在类外实现上

    (2)静态成员不可以是虚函数。因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

    (3)友元函数不属于成员函数,不能成为虚函数

    (4)静态成员函数就不能设置为虚函数(原因:静态成员函数与具体对象无关,属于整个类,核心关键是没有隐藏的this指针,可以通过类名::成员函数名 直接调用,此时没有this无法拿到虚表就无法实现多态,因此不能设置为虚函数

    (5)析构函数建议设置成虚函数,因为有时可能利用多态方式通过基类指针调用子类析构函数(尤其是父类的析构函数强力建议设置为虚函数,这样动态释放父类指针所指的子类对象时,能够达到析构的多态)

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

    答:可以,不过多态调用的时候编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中去。
    5. 静态成员可以是虚函数吗?
    答:不能,因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
    6. 构造函数可以是虚函数吗?
    答:不能,因为对象中的 虚函数表指针 是在 构造函数初始化列表阶段才初始化的 。虚函数的意义是多态,多态调用时到虚函数表中去找,构造函数之前还没初始化,如何去找?
    7. 析构函数可以是虚函数吗?
    什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。析构函数名统一会被处理成destructor()
    8. 对象访问普通函数快还是虚函数更快?
    答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
    9. 虚函数表是在什么阶段生成的,存在哪的?
    答: 虚函数表是在编译阶段就生成的 ,一般情况下存在代码段(常量区)的。( 虚函数表指针初始化是指把虚函数表的指针放到对象中去,但生成仍是在编译阶段
    10. C++菱形继承的问题?虚继承的原理?
    答:参考继承课件。注意这里不要把虚函数表和虚基
    表搞混了。
    11. 什么是抽象类?抽象类的作用?
    答:参考(3.抽象类)。抽象类强制重写了虚函数,另外抽
    象类体现出了接口继承关系。

  • 相关阅读:
    九、T100应付管理之应付期末账务处理
    同样做软件测试,和月收入 3W 的学弟聊了一晚上,我彻底崩溃了
    痛快,SpringBoot终于禁掉了循环依赖
    [信息论]LZW编解码的实现(基于C++&Matlab实现)
    Airflow用于ETL的四种基本运行模式, 2022-11-20
    代码随想录训练营 | 一刷总结
    详解CentOS8更换yum源后出现同步仓库缓存失败的问题
    curl (56) Recv failure Connection reset by peer
    假脸检测:Exploring Decision-based Black-box Attacks on Face Forgery Detection
    桥接模式学习
  • 原文地址:https://blog.csdn.net/zhang_si_hang/article/details/126173598