type
date
status
slug
summary
tags
category
icon
password
网址
纯虚函数的概念:
纯虚函数(pure virtual function)是指没有具体实现的虚成员函数。它用于这样的情况:设计一个类型时,会遇到无法定义类型中虚函数的具体实现,其实现依赖于不同的派生类。 定义纯虚函数的一般格式为: virtual 返回类型 函数名(参数表)= 0; “=0”表明程序员将不定义该虚函数实现,没有函数体,只有函数的声明; 函数的声明是为了在虚函数表中保留一个位置。“=0”本质上是将指向函数体的指针定义为nullptr。
抽象类的概念:
含有纯虚函数的类是抽象类。
抽象类是一种特殊的类,它是为抽象的目而建立的,它处于继承层次结构的较上层。 抽象类不能实例化对象, 因为纯虚函数没有实现部分,所以含有纯虚函数类型不能实例化对象;
抽象类的主要作用:
将相关的类型组织在一个继承层次结构中,抽象类为派生类型提供一个公共的根,相关的派生类型 是从这个根派生而来。
示例:
示例:
抽象类的使用规则: (1)抽象类只能用作其他类的基类,不能创建抽象类的对象。 (2)抽象类不能用作参数类型、函数返回类型或显式类型转换。 (3)可以定义抽象类的指针和引用,此指针可以指向(引用可以引用)它的派生类的对象,从而实现运行时多态。
注意:
抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类没有重新定义纯虚函 数,而派生类只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类型。
实例:
接口继承和实现继承
公有继承的概念看起来很简单,进一步分析,会发现它由两个可分的部分组成:函数接口的继承和 函数实现的继承。 为类的设计者: 有时希望派生类只继承成员函数的接口(声明),纯虚函数; 有时希望派生类同时继承函数的接口和实现,但允许派生类改写实现,虚函数。 有时则希望同时继承接口和实现,并且不允许派生类改写任何东西,非虚函数。 为了更好地体会这些选择间的区别,看下面这个类层次结构,它用来表示一个图形程序中的几何形状:
纯虚函数area使得Shape成为一个抽象类。所以,用户不能创建Shape类的实例,只能创建它的派生类的实例。但是,从Shape(公有)继承而来的所有类都受到Shape的巨大影响,因为: 成员函数的接口总会被继承。公有继承的含义是 "是一个" ,所以对基类成立的所有事实也必须对派生类成立。
Shape类中声明了三个函数。
第一个函数,area,计算当前对象的面积。 第二个函数,error,被其它成员函数调用,用于报告出错信息。 第三个函数,objectID,返回当前对象的一个唯一整数标识符。
每个函数以不同的方式声明:
area是一个纯虚函数;error是一个虚函数;objectID是一个非虚函数。这些不同的声明各有什么含义呢?
首先看纯虚函数area。纯虚函数最显著的特征是:它们必须在继承了它们的任何具体类中重新声 明,而且它们在抽象类中往往没有定义。把这两个特征放在一起,就会认识到:定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。 这对Shape::area函数来说非常有意义,因为,让所有Shape对象都可以被计算面积是很合理,但Shape类无法为Shape::area提供一个合理的缺省实现。例如,计算圆面积的算法就和计算正方形面积的算法大不一样。打个比方来说,上面Shape::area的声明就象是在告诉子类的设计者,"你必须提供一个area函数,但我不知道你会怎样实现它。" 有时,声明一个除纯虚函数外什么也不包含的类很有用。这样的类叫协议类(Protocol class)或接口,它为派生类仅提供函数接口,完全没有实现。
简单虚函数的情况和纯虚函数有点不一样。照例,派生类继承了函数的接口,但简单虚函数提供了 实现,派生类可以选择改写它们或不改写它们。思考片刻就可以认识到:声明简单虚函数的目的在于,使派生类继承函数的接口和缺省实现。 具体到Shape::error,这个接口是在说,每个类必须提供一个出错时可以被调用的函数,但每个类可以按它们认为合适的任何方式处理错误。如果某个类不想做什么特别的事,可以借助于Shape类中提供的缺省出错处理函数。也就是说,Shape::error的声明是在告诉子类的设计者,"你必须支持error函数,但如果你不想写自己的版本,可以借助Shape类中的缺省版本。" 实际上,为简单虚函数同时提供函数声明和缺省实现是很危险的。
Shape::objectID ,是非虚函数。当一个成员函数为非虚函数时,它在派生类中的行为就不应该不同。实际上,非虚成员函数表明了一种特殊性上的不变性,因为它表示的是不会改变的行为 ---- 不管一个派生类有多特殊。所以,声明非虚函数的目的在于,使派生类继承函数的接口和强制性实现
可以认为,Shape::objectID的声明就是在说,"每个Shape对象有一个函数用来产生对象的标识符,并且对象标识符的产生方式总是一样的。这种方式由Shape::objectID的定义决定,派生类不能改变它。" 因为非虚函数表示一种特殊性上的不变性,所以它决不能在子类中重新定义非虚函数。(同名覆盖) 理解了纯虚函数、简单虚函数和非虚函数在声明上的区别,就可以精确地指定你想让派生类继承什 么:仅仅是接口,还是接口和一个缺省实现?或者,接口和一个强制实现?因为这些不同类型的声明指的是根本不同的事,所以在声明成员函数时一定要从中慎重选择。只有这样做,才可以避免没经验的程序员常犯的两个错误。
第一个错误是把所有的函数都声明为非虚函数。这就使得派生类没有特殊化的余地; 当然,设计出来的类不准备作为基类使用也是完全合理的类型设计。 另一个常见的问题是将所有的函数都声明为虚函数。有时这没错 ---- 比如,协议类(Protocol class)就是证据。 但是,这样做往往表现了类的设计者缺乏表明坚定立场的勇气。一些函数不能在派生类中重定义, 只要是这种情况,就要旗帜鲜明地将它声明为非虚函数。
多继承(MI)与虚函数多继承(MI)要么被认为是神来之笔,要么被当成是魔鬼的造物。支持者宣扬说,它是对真实世界 问题进行自然模型化所必需的;而批评者争论说,它太慢,难以实现,功能却不比单继承强大。更让人为难的是,面向对象编程语言领域在这个问题上至今仍存在分歧:C++,Eiffel和the Common LISP Object System (CLOS)提供了MI;Smalltalk,Objective C和Object Pascal没有提供;而Java只是提供有限的支持。可怜的程序员该相信谁呢?
在相信任何事情之前,首先得弄清事实。C++中,关于MI一条不容争辩的事实是,MI的出现就象打开了潘朵拉的盒子,带来了单继承中绝对不会存在的复杂性。其中,最基本的一条是二义性。如果一个派生类从多个基类继承了一个成员名,所有对这个名字的访问都是二义的;你必须明确地说出你所指的是哪个成员。
当类Derived继承两个具有相同名字的函数时,C++没有认为它有错,此时二义只是潜在的。然 而,对doIt的调用迫使编译器面对这个现实,除非显式地通过指明函数所需要的基类来消除二义,函数调用就会出错:
这不会令很多人感到麻烦,但当看到上面的代码没有用到访问权限时,一些本来很安分的人会动起 心眼想做些不安分的事:
对doIt的调用还是具有二义性,即使只有Base1中的函数可以被访问。另外,只有Base1::doIt返回的值可以用于初始化一个int这一事实也与之无关——调用还是具有二义性。如果想成功地调用,就必须指明想要的是哪个类的doIt。 C++中有一些最初看起来会觉得很不直观的规定,现在就是这种情况。具体来说,为什么消除“对类成员的引用所产生的二义”时不考虑访问权限呢?有一个非常好的理由,它可以归结为:改变一个类成员的访问权限不应该改变程序的含义。 比如前面那个例子,假设它考虑了访问权限。于是表达式d.doIt()决定调用Base1::doIt,因为Base2的版本不能访问。现在假设Base1的doIt版本由public改为protected,Base2的版本则由private改为public。 转瞬之间,同样的表达式d.doIt()将导致另一个完全不同的函数调用,即使调用代码和被调用函数本身都没有被修改!这很不直观,编译器甚至无法产生一个警告。可见,不是象你当初所想的那样,对多继承的成员的引用要显式地消除二义性是有道理的。 既然写程序和函数库时有这么多不同的情况会产生潜在的二义性,那么,一个好的软件开发者该怎 么做呢?最根本的是,一定要时时小心它。想找出所有潜在的二义性的根源几乎是不可能的,特别是当程序员将不同的独立开发的库结合起来使用时,但在了解了导致经常产生潜在二义性的那些情况后,你就可以在软件设计和开发中将它出现的可能性降到最低。