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

编译时多态,早期绑定。
机器代码,编译链接时的函数名称:

运行时的多态
运行时多态的设计思想:
对于相关的类型,确定它们之间的一些共同特征,(属性和方法),将共同特征被转移到基类中,然后在基类中,把这些共同的函数或方法声明为公有的虚函数接口。然后使用派生类继承基类,并且在派生类中重写这些虚函数,以完成具体的功能。这种设计使得共性很清楚,避免了代码重复,将来容易
增强功能,并易于长期维护。
客户端的代码(操作函数)通过基类的引用或指针来指向这些派生类型对象,对虚函数的调用会自动绑定到派生类对象上重写的虚函数。
虚函数的定义:
虚函数是一个类的成员函数,定义格式如下:
virtual 返回类型 函数名(参数表);
关键字virtual指明该成员函数为虚函数。只能将类的成员函数定义为虚函数。当某一个类的成员函数被定义为虚函数,则由该类派生出来的所有派生类中,该函数始终保持虚函数的特征。
示例:运行时的多态性,晚绑定。
总结:运行时的多态性: 公有继承 + 虚函数 + (指针或引用调用虚函数)。
定义虚函数的规则
类的成员函数定义为虚函数,但必须注意以下几条:
- 派生类中定义虚函数必须与基类中的虚函数同名外,还必须同参数表,同返回类型。否则被认为是同名覆盖,不具有多态性。如基类中返回基类指针,派生类中返回派生类指针是允许的,这是一个例外(协变)。
- 只有类的成员函数才能说明为虚函数。这是因为虚函数仅适用于有继承关系的类对象。友元函数和全局函数也不能作为虚函数。
- 静态成员函数,是所有同一类对象共有,不受限于某个对象,不能作为虚函数。
- 内联函数每个对象一个拷贝,无映射关系,不能作为虚函数。
- 构造函数和拷贝构造函数不能作为虚函数。构造函数和拷贝构造函数是设置虚表指针。
- 析构函数可定义为虚函数,构造函数不能定义虚函数,因为在调用构造函数时对象还没有完成实例化(虚表指针没有设置)。在基类中及其派生类中都动态分配的内存空间时,必须把析构函数定义为虚函数,实现撤消对象时的多态性。
- 实现运行时的多态性,必须使用基类类型的指针变量或引用,使该指针指向该基类的不同派生类的对象,并通过该指针指向虚函数,才能实现运行时的多态性。
- 在运行时的多态,函数执行速度要稍慢一些:为了实现多态性,每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现。所以多态性总是要付出一定代价, 但通用性是一个更高的目标。
- 如果定义放在类外,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 存储在对象中。
//动态联编
虚表

obja 对象和虚表

base 对象和虚表

test 对象和虚表

虚函数的调用:

静态联编和动态联编
联编是指计算机程序彼此关联的过程,是把一个标识符名和一个存储地址联系在一起的过程,也就是把函数的调用和函数的入口地址相结合的过程 。
静态联编(static binding)早期绑定: 静态联编是指在编译和链接阶段,就将函数实现和函数调用关联起来。
C语言中,所有的联编都是静态联编,并且任何一种编译器都支持静态联编。
C++语言中,函数重载和函数模板也是静态联编。
C++语言中,使用对象名加点“.”成员选择运算符,去调用对象虚函数,则被调用的虚函数是在编译和链接时确定。(称为静态联编)。
动态联编(dynamic binding)亦称滞后联编(late binding)或晚期绑定: 动态联编是指在程序执行的时候才将函数实现和函数调用关联起来。
C++语言中,使用类类型的引用或指针调用虚函数(成员选择符“->”),则程序在运行时选择虚函数的过程,称为动态联编。
虚析构函数
C++ 中构造函数不能定义为虚函数:
关于C++为什么不支持虚构造函数,虚函数调用只需要“部分的”信息,即只需要知道函数接口,而不需要对象的具体类型。
但是构建一个对象,却必须知道具体的类型信息。如果你调用一个虚构造函数,编译器怎么知道你想构
建是继承树上的哪种类型呢?所以构造函数不能为虚。
为什么构造函数不可以是虚函数呢?
- 构造函数的用途:
1)创建对象
2)初始化对象中的属性
3)类型转换 。
- 在类中定义了虚函数就会有一个虚函数表(vftable), 对象模型中就含有一个指向虚表的指针(__vfptr)。在定义对象时构造函数设置虚表指针指向虚函数表。
- 使用指针和引用调用虚函数,在编译只需要知道函数接口,运行时指向具体对象,才能关联具体对象的虚方法(通过虚函数指针查虚函数表得到具体对象中的虚方法)
- 构造函数是类的一个特殊的成员函数: 1)定义对象由系统自动调用构造函数,对象自己是不可以调用构造函数; 2)构造函数的调用属于静态联编,在编译时必须知道具体的类型信息。
- 如果构造函数可以定义为虚构造函数, 使用指针调用虚构造函数, 如果编译器采用静态联编,构 造函数就不能为虚函数。如果采用动态联编,运行时指针指向具体对象,使用指针调用构造函数, 相当于已经实例化的对象在调用构造函数,这是不容许的调用, 对象的构造函数只执行一次 。
- 如果指针可以调用虚构造函数, 通过查虚函数表,调动虚构造函数,那么,当指针为nullptr, 如何 查虚函数表呢?
- 构造函数的调用是在编译时确定,如果是虚构造函数,编译器怎么知道你想构建是继承树上的哪种 类型呢? 总结:构造函数不允许是虚函数。 Bjarne建议的解决方案是factory pattern,也就是为每一个要构建的类型再创建一个对应的 factory,把问题放到factory的make方法中去解决。这也是C++中的通用解决方案。 虚析构函数: 析构函数是类的一个特殊的成员函数: 1)当一个对象的生命周期结束时,系统会自动调用析构函数注销该对象并进行善后工作,对象自身 也可以调用析构函数; 2)析构函数的善后工作是:释放对象在生命期内获得的资源(如动态分配的内存,内核资源); 3)析构函数也用来执行对象即将被撤销之前的任何操作。 根据赋值兼容规则,可以用基类的指针指向派生类对象,如果使用基类型指针指向动态创建的派生 类对象,由该基类指针撤销派生类对象,则必须将析构函数定义为虚函数,实现多态性,自动调用派生 类析构函数,否则可能存在内存泄漏问题。 总结: 在实现运行时的多态,无论其他程序员怎样调用析构函数都必须保证不出错,所以必须把析构函 数定义为虚函数。 注意:类中没有虚函数,就不要把析构函数定义为虚。 在动态分配内存时所有C++的标准库函数都采用这种格式。
纯虚函数和抽象类
纯虚函数的概念:
纯虚函数(pure virtual function)是指没有具体实现的虚成员函数。它用于这样的情况:设计一个类型时,会遇到无法定义类型中虚函数的具体实现,其实现依赖于不同的派生类。
定义纯虚函数的一般格式为:
virtual 返回类型 函数名(参数表)= 0;
“=0”表明程序员将不定义该虚函数实现,没有函数体,只有函数的声明; 函数的声明是为了在虚函数表中保留一个位置。“=0”本质上是将指向函数体的指针定义为nullptr。
抽象类的概念:
含有纯虚函数的类是抽象类。
抽象类是一种特殊的类,它是为抽象的目而建立的,它处于继承层次结构的较上层。
抽象类不能实例化对象, 因为纯虚函数没有实现部分,所以含有纯虚函数类型不能实例化对象;
抽象类的主要作用:
将相关的类型组织在一个继承层次结构中,抽象类为派生类型提供一个公共的根,相关的派生类型是从这个根派生而来。
抽象类的使用规则:
(1)抽象类只能用作其他类的基类,不能创建抽象类的对象。
(2)抽象类不能用作参数类型、函数返回类型或显式类型转换。
(3)可以定义抽象类的指针和引用,此指针可以指向(引用可以引用)它的派生类的对象,从而实现
运行时多态。
注意:
抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类没有重新定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类型。
接口继承和实现继承
公有继承的概念看起来很简单,进一步分析,会发现它由两个可分的部分组成:函数接口的继承和
函数实现的继承。
为类的设计者:
有时希望派生类只继承成员函数的接口(声明),纯虚函数;
有时希望派生类同时继承函数的接口和实现,但允许派生类改写实现,虚函数。
有时则希望同时继承接口和实现,并且不允许派生类改写任何东西,非虚函数。
为了更好地体会这些选择间的区别,看下面这个类层次结构,它用来表示一个图形程序中的几何形状: