type
date
status
slug
summary
tags
category
icon
password
网址

多态:

多态性是面向对象程序设计的关键技术之一。若程序设计语言不支持多态性,不能称为面向对象的语言。 多态性(polymorphism)多态性是考虑在不同层次的类中,以及在同一类中,同名的成员函数之间的关系问题。 函数的重载,运算符的重载,属于编译时的多态性。 以类的虚成员函数为基础的运行时的多态性,是面向对象程序设计的标志性特征。 体现了类推和比喻的思想方法。

编译时的多态和运行时的多态

notion image

编译时多态,早期绑定。

机器代码,编译链接时的函数名称:
notion image

运行时的多态

运行时多态的设计思想:

对于相关的类型,确定它们之间的一些共同特征,(属性和方法),将共同特征被转移到基类中,然后在基类中,把这些共同的函数或方法声明为公有的虚函数接口。然后使用派生类继承基类,并且在派生类中重写这些虚函数,以完成具体的功能。这种设计使得共性很清楚,避免了代码重复,将来容易 增强功能,并易于长期维护。 客户端的代码(操作函数)通过基类的引用或指针来指向这些派生类型对象,对虚函数的调用会自动绑定到派生类对象上重写的虚函数。 虚函数的定义: 虚函数是一个类的成员函数,定义格式如下: virtual 返回类型 函数名(参数表); 关键字virtual指明该成员函数为虚函数。只能将类的成员函数定义为虚函数。当某一个类的成员函数被定义为虚函数,则由该类派生出来的所有派生类中,该函数始终保持虚函数的特征。 示例:运行时的多态性,晚绑定。
总结:运行时的多态性: 公有继承 + 虚函数 + (指针或引用调用虚函数)。

定义虚函数的规则

类的成员函数定义为虚函数,但必须注意以下几条:
  1. 派生类中定义虚函数必须与基类中的虚函数同名外,还必须同参数表,同返回类型。否则被认为是同名覆盖,不具有多态性。如基类中返回基类指针,派生类中返回派生类指针是允许的,这是一个例外(协变)。
  1. 只有类的成员函数才能说明为虚函数。这是因为虚函数仅适用于有继承关系的类对象。友元函数和全局函数也不能作为虚函数。
  1. 静态成员函数,是所有同一类对象共有,不受限于某个对象,不能作为虚函数。
  1. 内联函数每个对象一个拷贝,无映射关系,不能作为虚函数。
  1. 构造函数和拷贝构造函数不能作为虚函数。构造函数和拷贝构造函数是设置虚表指针。
  1. 析构函数可定义为虚函数,构造函数不能定义虚函数,因为在调用构造函数时对象还没有完成实例化(虚表指针没有设置)。在基类中及其派生类中都动态分配的内存空间时,必须把析构函数定义为虚函数,实现撤消对象时的多态性。
  1. 实现运行时的多态性,必须使用基类类型的指针变量或引用,使该指针指向该基类的不同派生类的对象,并通过该指针指向虚函数,才能实现运行时的多态性。
  1. 在运行时的多态,函数执行速度要稍慢一些:为了实现多态性,每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现。所以多态性总是要付出一定代价, 但通用性是一个更高的目标。
  1. 如果定义放在类外,virtual只能加在函数声明前面,不能(再)加在函数定义前面。正确的定义必须不包括virtual。

多态的原理

虚函数表的示例:运行时多态的原理 虚函数指针表简称虚表, 虚表就是虚函数指针的集合,虚函数指针表本质是一个存储虚函数指针的指针数组,这个数组的首元素之上存储RTTI( 运行时类型识别信息的指针),从数组下标0开始依次存储虚函数地址, 最后面放了一个nullptr。 类型设计中定义了虚函数,此类型就有了对应的虚表(vftable),v代表virtual, f代表function,table代表表,数组; 使用此类型定义的对象就含有一个指向虚表的指针,名字是 __vfptr,v代表virtual, f代表 function, ptr 代表指针。
dynamic_cast
虚函数指针表存储在只读数据段(.rodata) //虚函数指针表简称虚表, 虚表就是虚函数指针的集合, //虚表本质是一个存储虚函数指针的指针数组, //这个数组的首元素之上存储RTTI(运行时类型识别信息的指针), //从数组下标0开始依次存储虚函数地址, 最后面放了一个nullptr。 //虚表存储在只读数据段(.rodata) // //类型设计中定义了虚函数,此类型就有了对应的虚表(vftable), //v代表virtual, f代表function, table代表表,数组; //使用此类型定义的对象就含有一个指向虚表的指针,名字是 vfptr, //v代表virtual, f代表function, ptr 代表指针。 //vfptr 存储在对象中。 //动态联编
虚表
notion image
obja 对象和虚表
notion image
base 对象和虚表
notion image
test 对象和虚表
notion image
虚函数的调用:
notion image

静态联编和动态联编

联编是指计算机程序彼此关联的过程,是把一个标识符名和一个存储地址联系在一起的过程,也就是把函数的调用和函数的入口地址相结合的过程 。
静态联编(static binding)早期绑定: 静态联编是指在编译和链接阶段,就将函数实现和函数调用关联起来。 C语言中,所有的联编都是静态联编,并且任何一种编译器都支持静态联编。 C++语言中,函数重载和函数模板也是静态联编。 C++语言中,使用对象名加点“.”成员选择运算符,去调用对象虚函数,则被调用的虚函数是在编译和链接时确定。(称为静态联编)。 动态联编(dynamic binding)亦称滞后联编(late binding)或晚期绑定: 动态联编是指在程序执行的时候才将函数实现和函数调用关联起来。 C++语言中,使用类类型的引用或指针调用虚函数(成员选择符“->”),则程序在运行时选择虚函数的过程,称为动态联编。

虚析构函数

C++ 中构造函数不能定义为虚函数: 关于C++为什么不支持虚构造函数,虚函数调用只需要“部分的”信息,即只需要知道函数接口,而不需要对象的具体类型。 但是构建一个对象,却必须知道具体的类型信息。如果你调用一个虚构造函数,编译器怎么知道你想构 建是继承树上的哪种类型呢?所以构造函数不能为虚。 为什么构造函数不可以是虚函数呢?
  1. 构造函数的用途:
    1. 1)创建对象
      2)初始化对象中的属性
      3)类型转换 。
  1. 在类中定义了虚函数就会有一个虚函数表(vftable), 对象模型中就含有一个指向虚表的指针(__vfptr)。在定义对象时构造函数设置虚表指针指向虚函数表。
  1. 使用指针和引用调用虚函数,在编译只需要知道函数接口,运行时指向具体对象,才能关联具体对象的虚方法(通过虚函数指针查虚函数表得到具体对象中的虚方法)
  1. 构造函数是类的一个特殊的成员函数: 1)定义对象由系统自动调用构造函数,对象自己是不可以调用构造函数; 2)构造函数的调用属于静态联编,在编译时必须知道具体的类型信息。
  1. 如果构造函数可以定义为虚构造函数, 使用指针调用虚构造函数, 如果编译器采用静态联编,构 造函数就不能为虚函数。如果采用动态联编,运行时指针指向具体对象,使用指针调用构造函数, 相当于已经实例化的对象在调用构造函数,这是不容许的调用, 对象的构造函数只执行一次 。
  1. 如果指针可以调用虚构造函数, 通过查虚函数表,调动虚构造函数,那么,当指针为nullptr, 如何 查虚函数表呢?
  1. 构造函数的调用是在编译时确定,如果是虚构造函数,编译器怎么知道你想构建是继承树上的哪种 类型呢? 总结:构造函数不允许是虚函数。 Bjarne建议的解决方案是factory pattern,也就是为每一个要构建的类型再创建一个对应的 factory,把问题放到factory的make方法中去解决。这也是C++中的通用解决方案。 虚析构函数: 析构函数是类的一个特殊的成员函数: 1)当一个对象的生命周期结束时,系统会自动调用析构函数注销该对象并进行善后工作,对象自身 也可以调用析构函数; 2)析构函数的善后工作是:释放对象在生命期内获得的资源(如动态分配的内存,内核资源); 3)析构函数也用来执行对象即将被撤销之前的任何操作。 根据赋值兼容规则,可以用基类的指针指向派生类对象,如果使用基类型指针指向动态创建的派生 类对象,由该基类指针撤销派生类对象,则必须将析构函数定义为虚函数,实现多态性,自动调用派生 类析构函数,否则可能存在内存泄漏问题。 总结: 在实现运行时的多态,无论其他程序员怎样调用析构函数都必须保证不出错,所以必须把析构函 数定义为虚函数。 注意:类中没有虚函数,就不要把析构函数定义为虚。 在动态分配内存时所有C++的标准库函数都采用这种格式。

纯虚函数和抽象类

纯虚函数的概念: 纯虚函数(pure virtual function)是指没有具体实现的虚成员函数。它用于这样的情况:设计一个类型时,会遇到无法定义类型中虚函数的具体实现,其实现依赖于不同的派生类。 定义纯虚函数的一般格式为: virtual 返回类型 函数名(参数表)= 0; “=0”表明程序员将不定义该虚函数实现,没有函数体,只有函数的声明; 函数的声明是为了在虚函数表中保留一个位置。“=0”本质上是将指向函数体的指针定义为nullptr。 抽象类的概念: 含有纯虚函数的类是抽象类。 抽象类是一种特殊的类,它是为抽象的目而建立的,它处于继承层次结构的较上层。 抽象类不能实例化对象, 因为纯虚函数没有实现部分,所以含有纯虚函数类型不能实例化对象; 抽象类的主要作用: 将相关的类型组织在一个继承层次结构中,抽象类为派生类型提供一个公共的根,相关的派生类型是从这个根派生而来。
抽象类的使用规则: (1)抽象类只能用作其他类的基类,不能创建抽象类的对象。 (2)抽象类不能用作参数类型、函数返回类型或显式类型转换。 (3)可以定义抽象类的指针和引用,此指针可以指向(引用可以引用)它的派生类的对象,从而实现 运行时多态。 注意: 抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类没有重新定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类型。

接口继承和实现继承

公有继承的概念看起来很简单,进一步分析,会发现它由两个可分的部分组成:函数接口的继承和 函数实现的继承。 为类的设计者: 有时希望派生类只继承成员函数的接口(声明),纯虚函数; 有时希望派生类同时继承函数的接口和实现,但允许派生类改写实现,虚函数。 有时则希望同时继承接口和实现,并且不允许派生类改写任何东西,非虚函数。 为了更好地体会这些选择间的区别,看下面这个类层次结构,它用来表示一个图形程序中的几何形状: