【C++】多态

[复制链接]
发表于 2025-9-23 06:14:51 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
声明:该博客代码都是在vs2022下x86步调中,涉及的指针都是4bytes。如果是x64步调,则必要考虑指针是8bytes题目。
1.多态的概念

就是多种形态,具体就是去完成某个举动,当差别的对象去完成时会产生出差别的状态。
2.多态的界说及实现

多态构成条件


  • 必须通过基类的指针大概引用调用虚函数
  • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数举行重写。只有如许才能指针指向父类时调父类,指向子类时调子类

重点:
满意多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满意多态的函数调用时编译时确认好的。
虚函数

被virtual修饰的类成员函数称为虚函数
留意:只有成员函数才能变成虚函数,全局函数不可。
虚函数和虚继承只是公用“虚”关键字,一点关系也没有,相互独立
声明时加上 virtual:告诉编译器这个函数是虚函数。
实现时不加 virtual:克制语法错误,编译器已经知道这是虚函数。
虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全类似的虚函数(即派生类虚函数与基类虚函数的返回值范例、函数名字、参数范例(跟形参无关)完全类似),称子类的虚函数重写了基类的虚函数。
条件
虚函数(virtual修饰),三同(返回值,参数,函数名三者类似)
破例:
1、派生类的重写虚函数可以不加virtual(发起加上):只要基类是虚函数,被继承下去在派生类中依然保持虚函数属性。
2.协变,返回的值可以差别,但是要求返回值必须是父子关系指针和引用
  1. #include<iostream>
  2. using namespace std;
  3. class A
  4. {};
  5. class B : public A
  6. {};
  7. class Person {
  8. public:
  9.         virtual A* BuyTicket() const {
  10.                 cout << "买票-全价" << endl;
  11.                 return 0;
  12.         }
  13. };
  14. class Student : public Person {
  15. public:
  16.         virtual B* BuyTicket() const {
  17.                 cout << "买票-半价" << endl;
  18.                 return 0;
  19.         }
  20. };
  21. void func(const Person * p)
  22. {
  23.         p->BuyTicket();
  24. }
  25. int main()
  26. {
  27.         Person pp;
  28.         func(&pp);
  29.         Student st;
  30.         func(&st);
  31.         return 0;
  32. }
复制代码
若基类没有virtual修饰,不构成虚函数重写

该函数为参数为平常对象,看当前者范例来确定一直调用的基类函数
virtual修饰,构成虚函数重写

析构函数的重写


子类析构完后会主动调用父类析构,然后父类再析构,因为子类中有一份父类的拷贝
析构函数加virtual,是不是虚函数重写?
是,因为类析构函数都被处置惩罚成destructor这个同一的名字,这么处置惩罚就是为了让它们构成重写
为什么要构成重写,因为如下场景

若不构成虚函数重写

析构函数只要基类加了virtual修饰就肯定是虚函数重写,没有返回值与参数,函数名底层类似,可以说子类可以不加virtual重要是为析构计划的
override和final

函数名字母序次写反而无法构成重载,这种错误在编译期间是不会报出的,只有在步调运行时通过预期效果debug得出。
C++11提供了override和final两个关键字,可以资助用户检测是否重写。
怎样计划不想被继承的类?
方法1:基类构造函数私有   (C++98)

A类中将构造函数设为私有,从而派生类B无法继承A因为无法调用基类的构造函数。为了A类自己能创建对象,在公共地域用了static来封装构造函数,static 方法属于类自己,而不是类的实例。这意味着可以在不创建类实例的环境下调用 static 方法。
方法2:基类加一个final  (C++11)

override使用场景:

在必要检测重写的函数参数名和括号后加上关键字
重载、覆盖(重写)、隐蔽(重界说)的对比


一道虚函数重写例题


答案B。
通过该题需明白,虚函数的意义就是重写,重写的是实现,框架用的是基类的(函数返回范例,参数,函数名),以是派生类可以不加virtual。这是一个语法题目,实际中通常不这么界说
3.抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包罗纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
  1. class Car
  2. {
  3. public:
  4.         virtual void Drive() = 0;
  5. };
  6. class Benz :public Car
  7. {
  8. public:
  9.         inline virtual void Drive()
  10.         {
  11.                 cout << "Benz-舒适" << endl;
  12.         }
  13. };
  14. class BMW :public Car
  15. {
  16. public:
  17.         inline virtual void Drive()
  18.         {
  19.                 cout << "BMW-操控" << endl;
  20.         }
  21. };
  22. class BYD :public Car
  23. {
  24. public:
  25.         inline virtual void Drive()
  26.         {
  27.                 cout << "BYD-build your dream" << endl;
  28.         }
  29. };
  30. void Func(Car* p)
  31. {
  32.         p->Drive();
  33.         p->Func();
  34. }
  35. int main()
  36. {
  37.         Func(new Benz);
  38.         Func(new BMW);
  39.         Func(new BYD);
  40.         return 0;
  41. }
复制代码
抽象类不能实例化出对象,但可以界说指针或引用如Func函数的参数范例。
inline函数可以是虚函数吗

可以,不外编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去
红方框代表放进虚表,固然Func函数直接展开但无法克制调用开销,因为虚函数的调用必要动态绑定。在反汇编代码中,内联函数 Func 的调用会被直接展开,没有 call 指令


如果在类中声明和界说分离,就不构成内联


接口继承和实现继承

平常函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,告竣多态,继承的是接口。以是如果不实现多态,不要把函数界说成虚函数。
4.多态的原理

虚函数表


vs2022x86步调中,涉及的指针都是4bytes,char_b占用一字节,为了内存对齐,编译器大概会在 _b 后面插入3字节的添补,使得类的总巨细为8字节。多出来的空间即为虚函数表指针的巨细

除了_b成员,还多一个__vfptr放在对象的前面(留意有些平台大概会放到对象的末了面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)
针对上面的代码我们做出以下改造,进一步观察
1.我们增加一个派生类Derive去继承Base
2.Derive中重写Func1
  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(地点发生变革),Func2没有重写以是地点不变。以是虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3.Func3也继承下来了,但不是虚函数,不进虚表
4.

虚函数表本质是一个存虚函数指针的指针数组,一般环境这个数组末了面放了一个nullptr。如果没有重新天生一下解决方案
总结一下派生类的虚表天生:
1.先将基类中的虚表内容拷贝一份到派生类虚表中
2.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
3.派生类自己新增加的虚函数按其在派生类中的声明序次增加到派生类虚表的末了。
虚函数表在编译器就天生好,虚函数指针在实行初始化列表时确认·
虚函数表存在那里的?

同范例的对象共用虚表

通过上图可得,虚表指针是类对象地点中前四个字节,我们可以通过将类对象地点强转成int*指针就刚好取到虚表指针地点,借此来观察虚表在内存中的存储位置

  • 测试代码
  1. class Person {
  2. public:
  3.         virtual void BuyTicket() { cout << "买票-全价" << endl; }
  4. };
  5. class Student : public Person {
  6. public:
  7.         virtual void BuyTicket() { cout << "买票-半价" << endl; }
  8. };
  9. void Func(Person& p)
  10. {
  11.         p.BuyTicket();
  12. }
复制代码

观察得出虚表地点与常量区(代码段)地点最为靠近,相差不到100字节,相比其他存储地域可以以为最有大概存储在常量区
子类虚函数在监督窗口和内存中的环境(单继承)


  • 测试代码
  1. class Person
  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. protected:
  13.         int _b = 1;
  14. };
  15. class Student: public Person
  16. {
  17. public:
  18.         virtual void Func3()
  19.         {
  20.                 cout << "Base::Func3()" << endl;
  21.         }
  22. protected:
  23.         int _d = 2;
  24. };
复制代码


监督窗口中没有看到派生类的虚函数func3,只能看到父类的虚函数表,但在内存窗口中多出一个地点,猜疑是func3,怎样验证?编译器的监督窗口故意隐蔽了这个函数,也可以以为是他的一个小bug

FUNC_PTR* table:指向函数指针数组的指针。这个数组存储了一系列函数指针。
FUNC_PTR f = table:将当前函数指针赋值给局部变量 f。
f():通过函数指针调用对应的函数。
留意:FUNC_PTR table[] 和 FUNC_PTR* table 是等价的。它们都表示一个指向函数指针的指针。
FUNC_PTR table[] 表示一个函数指针数组。table 是一个数组,其元素范例是 FUNC_PTR,在C++中,数组名作为参数时,会退化为指向数组第一个元素的指针。因此,FUNC_PTR table[] 实际上等价于 FUNC_PTR* table

证明我们猜测准确,监督窗口中只能看父类客气环境,内存中存储按照基类和派生类的界说次序来。子类自己的虚函数只会放到第一个父类的虚表后面,其他父类的虚函数表不必要被子类存储,因为它们的虚函数可以通过调解虚函数表指针来访问。
再深入明白多态构成条件

多态的条件:
1.父类的指针和引用
2.虚函数的重写
测试代码:
  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. //protected:
  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. protected:
  27.         int _d = 2;
  28. };
复制代码
提出以下题目:
1.中为什么不能是子类指针或引用?
因为只有父类才可以既指向基类又指向派生类,满意多态条件
2.(1.)中为什么不能传父类对象

可观察得到,对象d中修改_b后拷贝到对象b中去,但虚函数表没有拷贝过去。若拷贝,会发生对象切片。这意味着父类对象只会包罗派生类对象中继承自父类的部门,派生类中新增的成员变量和方法将被忽略。那么调用父类对象中的虚表就不是父类而变成子类的了,乱套了。以是子类赋值给父类对象切片,不会拷贝虚表。这就是不能用父类对象的缘故原由。
基类的指针和引用
通过虚函数表和虚函数指针,答应运行时决定调用哪个类的实现。在运行时根据对象的实际范例调用相应的方法。

基类指针或引用同样不会复制派生类中新增的成员变量和方法,但是可以通过引用或指针访问派生类对象的完整内容。这就是与父类对象的区别
多态原理


  • 测试代码
  1. class Person {
  2. public:
  3.         virtual void BuyTicket() { cout << "买票-全价" << endl; }
  4. };
  5. class Student : public Person {
  6. public:
  7.         virtual void BuyTicket() { cout << "买票-半价" << endl; }
  8. };
  9. void Func(Person* p)
  10. {
  11.         p->BuyTicket();
  12. }
  13. int main()
  14. {
  15.         Person ps;
  16.         Student st;
  17.         Student* ptr = &st;
  18.         Func(&st);
  19.         ptr->BuyTicket();
  20.         ps.BuyTicket();
  21.         return 0;
  22. }
复制代码
Func函数参数是基类的指针或引用,内里是虚函数,构成多态。接下来通过在返回编具体观察多态的实现


Func函数固然运行效果构成多态,但其在反汇编中就是平常函数的调用,都不是类的成员函数。必要跳转到p->BuyTicket();才能观察到多态的实现

出满意多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满意多态的函数调用时编译时确认好的。

先BuyTicket固然是虚函数,但是ps是对象,不满意多态的条件,以是这里是平常函数的调用转换成地点时,是在编译时已经从符号表确认了函数的地点,直接call 地点
动态绑定与静态绑定

静态绑定又称为前期绑定(早绑定),在步调编译期间确定了步调的举动,也称为静态多态,比如:函数重载,模板
动态绑定又称后期绑定(晚绑定),是在步调运行期间,根据具体拿到的范例确定步调的具体举动,调用具体的函数,也称为动态多态。比如继承,虚函数重写。
5.多继承关系的虚表


  • 测试代码
  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(*FUNC_PTR) ();
  23. //打印函数指针数组
  24. //void PrintfVFT(FUNC_PTR table[])
  25. void PrintfVFT(FUNC_PTR* table)
  26. {//Linux下函数指针数组最后不放nullptr条件要写死
  27.         for (size_t i = 0; table[i] != nullptr; i++)
  28.         {
  29.                 printf("[%d]:%p->", i, table[i]);
  30.                 FUNC_PTR f = table[i];
  31.                 f();
  32.         }
  33.         printf("\n");
  34. }
复制代码

20是一个Derive类对象占用的空间巨细,此中Base1和Base2中各有一个虚函数表指针占用4字节和int范例成员变量占用4字节,各占用8字节,总计16字节,再加上Derive类自己int范例成员变量4字节巨细。
可以发现固然重写了func1,但是Base1和Base2中func1不一样。
观察发现,多继承派生类的未重写虚函数放在第一个继承基类部门的虚函数表中
通过反汇编观察

区别在于Base1在调用时直接call func1的地点,而func2在调用时必要修正this指针再call地点。

缘故原由在于内存中没有范例的概念,范例是代码中的概念,取决我们用什么样的方式去表明它。Base1和Derive类指针的起始位置类似,可以直接调用,而Base2指针的位置在起始位置+sizeof(Base1)的位置,以是必要修正到起始位置,可以以为Base1中func1地点为真地点,而Base2中的地点是被封装后的。
这里的封装是调用函数之后再举行修正,也可以在调用之前就举行修正然后直接调用
6.菱形捏造继承(相识)

实际中我们不发起计划出菱形继承及菱形捏造继承,一方面太复杂轻易出题目,另一方面如许的模子,访问基类成员有肯定得性能斲丧。

  • 测试代码
  1. class A
  2. {
  3. public:
  4.         virtual void func1()
  5.         {
  6.                 cout << "A::func1" << endl;
  7.         }
  8. public:
  9.         int _a;
  10. };
  11. //class B : public A
  12. class B : virtual public A
  13. {
  14. public:
  15.         virtual void func1()
  16.         {
  17.                 cout << "B::func1" << endl;
  18.         }
  19.         virtual void func2()
  20.         {
  21.                 cout << "B::func2" << endl;
  22.         }
  23. public:
  24.         int _b;
  25. };
  26. //class C : public A
  27. class C : virtual public A
  28. {
  29. public:
  30.         virtual void func1()
  31.         {
  32.                 cout << "C::func1" << endl;
  33.         }
  34.         virtual void func2()
  35.         {
  36.                 cout << "C::func2" << endl;
  37.         }
  38. public:
  39.         int _c;
  40. };
  41. class D : public B, public C
  42. {
  43. public:
  44.         virtual void func1()
  45.         {
  46.                 cout << "D::func1" << endl;
  47.         }
  48.         virtual void func3()
  49.         {
  50.                 cout << "D::func3" << endl;
  51.         }
  52. public:
  53.         int _d;
  54. };
  55. int main()
  56. {
  57.         D d;
  58.         d.B::_a = 1;
  59.         d.C::_a = 2;
  60.         d._b = 3;
  61.         d._c = 4;
  62.         d._d = 5;
  63.         return 0;
  64. }
复制代码
单纯菱形继承的环境:

本质和多继承一样,有两张虚表,_a在B和C中各有一份
菱形捏造继承:
当基类有一个虚函数,中央两个派生类对其完成重写就会语法报错?
缘故原由是基类的虚函数属于中央两个派生类共享的,只有一张虚函数表不知道重写算谁的。
解决办法让末了派生类再重写一次,虚表中就是它覆盖的地点。中央派生类的重写只有在单独重写时会故意义。以是中央派生类会各自创建一张虚基表来存储到覆盖位置的偏移量

更复杂的场景,中央派生类添加一些不重写的虚函数:
不光基类有虚函数表,中央派生类各自也会有一张虚函数表,如有重写基类的虚函数,还会套有虚基表。虚基表中第二个位置存偏移量,可以存虚函数表的偏移量

可以看出终极派生类有几个父类,如果父类有虚函数,则就会有几张虚表,自身子类不会产生多余的虚表。
7.常考问答题总结


  • 什么是多态?
静态多态:函数重写
动态多态:继承中虚函数重+父类指针调用
目的是为了更方便和机动的举行多种形态调用。

  • 什么是重载、重写(覆盖)、重界说(隐蔽)?


  • 多态的实现原理?
函数名修饰规则+虚函数表,详见本章

  • inline函数可以是虚函数吗?
可以,不外编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。详见本章

  • 静态成员可以是虚函数吗?
不能,因为静态成员函数没有this指针,使用范例::成员函数的调用方式无法访问虚函数表,以是静态成员函数无法放进虚函数表。无法实现多态,没故意义,static和virtual是不能同时使用的。

  • 构造函数可以是虚函数吗?
不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。这里就像先有鸡还是先有蛋的题目
虚函数表尚未初始化:在构造函数实行时,虚函数表指针还未完全初始化,因此无法准确调用虚函数。
构造次序题目:基类构造函数先于派生类构造函数实行。如果构造函数是虚函数,基类构造函数大概会调用派生类的函数,导致未界说举动。

  • 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
可以将析构函数声明为虚函数通常用于多态场景,特殊是在通过基类指针或引用删除派生类对象时,确保准确调用派生类的析构函数。在多态场景中,如果基类的析构函数不是虚函数,通过基类指针删除派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数,这大概导致资源走漏或未界说举动。
良好风俗:如果基类包罗虚函数,通常应该将析构函数声明为虚函数。

  • 对象访问平常函数快还是虚函数更快?
起首如果是平常对象,是一样快的。如果是指针对象大概是引用对象,则调用的平常函数快,因为构成多态,运行时调用虚函数必要到虚函数表中去查找。

  • 虚函数表是在什么阶段天生的,存在哪的?
虚函数表是在编译阶段就天生的,一般环境下存在代码段(常量区)的。

  • C++菱形继承的题目?虚继承的原理?
数据冗余和二义性。
引入虚基类指针,确保基类只被继承一次,从而克制菱形继承的题目。虚基类的初始化由最派生类负责,访问虚基类成员时必要颠末两次解引用操纵(从派生类对象到虚基类指针。通过虚基类指针访问成员。)。

  • 什么是抽象类?抽象类的作用?
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包罗纯虚函数的类叫做抽象类(也叫接口类)。
抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表