C++ 程序设计 第5章 类的继承与派生
第5章 类的继承与派生
1 类的继承与类的派生
继承
人们追求代码复用(这是提高软件开发效率的重要手段),将继承和派生用于程序设计方法中,从而有了面向对象程序设计的重要特点。C++对代码复用有很强的支持,继承就是支持代码复用的机制之一。
继承与派生有相同的含义,不同的角度。继承对应于自下而上的过程,派生对应于自上而下的过程。从基类的角度看,是派生了子类。从子类的角度看,是继承与基类。
通过已有的类出建立新类的过程,叫作类的派生。原来的类称为基类,也称为父类或一般类,新类成为派生类,也称为子类或特殊类。派生类派生自基类,也可以说基类派生了派生类。派生类可以再作为基类派生新的派生类,由此基类和派生类的集合称作为类继承层次结构。
使用基类派生新类时,除构造函数和析构函数外,基类的所有成员自动成为派生类的成员,包括基类的成员变量和成员函数。同时,派生类可以增加基类中没有的成员,这同样是指成员变量和成员函数。当然派生类需要定义自己的构造函数和析构函数。
若派生类中定义了一个与基类中同名的成员,则会出现基类与派生类有同名成员的情况,这是允许的。通过派生类访问同名成员这叫做重写。对于成员函数来说,派生类既继承了基类的同名成员函数,又在派生类中重写了这个成员函数。这成为函数重定义,也称为同名隐藏。隐藏的意思是指,使用派生类对象调用这个名字的成员函数时,调用的是派生类中定义的成员函数,即隐藏了基类中的成员函数。
派生类的定义与大小
从基类派生派生类的一般格式如下
class 派生类名:继承方式说明符 基类名{
类体
};
继承方式说明符指明如何控制基类成员在派生类中的访问属性。通常有3种方式,分别是 public(公有继承)、 private(私有继承) prtected(保护继承)
空类也可以作为基类,也就是说,空类可以派生子类。
派生类改变基类成员的访问权限,父类 public 成员,子类改成私有成员,在派生类的成员函数中访问基类的成员时,需要使用类名加以限定,如 BaseClass::v1
派生类对象占用的存储空间大小,等于基类成员变量占用的存储空间大小加上派生类对象自身成员变量占用的存储空间大小。
对象占用的存储空间包含对象中各成员变量占用的存储空间。为变量分配内存时,会根据其对应的数据类型,在存储空间内对变量的起始地址进行边界对齐。可以使用sizeof()函数计算对象占用的字节数。
对象的大小与普通成员变量有关,与成员函数和类中的静态成员变量无关。对类对象分配空间时,遵循字节对齐的原则。空类的大小是1
基类与子类占用空间及字节对齐,也就是四个字节为一组,int类型类9个字节对齐到12个
继承关系的特殊性
如果基类有友元类或友元函数,则其派生类不会因继承关系而也有此友元类或友元函数。
如果基类是某类的友元,则这种友元关系是被继承的。即被派生类继承过来的成员函数,如果原来是某类的友元函数,那么它作为派生类的成员函数仍然是某类的友元函数。
总之,基类的友元不一定是派生类的友元,基类的成员函数是某类的友元函数,则其作为派生类继承的成员函数仍是某类的友元函数。
如果想在子类里面重写父类的友元函数,则必须在某类里面也声明子类的友元函数
如果基类中的成员是静态的,则在其派生类中,被继承的成员也是静态的,即其静态属性随静态成员被继承。
如果基类中的静态成员是公有的或是保护的,则他们被其派生类继承为派生类的静态成员。访问这些成员时,通常用 类名::成员名
的方式引用或调用。无论有多少个对象被创建,这些成员都只有一个拷贝,他为基类和派生类的所有对象所共享。
父类中的友元函数子类无法继承,但是父类中的成员函数是其他类的友元函数时可以继承
有继承关系的类之间的访问
派生类和基类中都可以定义自己的成员变量和成员函数,派生类中的成员函数可以访问基类中的公有成员变量,但不能直接访问基类中的私有成员变量。不能使用基类对象名.基类私有成员函数(实参)
基类对象名.基类私有成员变量
基类名::基类私有成员
作用范围,派生类在内层,基类在外层。如果子类声明了一个和基类某个成员同名的新成员,派生的新成员就隐藏了外层同名成员,直接使用成员名只能访问到派生类的成员。如果同名的是成员函数,就算重载形式不一样,也无法访问父类的同名成员函数。
访问基类和派生类成员的方式,通过 基类对象名.基类成员函数(实参)
基类对象名.基类成员变量
基类名::基类成员
访问
protected 访问范围说明符
保护成员的访问范围比私有成员的访问范围大,能访问私有成员的地方都能访问保护成员,此外,基类中的保护成员可以在派生类的成员函数中被访问。
引入保护成员的理由是,基类的成员本来就是派生类的成员。因此对于那些处于隐藏的目的不宜设为公有成员,但又确实需要在派生类的成员函数中经常访问的基类成员,将他们设置为保护成员,既能起到隐藏的目的,又避免了派生类成员函数要访问它们时只能间接访问所带来的麻烦。
不过需要注意的是,派生类的成员函数只能访问所作用的那个对象,即this指针指向的对象 的基类保护成员,不能访问其他基类对象的基类保护成员。
在基类中,一般都将需要隐藏的成员说明为保护成员而非私有成员。将基类中成员变量的访问方式修改为protected后,在派生类中可以直接访问。
多重继承
C++允许从多个类派生一个类,即一个类可以同时有多个基类,这称为多重继承。相应的,从一个基类派生一个派生类的情况,称为单继承或单重继承。
多重继承格式如下
class 派生类名:继承方式说明符 基类名1,继承方式说明符 基类名2,···,继承方式说明符 基类名n{
类体
};
多重继承情况下如果多个基类间成员重名时,按如下方式进行处理:
对派生类而言,不加类名限定时默认访问的是派生类的成员,而要访问基类重名成员时,要通过类名加以限定。
多重继承时,如果多个基类中有重名的成员,则他们都被继承到派生类中,在这种情况下,访问重名成员时会遇到二义性问题。
如果派生类中新增了同名成员,不会产生二义性。
如果派生类中没有新增同名成员,当满足访问权限时,系统无法判断到底是调用哪个基类的成员,从而产生二义性。为了避免二义性,必须通过基类名和作用域分辨符来标识成员。
2 访问控制
继承方式说明符可以是 public private protected
公有继承
基类的公有成员和保护成员的访问属性在派生类中不变,而基类的私有成员在基类外不可直接访问。在派生类和基类之外,可以通过派生类的对象从基类继承的公有成员,且必须通过基类的公有成员函数访问基类的私有成员。
类型兼容规则
类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来代替。也称为赋值兼容规则,通过公有继承,派生类得到了基类中除构造函数,析构函数外所有的成员。这样,公有派生类实际就具备了基类的所有功能,凡是基类能解决的问题,公有派生类都可以解决。
3条类型兼容规则
- 派生类的对象可以赋值给基类对象
- 派生类对象可以用来初始化基类引用
- 派生类对象的地址可以赋值给基类指针,即派生类的指针可以赋值给基类的指针
上述三条规则反过来是不成立的。
私有继承
除公有派生外,C++还支持私有派生。
当类的继承方式为私有继承时,基类中的公有成员和保护成员都以私有成员身份出现在派生类中,而基类的私有成员在派生类中不可以直接访问。
经过私有继承之后,所有基类的成员都成为派生类的私有成员或不可直接访问的成员,如果进一步派生,基类的全部成员就无法在新的派生类中被访问。因此,私有继承之后,基类的成员再也无法在以后的派生类中直接发挥作用,也就是中止了基类功能的继续派生,出于这种原因,一般情况下私有继承的使用比较少。
当第一级派生类是基类的私有派生时,在第二级派生类中对基类的所有成员都是不可访问的,因为基类中的公有函数在第一级派生类中也是私有成员函数,在第二级派生类中不可以调用,因此也不能通过这些函数来访问基类中的成员。
C++中,不加说明时,默认的继承方式是 private
保护继承
保护继承中,基类的公有成员和保护成员都以保护成员的身份出现在派生类中,而基类的私有成员不可以直接访问。在类外无法通过派生类的对象无法直接访问他们。
3 派生类的构造函数和析构函数
构造函数与析构函数
派生类对象在创建时,除了要调用自身的构造函数进行初始化外,还要调用基类的构造函数初始化其包含的基类成员变量。如果不这么做的话,则编译器认为基类成员变量要使用无参的构造函数进行初始化,如果基类没有无参的构造函数,则会导致编译错误。
在执行一个派生类的构造函数之前,总是先执行基类的构造函数。派生类对象消亡时,先执行派生类的析构函数,再执行基类的析构函数。
生成派生类对象时,先执行各基类的构造函数,调用顺序为派生继承时的基类声明顺序。
派生类的析构函数内,只需要处理派生类新增的一般成员所占用的空间,所继承的基类成员的处理情况,由系统调用基类的析构函数来完成。派生类对象消亡时,析构函数各部分执行次序与构造函数相反。先执行自身的析构函数,再执行基类的析构函数。
如果一个派生类对象是用默认复制构造函数初始化的,那么他内部包含的基类对象也要用基类的复制构造函数初始化。
派生类构造函数执行的一般次序
- 调用基类构造函数,调用顺序按照他们被继承时声明的顺序,从左向右。
- 对派生类新增的成员变量初始化,调用顺序按照他们在类中声明的顺序。
- 执行派生类的构造函数体的内容。
构造函数初始化列表中基类名,对象名之间的次序无关紧要,他们各自出现的顺序可以是任意的,无论他们的顺序怎么样安排,基类构造函数的调用和各个成员变量的初始化顺序都是确定的。
赋值构造函数
对于一个类,如果程序中没有定义复制构造函数,则编译器会自动生成一个隐含的复制构造函数,这个隐含的复制构造函数会自动调用基类的复制构造函数,对派生类新增的成员对象一一执行复制。
派生类的默认赋值构造函数会调用基类的复制构造函数,以对派生类对象中的基类对象进行初始化。如果基类重载了运算符 =
,而派生类中没有重载,那么在派生类对象之间赋值,或用派生类对象对基类对象进行赋值时,其中基类部分的赋值操作是调用被基类重载的 =
完成的
多重继承的构造函数与析构函数
当创建有多个基类的派生类的对象时,按照类定义中给出的基类的顺序,依次调用它们的构造函数,再调用派生类的构造函数。对象消亡时,按照构造函数调用的次序的逆,调用析构函数。
4 类之间的关系
类与类之间的关系
使用已有类编写新的类有两种方式,继承和组合。
继承关系也称为 is a 关系或是关系,派生类对象也是一个基类对象。
使用继承机制设计类时,如果想让类B继承于类A,必须保证类B所代表的事物也是类A所代表的事物,这个命题从逻辑上是成立的,也就是说类B的范畴全部包含在类A的范畴内。
组合关系也称为 has a 关系或有关系,变现为封闭类,即一个类以另一个类的对象作为成员变量。包含其他对象如string的类成为封闭内,string称为内嵌类。
is a关系具有传递性,基类具有一般性,而派生类具有特性。
封闭类的派生
在派生类也是封闭类的情况下,构造函数的初始化列表不但要指明基类对象的初始化方式,还要指明成员对象的初始化方式。
生成派生类对象时,会引发一系列构造函数的调用。
顺序是,现根据派生层次从上至下依次执行所有基类的构造函数,最后执行自身的构造函数。如果某个类是封闭类,则在执行本类构造函数之前,先按照成员对象的定义顺序执行各个成员对象所属类的构造函数。而当派生类对象消亡时,执行析构函数的次序与执行构造函数的次序相反。
派生类构造函数中的某些初始化可能是基于基类的,所以规定构造函数从类层次的最上层处开始,一层层的执行构造函数。
互包含关系的类
两个类相互引用的情况,这种情况称为循环依赖。
使用向前引用申明不是万能的,在提供一个完整的类定义之前,不能定义该类的对象,也不能在内联成员函数中使用该类的对象。
如果确实需要循环定义两个类,即在类A中含有类B的成员变量,类B中含有类A的成员变量,那么可以让其中的一个类使用指针。
5 多层次的派生
派生可以是多层次的。在C++中,类之间的继承关系具有传递性
当生成派生类的对象时,会从最顶层的基类开始逐层往下执行所有基类的构造函数,最后执行派生类自身的构造函数,当派生类对象消亡时,会先执行自身的析构函数,然后自底向上依次执行各个基类的西沟函数。
一个类可以是另一个或多个类的基类,但说明为某个派生类的基类时,只能说明一次,不能重复说明。
6 基类和派生类指针的互相转换
对于指针类型,可以使用基类指针指向派生类对象,也可以将派生类的指针直接赋值给基类指针。但即使基类指针指向的是一个派生类的对象,也不能通过基类指针访问基类中没有而仅在派生类中定义的成员函数。
当派生类指针指向基类对象时,必须要将基类对象进行强制类型转换,才能赋给派生类指针。
编译器看到哪个类的指针,就会认为要通过它访问哪个类的成员,编译器不会分析基类指针指向的到底是基类对象还是派生类对象。
基类引用也可以强制转换为派生类引用。将基类指针强制转换为派生类指针,或将基类引用强制转换为派生类引用,都会有安全隐患。
C++提供了 dynamic_cast
强制类型转换运算符,可以判断这两种转换是否安全(即转换后的指针或引用是否真的指向或引用派生类对象)