C++语言程序设计笔记 - 第7章 - 继承与派生

第7章 继承与派生

7.1 类的继承与派生

7.1.1 继承关系举例

类的继承,就是新的类从已有类那里得到已有的属性

类的派生,就是已有类产生新类的过程

基类(父类):原有的类称为基类父类

派生类(子类):新类称为派生类子类

基类的所有成员都是其派生类的成员,派生类还可以有自己的新成员。


7.1.2 派生类的定义

定义派生类:

class 派生类名:继承方式 基类名1,继承方式 基类名2,...,继承方式 基类名n{
    派生类成员声明;
};

多继承:一个派生类,可以同时有多个基类,这种情况称为多继承。

单继承:一个派生类只有一个基类,这种情况称为单继承。

继承具有传递性。

直接基类:直接参与派生出某类的基类,称为直接基类。

间接基类:基类的基类,甚至更高层的基类,称为间接基类。

继承方式关键字为:public(公有继承)、protected(保护继承)、private(私有继承)。

类中默认继承方式是private(私有继承)。

结构体中默认继承方式为public。

继承方式规定了如何访问从基类继承的成员。

类的继承方式指定了派生类成员以及类外对象对于从基类继承来的成员的访问权限。

派生类成员:指除了从基类继承来的所有成员之外,新增加的数据和函数成员


7.1.3 派生类生成过程

派生出新类的过程可分为三个步骤:吸收基类成员、改造基类成员、添加新的成员。

(1)吸收基类成员:派生类接收基类中除构造函数和析构函数外的所有非静态成员。

(2)改造基类成员:对基类成员的访问控制,以及对基类数据或函数成员的覆盖(见第8章)或隐藏(在派生类中声明一个和基类的数据或函数成员同名的成员)。

同名隐藏规则:如果派生类声明了一个和某基类成员同名的新成员(如果是成员函数,则参数表也要相同,参数不同的情况属于重载),派生类的新成员就隐藏了外层同名成员。这时,在派生类中或者通过派生类的对象,直接使用成员名,就只能访问到派生类中声明的同名成员,基类中的同名成员就被“隐藏”了,这称为同名隐藏规则

(3)添加新的成员:添加新的数据和函数成员以扩展功能,添加构造函数与析构函数以进行初始化和扫尾工作。


7.2 访问控制

7.2.1 公有继承

公有继承时,基类的公有、保护成员的访问属性在派生类中不变,而基类的私有成员在派生类中不可直接访问。也就是说,基类公有、保护成员在派生类中仍作为公有、保护成员,派生类的其他成员可以直接访问它们。在类族外只能通过派生类的对象访问从基类继承的公有成员,而无论是派生类的成员,还是派生类的对象,都无法直接访问基类的私有成员。


7.2.2 私有继承

私有继承时,基类的公有、保护成员都以私有成员的身份出现在派生类中,而基类的私有成员在派生类中不可直接访问


7.2.3 保护继承

保护继承时,基类的公有、保护成员都以保护成员的身份出现在派生类中,而基类的私有成员在派生类中不可直接访问

如果合理地利用保护成员,就可以在类的复杂层次关系中,在共享与成员隐藏之间找到一个平衡点,既能实现成员隐藏,又能方便继承,实现代码的高效重用和扩充。


7.3 类型兼容规则

类型兼容规则:在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。

类型兼容规则中的替代包括以下情况:

(1)派生类的对象可以隐含地转换为基类的对象;

(2)派生类的对象可以初始化基类的引用;

(3)派生类的指针可以隐含转换为基类的指针。

在替代之后,派生类对象就可以作为基类对象使用,但只能使用从基类继承来的成员。

通过基类对象名、指针只能使用从基类继承的成员。

例子:如果B为基类,D为B类的公有派生类,则D类中包含了基类B中除构造函数、析构函数之外的所有成员。这时,根据类型兼容规则,在基类B的对象可以出现的任何地方,都可以用派生类D的对象来替代。在如下的程序中,b1为B类的对象,d1为D类的对象:

class B{...};
class D:public B{...};	//公有继承
B b1,*pb1;
D d1;

这时:

(1)派生类对象可以隐含地转换为基类对象,即用派生类对象中从基类继承来的成员,逐个赋值给基类对象的成员:

b1=d1;	//派生类对象d1可以隐含地转换为基类对象

(2)派生类对象可以初始化基类对象的引用:

B &rb=d1;	//派生类对象d1可以初始化基类对象的引用

(3)派生类对象的地址可以隐含转换为指向基类地指针:

pb1=&d1;	//派生类对象d1的地址&d1可以隐含转换为指向基类地指针

由于类型兼容规则的引入,对于基类及其公有派生类的对象,可以使用相同的函数统一进行处理。因为当函数的参数为基类的对象(或引用、指针)时,实参可以是派生类的对象(或指针),而没有必要为每一个类设计单独的模块,大大提高了程序的效率。这正是C++的又一重要特色,即多态性(见第8章)。类型兼容规则是多态性的重要基础之一。

**虽然根据类型兼容规则,可以将派生类对象地址赋给基类指针,但通过这个基类指针访问成员时,只能访问到基类成员,无法访问到派生类成员。**这种情况下,虽然根据类型兼容规则,在基类对象出现的场合使用派生类对象进行了替代,但替代之后派生类仅仅发挥出基类的作用,而采用多态性(见第8章)的设计方法,可以保证在类型兼容的前提下,基类、派生类分别以不同的方式来响应相同的信息。


7.4 派生类的构造和析构函数

基类的构造函数和析构函数不能被继承

在派生类中,如果要对派生类新增的成员进行初始化,就必须为派生类添加新的构造函数。对派生类对象的清理扫尾工作也需要加入新的析构函数。

派生类的构造函数只负责对派生类新增的成员进行初始化,对所有从基类继承而来的成员,其初始化工作还是由基类的构造函数完成。

7.4.1 派生类的构造函数

构造派生类的对象时,要对基类的成员对象和新增成员对象进行初始化。

派生类构造函数的一般语法:

派生类名::派生类名(参数表):基类名1(基类1初始化参数表),...,基类名m(基类m初始化参数表),成员对象名1(成员对象1初始化参数表),...,成员对象名n(成员对象n初始化参数表){
    派生类构造函数的其他初始化操作;
}

**当一个类同时有多个基类时,对于所有需要给予参数来进行初始化的基类,都要显式地给出基类名和参数表。**对于使用默认构造函数的基类,可以不给出类名。同样,对于成员对象,如果是使用默认构造函数,也不需要写出对象名和参数表。

《面向对象程序设计——C++语言描述(原书第2版)》P.147:如果基类拥有构造函数但没有默认构造函数,那么派生类的构造函数必须显式地调用基类的某个构造函数。

对基类初始化时,如果需要调用基类的带有形参表的构造函数,派生类就必须声明构造函数,来提供一个将参数传递给基类构造函数的途径,保证在基类进行初始化时能够获得必要的数据。

如果不需要调用基类的带参数的构造函数,也不需要调用新增的成员对象的带参数的构造函数,派生类也可以不声明构造函数,全部采用默认构造函数,这时,新增成员的初始化工作可以由其他公有函数来完成。

《面向对象程序设计——C++语言描述(原书第2版)》P.148:一般来说,最好为基类提供一个默认构造函数(即无形参),这样可以避免上述派生类显式调用基类构造函数的问题,而且并不妨碍派生类构造函数去调用基类的非默认构造函数。

当派生类没有显式的构造函数时,系统会隐含地生成一个默认构造函数,该函数会使用基类的默认构造函数,来对从基类继承的数据进行初始化,并且会调用类类型的成员对象的默认构造函数,对这些成员对象初始化(可见默认构造函数并非什么也不做)。

《面向对象程序设计——C++语言描述(原书第2版)》P.149:以“Derived类从Base类派生”为例,可总结如下四点:

(1)若Derived有构造函数而Base没有,当创建Derived类的对象时,Derived的相应构造函数被自动调用;

(2)若Derived没有构造函数而Base有,则Base还必须拥有默认构造函数。只有这样,当创建Derived类的对象时,才能自动执行Base的默认构造函数;

(3)若Derived有构造函数,且Base有默认构造函数,则创建Derived类的对象时,Base的默认构造函数会自动执行,除非当前被调用的派生类构造函数在其初始化段中显式地调用了Base的非默认构造函数;

(4)若Derived和Base都有构造函数,但Base没有默认构造函数,则Derived的每个构造函数必须在其初始化段中显式地调用Base的某个构造函数。只有这样,当创建Derived类的对象时,Base的构造函数才能获得执行机会。

注意:在创建派生类对象时,必须显式地或隐式地执行其基类的某个构造函数。


派生类构造函数执行的一般次序如下:

(1)调用基类构造函数,调用顺序按照它们被继承时声明的顺序(从左向右);

(2)对派生类新增的成员对象初始化,调用顺序按照它们在类中声明的顺序;

(3)执行派生类的构造函数体中的内容。

构造函数初始化列表中基类名、对象名之间的次序是无关紧要的,基类构造函数的调用和各个对象成员的初始化顺序是确定的,与其无关。

#include<iosteam>
using namespace std;
class Base1{
public:
    Base1(int i){cout<<"Constructing Base1 "<<i<<endl;}
};
class Base2{
public:
    Base2(int j){cout<<"Constructing Base2 "<<j<<endl;}
};
class Base3{
public:
    Base3(){cout<<"Constructing Base3 *"<<endl;}	//无参构造函数
};
//派生新类Derived,注意基类被继承时声明的顺序:Base2,Base1,Base3
class Derived:public Base2,public Base1,public Base3{
public:
    Derived(int a, int b, int c,int d):Base1(a),member2(d),member1(c),Base2(b){}
private:
//注意派生类新增的成员对象在类中声明的顺序:member1,member2,member3
    Base1 member1;
    Base2 member2;
    Base3 member3;
};
int main(){
    Derived obj(1, 2, 3, 4);
    return 0;
}
//程序运行结果为:
//Constructing Base2 2	基类的构造函数的调用顺序是按照派生类定义时的顺序
//Constructing Base1 1
//Constructing Base3 *
//Constructing Base1 3	内嵌对象的构造函数的调用顺序是按照成员对象在类中声明的顺序
//Constructing Base2 4
//Constructing Base3 *

7.4.2 派生类的复制构造函数

当存在类的继承关系时,对于一个类,如果程序员没有编写复制构造函数,编译系统会在必要时自动生成一个隐含的复制构造函数,这个隐含的复制构造函数会自动调用基类的复制构造函数,然后对派生类新增的成员对象一一执行赋值。

如果要为派生类编写复制构造函数,一般需要为基类相应的复制构造函数传递参数。


7.4.3 派生类的析构函数

在派生过程中,基类的析构函数也无法被继承下来。

派生类析构函数的声明方法与没有继承关系的类中析构函数的声明方式完全相同,只负责清理派生类新增的非对象成员,而对于基类以及对象成员的清理,系统会自动调用基类以及对象成员的析构函数来完成。

《面向对象程序设计——C++语言描述(原书第2版)》P.151:由于每个类至多只有一个析构函数,因此对析构函数的调用不会产生二义性,在析构函数中不必显式地调用其他析构函数。

派生类中析构函数的执行次序与构造函数正好完全相反

(1)首先执行派生类析构函数的函数体;

(2)调用类类型的派生类对象成员所在类的析构函数,对派生类新增的类类型的成员对象进行清理;

(3)调用基类析构函数,对所有从基类继承来的成员进行清理。


7.5 派生类成员的标识与访问

在派生类中,成员按访问属性可划分为以下4种:

(1)不可访问的成员:不可访问的成员从基类的私有成员继承而来。派生类或是建立派生类对象的模块都无法访问到它们。如果从派生类继续派生新类,也是无法访问的。

(2)私有成员:私有成员包括从基类继承而来的成员,以及新增加的成员。在派生类内部可以访问,但是建立派生类对象的模块中无法访问。如果继续派生,就变成了新的派生类中的不可访问的成员。

(3)保护成员:保护成员包括从基类继承而来的成员,以及新增加的成员。在派生类内部可以访问,但是建立派生类对象的模块中无法访问。如果继续派生,可能是新派生类中的私有保护成员。

(4)公有成员:派生类、建立派生类的模块都可以访问。如果继续派生,可能是新派生类中的私有保护公有成员。

《面向对象程序设计——C++语言描述》P.143:一般来说,应避免将数据成员设计为保护类型,即使该数据成员可以成为保护成员,更好的解决方案是:首先将这个数据成员定义为私有成员,然后为它设计一个用来进行存取访问的保护成员函数,通常将这种类型的成员函数称为访问函数。(当然,也不能一概而论,如果这个数据成员比较复杂,例如一个数组,与其为它设计大量的访问函数,还不如直接将它定义为保护成员。)将数据成员定义为私有,好处在于:(1)实现数据隐藏;(2)采用上述私有数据成员与相应保护型访问函数相结合的设计模式,可以在不修改类的访问接口(保护和公有的成员函数)的前提下,任意修改修改这个类的实现代码,即接口与实现分离


7.5.1 作用域分辨符

同名隐藏规则:如果派生类声明了一个和某基类成员同名的新成员,派生类的新成员就隐藏了外层同名成员。这时,在派生类中或者通过派生类的对象,直接使用成员名,就只能访问到派生类中声明的同名成员,基类中的同名成员就被“隐藏”了,这称为同名隐藏规则。这时,如果要访问被隐藏的成员,就需要使用基类名加作用域分辨符来限定。


如果某个派生类的多个基类拥有同名的成员(各个基类之间没有任何继承关系,同时也没有共同基类),同时,派生类又新增了这样的同名成员,这种情况下,派生类成员将隐藏所有基类的同名成员。这时,派生类新增成员可以用“派生类对象名.成员名”或“派生类对象指针->成员名”唯一标识基类的同名成员只能用“派生类对象名.基类名::成员名”或“派生类对象指针->基类名::成员名”来访问

如果某个派生类的多个基类拥有同名的成员(各个基类之间没有任何继承关系,同时也没有共同基类),但是,派生类中没有没有声明同名成员,这种情况下,“派生类对象名.成员名”或“派生类对象指针->成员名”就无法唯一标识成员。这时,从不同基类继承过来的成员具有相同的名称,又具有相同的作用域,则必须通过“派生类对象名.基类名::成员名”或“派生类对象指针->基类名::成员名”来访问这些继承过来的成员。


只有在相同作用域中定义的函数才可以重载。

如果子类中定义的函数与父类的函数同名,但具有不同的参数数量或参数类型,不属于函数重载。这时,子类中的函数会将父类中的函数隐藏

《面向对象程序设计——C++语言描述》P.134-135:使用using声明可以改变成员在派生类中的访问限制。例如,基类中的某公有成员一般情况下被(派生类)继承为公有成员,但(在派生类的“private:”部分中)使用using声明可将其改为私有成员,这样,派生类的对象就无法直接访问这些成员,从而将这些成员在派生类中隐藏起来。

当子类中定义的函数与父类的函数同名,但具有不同的参数数量或参数类型时,若不希望基类的函数被隐藏,可在子类中声明函数时使用“using 基类名::成员函数名;”,这样可使子类和父类中的同名函数产生重载,共同存在于派生类作用域中。


如果某个派生类的部分或全部直接基类是从另一个共同的基类(即该派生类的间接基类)派生而来,在这些直接基类中,从上一级共同基类继承来的成员就拥有了相同的成员名,这时,该派生类中就会出现间接继承而来的成员同名的情况。在派生类中不再添加新的同名成员(如果添加了新的同名成员,同样遵循同名隐藏规则)。这时,通过派生类的对象来标识和访问间接基类中的同名成员,必须用直接基类名来限定(指定一条“路径”):派生类的对象名.直接基类名::间接基类中的成员名

以外,解决这种情况下的同名成员唯一标识问题还有其他方法(见下一节7.5.2节)。


7.5.2 虚基类

如果某个派生类的部分或全部直接基类是从另一个共同的基类(即该派生类的间接基类)派生而来,在这些直接基类中,从上一级共同基类继承来的成员就拥有了相同的成员名。这时,在派生类的对象中,同名数据成员在内存中会有多个副本,同名函数成员会有多个映射。这种情况下,要解决同名成员的唯一标识问题(避免二义性问题),可以:

(1)使用直接基类名加作用域分辨符来限定:派生类的对象名.直接基类名::间接基类中的成员名(见上一节7.5.1节);

(2)使用using(见上一节7.5.1节);

(3)在派生类的直接基类中,将直接基类的共同基类设置为虚基类(将派生类的间接基类设置为虚基类)。

虚基类的声明:

class 派生类名:virtual 继承方式 基类名

在派生类的直接基类中声明间接基类为虚基类之后,虚基类的成员在进一步派生过程中和和派生类一起维护同一个内存数据副本。这时,在派生类中标识和访问间接基类中的成员,不需要用直接基类名来限定,只需“派生类的对象名.间接基类中的成员名”。


7.5.3 虚基类及其派生类构造函数

如果虚基类声明有非默认形式的(即带形参的)构造函数,并且没有声明默认形式的构造函数,这时,直接或间接继承虚基类的所有类,都必须在构造函数的成员初始化列表中列出对虚基类的初始化,并且,派生类不仅要为间接基类(虚基类)初始化,还要为直接基类初始化

#include<iostream>
using namespace std;
class Base0{
public:
    Base0(int var):var0(var){}
    int var0;
    void fun0(){cout<<"Member of Base0"<<endl;}
};
class Base1:virtual public Base0{	//Base0为虚基类,派生Base1类
public:
    Base1(int var):Base0(var){}
    int var1;
};
class Base2:virtual public Base0{	//Base0为虚基类,派生Base2类
public:
    Base2(int var):Base0(var){}
    int var2;
};
class Derived:public Base1,public Base2{	//多继承的派生类Derived
public:
    Derived(int var):Base0(var),Base1(var),Base2(var){}	//不仅要为间接基类初始化,还要为直接基类初始化
    int var;
    void fun(){cout<<"Member of Derived"<<endl;}
};
int main(){
    Derived d(1);
    d.var0=2;	//直接访问虚基类的数据成员
    d.fun0();	//直接访问虚基类的函数成员,执行结果为“Member of Base0”
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值