2021-09-15 C++ 继承和多态(虚函数,纯虚函数,虚继承)

C++继承和多态(虚函数、纯虚函数、虚继承)

 

一:继承

继承的概念:为了代码的复用,保留基类的原始结构,并添加派生类的新成员。

继承的本质:代码复用

我们用下图解释下:

 

那么我们这里就可以提出几个问题了:

①:进程的方式有哪些呢?

这里有三种继承方式:

  • public:任意位置可以访问
  • protected:只允许本类类中以及子类类中访问
  • private:只允许本类类中访问

②:派生类继承了基类的什么?

  • 所有成员变量,包括static静态成员变量
  • 成员方法,除构造和析构以外的所有方法
  • 作用于也继承了,但是友元关系没有继承

③:派生类生成的对象的内存布局是什么样的?

派生类对象构造时,基类数据在前,派生类自身数据在后。

如下图所示:(B继承了A)

④:派生类对象的构造析构顺序

(1):派生类对象的构造顺序:

  • 系统调用基类的构造(没有指明构造方式,则按默认的走)
  • 系统调用派生类的构造

(2):派生类对象的析构顺序:

  • 系统调用派生类的析构
  • 系统调用基类的析构

 

那么基类中不同访问限定符下的成员 以不同的继承方式 继承后在派生类中的访问限定是什么样的呢?

核心思想:继承后的权限不会大于继承方式的权限

我们这里有一张图可以概括:

我们可以用以下代码来验证:

测试代码如下:


   
   
  1. #include <iostream>
  2. class Base
  3. {
  4. public:
  5. Base( int a = 10, int b = 20, int c = 30):ma(a), mb(b), mc(c){}
  6. public:
  7. int ma;
  8. protected:
  9. int mb;
  10. private:
  11. int mc;
  12. };
  13. class Derived : private Base
  14. {
  15. public:
  16. void Show()
  17. {
  18. std:: cout << ma << std:: endl;
  19. std:: cout << mb << std:: endl;
  20. //std::cout << mc << std::endl;
  21. }
  22. };
  23. class Derived2 : public Derived
  24. {
  25. public:
  26. void Show()
  27. {
  28. std:: cout << ma << std:: endl;
  29. //std::cout << mb << std::endl;
  30. //std::cout << mc << std::endl;
  31. }
  32. };
  33. int main()
  34. {
  35. Derived d;
  36. Derived2 d2;
  37. d.Show();
  38. d2.Show();
  39. std:: cout << sizeof(Derived) << std:: endl;
  40. return 0;
  41. }

 

 

二:多态

多态的概念:多态可以使我们以相同的方式处理不同类型的对象,其实用一句话来说,就是允许将子类类型的指针赋值给父类类型的指针。

多态的本质:接口复用(一种接口,不同形态)

 

多态性在C++中是通过虚函数实现的。

虚函数:就是父类允许被其子类重新定义的成员函数,而子类重新定义父类函数的做法,称为“覆盖”,或者称为“重写”。

子类重写父类中虚函数时,即使没有virtual声明,该重载函数也是虚函数。

 

我们可以将多态分为3类:

  • 静多态:在编译阶段已经确定函数的入口地址,例如函数重载,模板等
  • 动多态:在运行阶段时才确定函数的入口地址,例如虚函数调用机制
  • 宏多态:例如宏函数,在预编译阶段已经进行了替换

 

动多态:

  1. 在编译期间生成虚函数表,表中保存函数入口地址
  2. 放在只读数据段.rodata
  3. 一个类共用一个虚表,而不是一个对象

而类中是不存在虚函数表的,主要由于数据冗余,太大了,所以都保存一个指针vfptr,让这个指针指向这个虚函数表即可。

那这个虚函数表长什么样子呢?

 

注意一点:

  • 如果基类中有一个成员函数是虚函数,那么派生类中与其同名同参的函数默认会变成虚函数,这是一个覆盖的关系。
  • 编译期间,派生类会生成自己的虚函数表,这时两个虚函数表会进行合并,同名同参的虚函函数覆盖了基类中同名同参的虚函数。

 

下面,我们将介绍一下什么是覆盖:

首先我们需要知道,类与类之间的关系有三种,分别为:

  • 组合 :a part of   是一个 has_a的关系(有一个),例如:A是B的一部分,则不允许B继承A的功能,而是要用A和其他东西组合成B,他们之间就是has_a的关系,现实中则就是眼睛,鼻子和脑袋的关系。
  • 继承 :a kind of 是一个is_a的关系(是一个),例如:若B是A的一种,则允许B继承A的功能,他们之间就是is_a的关系,现实中就是香蕉和水果的关系。
  • 代理 :限制底层的接口,提供新的接口。
  • 这里需要注意的是,用private方式继承,是一个has_a的关系,不是is_a的关系,是有一个的关系,不是是一个的关系。

 

而同名函数之间也有三种关系,分别为:

  • 重载 overload 重载三要素:同名,不同参,同作用域
  • 隐藏 overhide 继承时,派生类中同名的函数隐藏了继承来的同名方法,继承来的函数存在,但是看不到
  • 覆盖 override 继承时,派生类中同名的虚函数覆盖了继承来的同名方法,继承来的虚函数不存在,直接被覆盖掉了。

 

内存分布:

如果基类有虚函数,而派生类中也有虚函数,则如下图:

虚函数表和类是一对一的,这个vfptr指向的是派生类对象的虚表。

 

如果这里有这6个函数,那么哪些可以成为虚函数呢?

  1. 普通函数×(遵守__cdecall调用约定,不依赖对象调用)
  2. 构造函数×(虽然遵守__thiscall调用约定,但是手动调用不了)
  3. 析构函数√(遵守__thiscall调用约定,且可以手动调用)
  4. static修饰的成员方法×(遵守__cdecall调用约定,不依赖对象调用)
  5. inline函数×(inline函数无法取地址,它直接在调用点直接展开)
  6. 普通的成员函数√(遵守__thiscall调用约定,且可以手动调用)

首先我们得知清楚道成为虚函数的条件:

  • 能取地址(排除5)
  • 依赖对象调用(排除1,2,4)

并且如果在构造函数以及析构函数内调用虚函数,那么只会是一个静态绑定,因为这时依赖调用的对象已经不完整了。

注意:虚函数指针的写入时机,是在构造函数第一行代码之前。

 

重点:如果有基类的指针指向了派生类的对象,那么基类就要有虚析构。

首先我们需要了解的是动多态的发生时机:

  • 调用的对象需要完整(这也是为什么在构造函数以及析构函数中调用虚函数,只会触发静多态的原因)
  • 指针调用的是虚函数(要有virtual关键字标识)

这时候我们再来看一看原因:如果派生类申请了内存空间,并在其析构函数中进行了释放,假设由于基类中采用的是非虚析构函数,那么当基类的指针指向了派生类的对象后,当delete释放内存的时候,首先会调用析构函数,但是因为基类的析构函数并不是虚析构,只是普通析构函数,所以只会触发静态绑定(静多态),不会触发动态绑定(动多态),因此调用的是基类的析构函数,而不是派生类的析构函数,那么申请的空间就会得不到释放从而造成内存泄漏,所以,为了防止这种情况的发生,我们需要将基类中的析构函数写成虚析构。

 

注意:如果基类没写虚函数,而派生类写了虚函数,那么当基类指针指向派生类后,delete pb就会崩溃

例如下面代码:


   
   
  1. #include <iostream>
  2. class A
  3. {
  4. public:
  5. A( int a) :ma(a)
  6. {
  7. std:: cout << "A::A(int)" << std:: endl;
  8. }
  9. void Show()
  10. {
  11. std:: cout << "A::ma:" << ma << std:: endl;
  12. }
  13. ~A()
  14. {
  15. std:: cout << "A::~A()" << std:: endl;
  16. }
  17. protected:
  18. int ma;
  19. };
  20. class B : public A
  21. {
  22. public:
  23. B( int b) :A(b),mb(b)
  24. {
  25. std:: cout << "B::B()" << std:: endl;
  26. }
  27. virtual void Show()
  28. {
  29. std:: cout << "B::mb:" << mb << std:: endl;
  30. }
  31. ~B()
  32. {
  33. std:: cout << "B::~B()" << std:: endl;
  34. }
  35. private:
  36. int mb;
  37. };
  38. int main()
  39. {
  40. A* pa = new B( 10);
  41. pa->Show(); //class Base*
  42. //delete (A*)((char*)pa -4);
  43. delete pa;
  44. return 0;
  45. }

 

我们画个图分析一下为什么会崩溃:

原因:当基类指针指向派生类的时候,因为new是从0x100开辟的,但是由于基类指针赋值的是基类构造时的地址0x200,所以当开辟地址与释放地址不一致时,则会造成崩溃。

 

我们可以将基类中析构函数变成虚析构函数,那么基类就会有一个虚函数指针,当两者合并时,就不会产生开辟地址与释放地址不一致的问题了,因为此时内存布局如下:

这时内存开辟地址和释放地址都为0x100,则不会造成崩溃。

 

 

三:纯虚函数

纯虚函数:是一种特殊的虚函数,很多情况下,在基类中不能对虚函数给出有意义的实现,从而把它声明为纯虚函数,它的实现留给派生类去做。这就是纯虚函数的作用。

纯虚函数的两个特点:

  • 拥有纯虚函数的类叫做抽象类
  • 抽象类不能实例化对象

例如,动物这个类,可以派生出狗和猫这两个类,但是由于它俩都有发出叫声这个方法,那么我们想通过一个函数,通过传入不同的基类指针,实现发出不同的叫声。

代码如下:


   
   
  1. #include <iostream>
  2. #include <string>
  3. class Animal//抽象类
  4. {
  5. public:
  6. Animal( std:: string name) :mname(name)
  7. {
  8. std:: cout << "Animal::Animal()" << std:: endl;
  9. }
  10. virtual void Bark() = 0; //纯虚函数
  11. virtual ~Animal()
  12. {
  13. std:: cout << "Animal::~Animal()" << std:: endl;
  14. }
  15. protected:
  16. std:: string mname;
  17. };
  18. class Dog : public Animal
  19. {
  20. public:
  21. Dog( std:: string name) :Animal(name)
  22. {
  23. std:: cout << "Dog::Dog()" << std:: endl;
  24. }
  25. void Bark()
  26. {
  27. std:: cout << mname << " wang wang wang!" << std:: endl;
  28. }
  29. ~Dog()
  30. {
  31. std:: cout << "Dog::~Dog()" << std:: endl;
  32. }
  33. };
  34. class Cat : public Animal
  35. {
  36. public:
  37. Cat( std:: string name) :Animal(name)
  38. {
  39. std:: cout << "Cat::Cat()" << std:: endl;
  40. }
  41. void Bark()
  42. {
  43. std:: cout << mname << " miao miao miao!" << std:: endl;
  44. }
  45. ~Cat()
  46. {
  47. std:: cout << "Cat::~Cat()" << std:: endl;
  48. }
  49. };
  50. void ShowBark(Animal* pa)
  51. {
  52. pa->Bark();
  53. }
  54. int main()
  55. {
  56. Cat* pc = new Cat( "cat");
  57. Dog* pd = new Dog( "dog");
  58. ShowBark(pc);
  59. ShowBark(pd);
  60. delete pc;
  61. delete pd;
  62. return 0;
  63. }

 

我们运行一下,看一看结果:

我们可以看到,通过一个函数Bark写成纯虚函数virtual void Bark() = 0;,这时派生类对象就可以自行定义这个函数Bark,而我们提供的普通函数ShowBark,通过传入不同的基类指针,调用其纯虚函数Bark,则可以发出不同的叫声。

 

 

四:虚继承

继承可以分为单继承和多继承,那么就会出现这样一种巧妙地结果,菱形继承:

菱形继承:我们很清楚的可以看到,它存在内存重复的问题,所以我们引进了虚继承。

我们可以对内存重复的间接基类做特殊处理,在B和C继承时,加上关键字virtual,这时就形成了虚继承(class B : virtual public A)

 

那么A就叫做虚基类,在内存的最下方开辟一块内存,用来存放A,在其原本位置置放一个虚基类指针vbptr,通过这个指针可以找到这块内存,因为内存在开辟期间不能赋值指向,所以只能通过偏移来找到。

 

如果不加virtual关键字,那么构造顺序则是:ABACD

但是给BC加上virtual,那么构造顺序则是:ABCD

重点:构造时,虚基类的构造顺序最高

重点:而内存分布的时候,是非虚基类顺序>虚基类顺序,但是需要注意的是,虚基类内存向下放的时候,是按照虚继承的顺序,先看见谁,先放谁

 

完整代码如下(下面会根据情况进行简单修改,并进行测试,以验证以上结论):


   
   
  1. #include <iostream>
  2. class A
  3. {
  4. public:
  5. A( int a) :ma(a)
  6. {
  7. std:: cout << "A" << std:: endl;
  8. }
  9. ~A()
  10. {
  11. std:: cout << "~A" << std:: endl;
  12. }
  13. public:
  14. int ma;
  15. };
  16. class B : virtual public A
  17. {
  18. public:
  19. B( int b) :mb(b),A(b)
  20. {
  21. std:: cout << "B" << std:: endl;
  22. }
  23. ~B()
  24. {
  25. std:: cout << "~B" << std:: endl;
  26. }
  27. public:
  28. int mb;
  29. };
  30. class C : virtual public A
  31. {
  32. public:
  33. C( int c) :mc(c),A(c)
  34. {
  35. std:: cout << "C" << std:: endl;
  36. }
  37. ~C()
  38. {
  39. std:: cout << "~C" << std:: endl;
  40. }
  41. public:
  42. int mc;
  43. };
  44. class E
  45. {
  46. public:
  47. E( int e) :me(e)
  48. {
  49. std:: cout << "E" << std:: endl;
  50. }
  51. ~E()
  52. {
  53. std:: cout << "~E" << std:: endl;
  54. }
  55. public:
  56. int me;
  57. };
  58. class D : public B, virtual public E, public C
  59. {
  60. public:
  61. D( int d) :md(d), B(d), C(d), E(d), A(d)
  62. {
  63. std:: cout << "D" << std:: endl;
  64. }
  65. ~D()
  66. {
  67. std:: cout << "~D" << std:: endl;
  68. }
  69. public:
  70. int md;
  71. };
  72. int main()
  73. {
  74. D d(10);
  75. //d.ma = 10;
  76. return 0;
  77. }

 

我们修改类D的继承方式,以验证以上结论:

①:

class B : virtual public A

class C : virtual public A

class D : public B ,virtual public E, public C

运行结果:

内存分布:

可以看到,构造顺序是虚基类的最高。

 

②:

class B : virtual public A

class C : virtual public A

class D :virtual public E, virtual public B, public C

运行结果:

内存分布:

我们可以看到,内存中首先是非虚基类的C,但是按照虚继承顺序,接下来是E,最后才是A以及B。

 

查看内存命令:在开发人员命令提示中输入 cl -d1reportSingleClassLayoutD 测试1.cpp   (D为类名)

 

我们可以得到rfptr与rbptr的区别:

  • rfptr的偏移是总体作用域减当前
  • rbptr的偏移是当前作用域减当前

 

建议:所以说,有虚继承的话,一般不使用动态开辟内存,一般使用栈开辟内存,因为虚继承总会将基类放到最下面,导致内存开辟的地址和释放的地址不一致,导致崩溃。

 

至此,C++继承、多态、虚函数、纯虚函数、虚继承基本了解完毕。

本文转载如下:
————————————————
版权声明:本文为优快云博主「WuDi_Quan」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/IT_Quanwudi/article/details/88081934

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值