🥒继承
2022-6-10
| 2023-8-2
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password
Property

通过继承关联起来的类组成了层级。通常层级的顶端是一个基类(base class),其它类直接或间接地从基类继承而来,这些继承的类称之为派生类(derived classes)。基类定义层级中所类型都共通的成员,每个派生类定义各自特有的成员。
 
如果要设计一个图书管理系统,每个角色的权限是不同的,为了区分这些角色,就要设计一些类出来:
其存在大量冗余部分,有些信息是公共的,有些信息是每个角色独有的。对于有些数据和方法是每个角色都具有的,每次都写一遍,这就导致设计重复了。代码是要讲究复用的,要想办法去做一个 "提取" ,把共有的成员变量提取出来。
 
解决方案:设计一个Person类,使用 "继承" 去把这些大家公有的东西运送给各个角色:
在需要称为子类的类的类名后加上冒号,并跟上继承方式和父类类名即可,希望让Studentpublic的继承方式继承自Person
 
 
继承的定义格式:
Student子类,称之为派生类。Person是父类,称之为基类
 

基类

基类通常应该定义一个虚析构函数,即便该函数不执行任何实际操作也是如此:

重写(覆盖)

Quoteisbn() 函数在基类中定义,这是在整个层级都共通的成员,而派生类定义自己的net_price函数,因为每个类有其自己的不同策略,需要QuoteBulk_qute类定义自己的版本。当遇到如net_price这样与类型相关的操作时,派生类需要对这些操作提供自己的新定义以覆盖(override)掉其从基类继承来的旧定义。
重写是子类对父类的允许访问的方法的实现过程进行重新编写,返回值和形参都不能改变,"外壳不变,核心重写。"
 

virtual

基类必须明确区分希望派生类覆盖的函数希望派生类继承而不改变的函数
  • 基类将希望派生类覆盖的函数定义为virtual。通过指针或引用调用虚函数,这个调用将是动态绑定的。根据引用或指针绑定的不同对象类型,该调用可能执行基类的版本也可能执行某个派生类的版本。
    • 所有的非静态成员函数(除了构造函数)都可以是virtual的。virtual关键只出现在类体内的函数声明处,而不会被用于类体外的函数定义处。在基类中被定义为virtual的函数,其在派生类中隐式也是virtual的。
  • 没有被定义为virtual的成员函数将在编译时确定下来,而不是运行时。如:isbn() 函数只有一份定义,不论是以引用、指针还是对象值进行调用,都可以在编译时确定调用哪个函数,即Quote中的版本。
 

访问控制和继承

派生类继承基类中的所有成员,但这并不意味着派生类可以访问基类中的所有成员。与其它使用基类的代码一样,派生类可以使用基类的public成员,但是不能访问基类的private成员。基类将只允许派生类访问,而不允许其它用户代码访问的成员定义为protected
 
 

派生类

派生类必须通过使用类派生列表明确指出它是从哪个(哪些)基类继承而来的。类派生列表的形式是:首先是一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以有以下三种访问说明符中的一个:publicprotected或者private
派生类必须在其内部对所有重新定义的虚函数进行声明。派生类可以在这样的函数之前加上virtual关键字,但并不是必要的C++11允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,具体措施是在该函数的形参列表之后增加override关键字
继承列表中的访问说明符将决定派生类的用户代码是否可以知道派生类从哪个基类继承而来。当继承是public的,基类的public成员变成了派生类的接口的一部分。并且,可以将公共派生的类型对象绑定到基类的指针或引用。
绝大多数类只会直接从一个基类继承,这种形式的继承称之为单继承(single inheritance)
 

派生类中的虚函数

派生类经常但不总是覆盖其继承的虚函数。如果派生类不覆盖其基类的某个虚函数,那么与别的成员一样,派生类将继承基类中定义的版本。
派生类将在其覆盖的函数上包含virtual关键字,但是不是必须这么做。C++11允许派生类显式告知它将覆盖一个继承自基类的虚函数。具体做法是在形参列表后面、或者在const成员函数的const关键字后面、或者在引用成员函数的引用限定符后面添加一个关键字override
 

派生类对象和派生类到基类的转换

一个派生类对象包含多个部分:包含派生类自己定义的成员的子对象,加上每一个基类的子对象(如果有多个基类,那么这样的子对象也有多个)。Bulk_quote 类的对象包含两个部分:自己定义的成员组成的子对象,与基类 Quote 子对象。由于派生类对象包含每个基类对应的子对象,可以把派生类对象当作基类对象一样使用,特别是,可以将基类对象的引用或指针绑定到派生对象的基类部分。
 
C++ 标准没有明确规定派生类的对象在内存中如何分布,但是可以认为Bulk_quote的对象包含如下的两部分:
notion image
这种转换称为派生类到基类的转换(derived-to-base conversion),这种转换是由编译器隐式执行的。由于这种转换是隐式的,可以将派生类对象或派生对象的引用用于需要基类对象引用的地方。同样的,可以将派生类对象的指针用于需要基类指针的地方。
派生类对象包含其基类的子对象是理解继承如何工作的关键。
 
 

派生类构造函数

尽管派生对象包含从基类继承来的成员,但派生类并不能直接初始化这些成员。与其他创建基类对象的代码一样,派生类必须使用基类构造函数来初始化基类部分。每个类的构造函数控制其成员如何进行初始化。
派生类对象的基类部分与派生类对象自己的数据成员都是一起在构造函数的初始化阶段进行初始化。与初始化成员一样,派生类构造函数同样是通过构造函数初始化列表来将实参传递给基类构造函数的。如接受四个参数的Bulk_quote构造函数:
该函数将它的前两个参数(ISBN和价格)传递给Quote的构造函数,由Quote的构造函数负责初始化Bulk_quote的基类部( bookNoprice)。当 Quote构造函数体(空的)结束后,对象的基类部分也就完成初始化了。接下来初始化由派生类直接定义的min_qty成员和discount成员。最后运行Bulk_quote构造函数的函数体。
除非特别指出,否则派生类对象的基类部分会像数据成员一样执行默认初始化。如果想使用其他的基类构造函数,需要以类名加圆括号内的实参列表的形式为构造函数提供初始值。这些实参将帮助编译器决定到底应该选用哪个构造函数来初始化派生类对象的基类部分。
 
如何设计一个不能被继承的类?
将父类的构造函数私有化:
父类 A 的构造函数私有化后 B 就无法构造对象,因为 B 的构造函数必须要调用 A 的。但是好像A也没办法构造了,但是可以这样
 

在派生类中使用基类成员

派生类可以访问基类的publicprotected成员。派生类的作用域被嵌套在基类的作用域中,那么在派生类成员函数中使用派生类自己定义的成员和使用基类中定义的成员没有区别。
 
遵循基类的接口
理解每个类定义自己的接口是很重要的,与类对象进行交互时应该使用那个类的接口,即便那个对象是派生对象的基类部分。因而,派生类构造函数不会直接初始化基类的成员。派生构造函数的函数体可以给publicprotected基类成员进行赋值。尽管它可以这样做,通常不应该这样做。与任何别的使用基类的用户代码一样,派生类应该尊重其基类的接口,所以应该使用基类的构造函数来初始化其继承的成员。
 
 

继承和静态成员

如果在基类中定义了静态成员,那么整个继承体系中只存在此成员的唯一定义。不管从一个基类中派生了多少个派生类,静态成员只存在一份实例,为基类和派生类的所有对象所共享。
 
静态成员遵循常规的访问控制:如果一个成员在基类中是private的,那么派生类将无法访问它。如果某静态成员是可访问的,则可以通过基类或派生类中使用此静态成员。
 

声明派生类

派生类的声明与常规类的声明是一样的。声明中包含类的名字,但不包含派生列表。
声明的目的在于让程序知晓某个名字的存在以及它表示什么类型的实体,如:类、函数或变量。派生列表和所有其他的定义细节必须一起出现在类体中。
 
一个类在被用作基类之前必须定义而不能仅仅只声明。原因在于,每个派生类都包含并且可能使用其从基类继承来的成员。为了使用这些成员,派生类必须知道它们具体是什么。这也隐式说明一个类不能派生它本身。
一个类是基类,同时它是一个派生类:
直接基类被放在派生列表中,间接基类是派生类通过其直接基类继承来的。
每个类都继承其直接基类的所有成员。最具体的派生类将继承其直接基类的所有成员,直接基类中的成员包含它自己从它的基类中继承来的成员,以此类推到整个继承链的顶端。所以,最具体的派生对象将包含其直接基类的子对象以及每个间接基类的子对象。
 

阻止继承final

有时不希望一个类被继承,在新标准中可以通过在类名后加上final来阻止类被当作基类。
 

动态绑定

通过动态绑定,可以使用相同的代码来平滑处理基类和派生类对象,在这里是QuoteBulk_quote
由于itemQuote的引用,调用函数时既可以传递Quote对象也可以传递Bulk_quote对象,并且由于net_price是虚函数,由于调用 net_price 函数是通过引用,具体调用哪个版本的函数,将依据传入对象的类型决定。如果传入Bulk_quote的对象则调用Bulk_quote 的版本,如果传入Quote类的对象,则调用Quote的版本。
由于调用哪个版本是由实参的类型决定的,而实参类型只有在调用时才能知道。因而,动态绑定有时也被称为运行时绑定(run-time binding)。
C++中,动态绑定发生在虚函数通过基类的引用或指针调用时。
 
 
 
 

类型转换和继承

通常,只能将引用和指针绑定到有相同类型的对象上,或者绑定到可以进行const转换的对象上。存在继承关系的类时一个例外:可以将基类的指针或引用绑定到这个类的派生对象上。
这种操作称之为 "切割"(或切片),寓意是把子类中父类的那部分切过来赋值过去。
notion image
当使用基类的引用或指针时,不知道绑定的对象的真实类型是什么,这个对象可能是基类对象,也可能是派生类对象。而且与内置指针一样,智能指针也支持派生类到基类的转换,可以将指向派生对象的指针存储到基类的智能指针。
 
 

静态类型和动态类型

当使用存在继承关系的类时,应该区分变量或表达式的静态类型(static type)以及其背后的动态类型(dynamic type)。表达式的静态类型在编译时就是已知的,它是变量声明时的类型或者表达式的结果类型。动态类型是变量或表达式所表示的在内存中的真正对象的类型,这个类型必须到运行时才能知道。如:
item的静态类型是 Quote&,动态类型则依据绑定到 item 的参数类型,这个类型直到运行时才能知道。如果传递 Bulk_quote 那么 item 的静态类型与动态类型将不一样,此时,item 的静态类型是 Quote& ,而其动态类型是 Bulk_quote
既不是引用也不是指针的表达式的静态类型和动态类型是一样的。理解基类的指针或引用的静态类型和动态类型不一样是至关重要的。
 

没有从基类到派生类的隐式转换

从派生类到基类的转换是因为每个派生对象都包含基类部分,这个部分可以被基类的指针或引用绑定。基类对象可以独立存在,也可以作为派生对象的一部分。一个非派生类一部分的基类对象只包含基类定义的成员,并不包含派生类定义的成员。所以,并不存在从基类到派生类的自动转换。
如果以上赋值是合法的,那么将在bulkPbulkRef中使用base中不存在的成员。即便基类指针或引用绑定到派生对象,其依然不能转为派生类:
编译器没有在编译时以任何方式知道从基类转为派生类是否是安全的。编译器只能依据指针或引用的静态类型来判断转换是否是合法的。如果基类有一个或多个虚函数,可以使用 dynamic_cast 来请求带运行时检查的转换。同样,在确实知道从基类到派生类的转换是安全的,可以使用 static_cast 类覆盖掉编译器的规则。
 

对象间不存在转换

派生类到基类的自动转换只发生于引用或指针类型。从派生对象到基类对象是不存在转换的。然而,从表现上看经常可以将派生对象转为基类,只是这和引用、指针的那种派生类到基类的转换是不一样的。
当初始化一个类对象时,将调用构造函数。当赋值时将调用赋值操作符。这些成员函数通常具有一个该类对象的 const 引用的参数。由于其参数接收引用,派生类到基类的转换允许传递一个派生对象给基类的拷贝/移动操作成员函数。这些操作并不是virtual的。当传递派生对象给基类构造函数时,基类中定义的构造函数将被执行。那个构造函数只能识别基类自己定义的成员,类似的,如果将一个派生对象赋值给基类对象,基类中定义的赋值操作符将被执行。那个操作符只能识别基类中定义的成员。由于派生类部分被忽略了,所以派生类部分被裁剪(sliced down)掉了。
当用派生对象去初始化或赋值基类对象时,只有基类部分被拷贝、移动或赋值,派生类部分将被忽略。
 
  • C++
  • 重载、类型转换与运算符虚函数和多态
    目录