C++:多态


文章目录

  • 一.多态的现实意义与基本语法
    • 多态的现实意义
    • 语法层面上的多态
    • 构成多态的语法条件:
    • 子类和父类中重名函数间的关系梳理:
    • 继承体系中析构函数的多态:
    • C++11中针对多态编程的语法保护
    • 一道关于多态的恶心面试题:
  • 二.多态的底层实现原理--虚函数表
    • 虚函数表与多态
    • 接口的多态调用与普通调用
  • 三.多继承体系下类对象的虚函数表--图解

一.多态的现实意义与基本语法

多态的现实意义

  • 现实意义上,多态指的是针对同一种行为,不同的对象去执行该行为时会有不同的表现不同的执行结果,比如:
    C++:多态_第1张图片

语法层面上的多态

  • 语法层面上,多态是继承体系的不同类对象,调用了同一函数,但却得到不同的执行过程和结果。

构成多态的语法条件:

  • 构成多态的第一个条件:
    继承体系的父类中定义了虚函数(由virtual关键字修饰的函数),并且该虚函数在其子类中完成了重写(父类虚函数的重写指的是,在其子类中实现了返回值,函数名,形参表完全相同的对应函数),比如:C++:多态_第2张图片
    • 虚函数的重写有两种特例:一是子类中的重写虚函数可以不加关键字virtual;二是父类虚函数返回值为父类对象的指针或者引用时,子类的重写虚函数返回值可以是其自身的指针或者引用(称为重写中的协变),比如:C++:多态_第3张图片
    • 这两种特殊的虚函数重写语法实际中用的很少,是C++语法设计上的冗余
  • 构成多态的第二个条件:
    子类对象通过继承体系的父类的指针或者引用来调用重写的虚函数,此时便实现了多态,比如:C++:多态_第4张图片
    • 实现多态时,父类的指针或者引用所调用的具体虚函数重写版本是由父类的指针或者引用所指向的子类所决定的

子类和父类中重名函数间的关系梳理:

C++:多态_第5张图片

继承体系中析构函数的多态:

  • 在继承体系中,子类和父类的析构函数名会统一被编译器改成destructor()
  • 如果父类中析构函数没有被设计成虚函数(并在子类中完成重写),可能会出现以下情形:C++:多态_第6张图片
  • 由于poly1和poly2都是父类指针,因此delete执行时都会去调用父类的析构函数,这会导致子类对象的内存空间得不到完全清理,从而可能造成内存泄漏,造成隐患.因此需要将父类的析构函数设计成虚函数,并且在子类中完成其重写,如下:C++:多态_第7张图片
  • 因此,在继承体系中,管理了动态内存的类的析构函数应统一设计成虚函数以防止内存泄漏的情形出现

C++11中针对多态编程的语法保护

  1. 在子类中的重写虚函数首部后加上override关键字,编译器就会自动检查虚函数是否重写成功,若没有重写成功则报错
  2. 纯虚函数:
    为了规范多态编程,C++11引入了纯虚函数(在父类的虚函数后加上"=0"),比如:C++:多态_第8张图片
    • 含有纯虚函数的父类称为抽象类,抽象类不能实例化出对象.继承了抽象类的子类必须重写虚函数才能实例化出子类对象.抽象类使多态编程得到了良好地规范化

一道关于多态的恶心面试题:

  • 判断下面程序的输出结果:
 class A
 {
 public:
   virtual void func(int val = 1)
   { 
   		std::cout<<"A->"<< val <<std::endl;
   }
   virtual void test()
   { 
   		func();
   }
 };
 
 class B : public A
 {
 public:
   void func(int val=0)
   { 
   		std::cout<<"B->"<< val <<std::endl; 
   }
 };
 int main(int argc ,char* argv[])
 {
   B*p = new B;
   p->test();
   return 0;
 }
  • 分析:C++:多态_第9张图片
  • 普通函数的继承是一种实现继承,子类继承了父类的整个函数,可以使用该函数.
  • 虚函数的继承在语法上是一种接口继承,子类继承的是父类虚函数的函数接口(首部)

二.多态的底层实现原理–虚函数表

虚函数表与多态

  • 虚函数表:

    • 虚函数表实质上是函数指针数组(C语言中的转移表),如果一个类中定义了虚函数,那么这个类对象的内存模型中就会存在虚表指针–指向函数指针的指针,该虚表指针会指向一个存放了虚函数地址函数指针数组的首地址:C++:多态_第10张图片
  • 继承体系中的虚函数表:

    • 在继承体系中,父类定义了虚函数,并且虚函数在子类中完成了重写,那么子类对象内存模型中的虚表指针会存在于子类对象的父类内存区块中,这也解释了为什么多态的实现条件必须是通过父类指针或者引用去调用重写的虚函数:C++:多态_第11张图片
    • 此时子类对象所对应的虚表中存放的函数指针指向的是子类的重写虚函数(而不是指向父类的虚函数)
    • 父类的指针或者引用可以通过子类对象的虚表指针找到子类对象所对应的虚表,并在虚表中找到子类的重写虚函数的地址,从而实现多态调用:C++:多态_第12张图片
    • Tips:为了节省内存空间,同类型的类对象会共用虚表
    • 在继承体系中,无论父类虚函数是否在子类中完成了重写,父类和子类分别拥有自己的虚表
    • 只要类中定义了虚函数,程序运行时内存中就会生成相应的类的虚表,因此如果不实现多态就不要定义虚函数,避免内存浪费

接口的多态调用与普通调用

  • 普通函数调用:
    • 普通函数调用是一种编译时决策,也就是说,特定的函数调用语句在程序编译时就已经确定了要调用哪一个具体的函数
  • 多态函数调用:
    • 多态函数调用是一种运行时决策,特定的函数调用语句具体会调用哪一个虚函数重写版本取决于运行时父类的引用或指针子类的虚函数表中的寻址定位结果

多态是面向对象编程的三大特性之一,接口的多态调用指令相比于普通函数调用指令更具有灵活性和可扩展性,这一点为面向对象编程提供了更丰富的可能性。

三.多继承体系下类对象的虚函数表–图解

C++:多态_第13张图片

C++:多态_第14张图片

你可能感兴趣的:(c++,开发语言)