类继承

第13章 类继承
            引言
     1.面向对象编程的主要目的之一是提供可重用的代码。
      2.类库是由类声明和实现构成。通常,类库是由源代码的方式提供的,这意味着可以对其进行修改,以满足需求。
      3.C++提供了比修改代码更好的方法来扩展和修改类,这种方法叫做类继承,它能够从已有的类派生出新的类,而派生类继承了原有类(称为基类)的特征的特征,包括方法。
      4.可以通过继承完成一些工作:
1.可以在已有类的基础上添加功能;
2.可以给类添加数据;
3.可以修改类方法的行为。
当然,可以通过复制原始类代码,并对其进行修改来完成上述工作,但类继承机制只需提供新特性,甚至不需要访问源代码就可以派生出类。
13.1 一个简单的基类
     从一个类派生出另一个类时,原始类叫做基类,继承类叫做派生类。
建立一个TableTennisPlayer类,然后从这个类中派生出Ratedplayer类:
class RatedPlayer : public TableTennisPlayer
{
    ..........
};
冒号指出RatedPlayer类的基类是TableTennisPlayer类。public表示这是公有派生。
派生类对象包含基类对象,使用公有派生,基类的公有成员将成为派生类的公有成员,
基类的私有部分也将成为派生类的一部分,但是只能通过基类的公有和保护方法访问。
派生类有以下特点:
1.派生类对象存储了基类的数据成员(继承了实现);
2.派生类对象可以使用基类的方法(继承基类的接口);
派生类可以添加什么???
1.需要自己的构造函数
2.可以根据需要添加额外的数据成员和成员函数。
例:RatedPlayer(unsigned int r = 0, const string & fn = "none", const string & in = "none", bool ht = false);
构造函数必须给新成员和继承的成员提供数据。
派生类的构造函数:
派生类不能直接访问基类的私有成员,而必须提供基类方法进行访问。派生类构造函数必须使用基类构造函数。
创建派生类对象时,程序首先创建基类对象。从概念讲,这意味着基类对象应当在程序进入派生类构造函数之前就要被创建。
RatedPlayer::RatedPlayer(unsigned int r, const string & fn, const string & in, bool ht):TableTennisPlayer(fn,in,ht)
     {
      rating = r;
     }
其中:TableTennisPlayer(fn,in,ht)是成员初始化列表。派生类成员也可以使用成员初始化列表语法。
必须首先创建基类对象,如果不调用基类构造函数,程序将使用默认的基类构造函数,除非要使用默认构造函数,否则应显式调用正确的基类构造函数。
有关派生类构造函数的要点如下:
1.首先创建基类对象;
2.派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数;
3.派生类构造函数应初始化派生类新增的数据成员;
注意:
创建派生类对象时,程序首先调用基类构造函数,然后在调用派生类构造函数。基类构造函数负责初始化继承的数据成员。
派生类构造函数主要用于初始化新增的数据成员。派生类的构造函数总是调用一个基类构造函数。可以使用成员初始化列表语法指明要使用的基类构造函数,否则将调用默认的基类构造函数。
派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类析构函数。
成员初始化列表只能用于构造函数。
派生类和基类之间的特殊关系:
         1.派生类对象可以使用基类的方法,条件是基类方法不是私有的:
2.基类指针可以在不进行显式类型转换的情况下指向派生类对象;基类引用可以在不进行显式类型转换的情况下引用派生类对象;
RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
TableTennisPlayer & rt = rplayer1;
TableTennisPlayer * pt = &rplayer1;
rt.Name();
pt->Name();
基类指针或引用只能用于调用基类类方法。
3.不可以将基类对象的指针和引用赋给派生类的指针和对象;
TableTennisPlayer player1("Tara", "Boomdea", false);
RatedPlayer & rplayer1 = player1;  //不可用!!!
RatedPlayer * rplayer1 = &player1;  //不可用!!!

原因就是派生类对基类进行了一定程度的扩充,派生类可能含有基类没有的类方法,rplayer1调用派生类方法时,基类对象player1是无法访问
除基类方法之外的派生类方法的,但是rt或者pt访问的基类方法,派生类对象是可以访问的。
注:
1.形参是一个基类引用,它可以指向基类对象或者派生类对象;
2.对于形参为指向基类的指针吗,它可以使用基类对象的地址或者派生类的地址作为实参;
3.引用属性能够将基类对象初始化为派生类对象;   //复制构造函数
例:RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
        TableTennisPlayer player1(rpalyer1);
4.也可以将派生对象赋给基类对象;        //重载赋值运算符
  RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
  TableTennisPlayer player1;
  player1 = rplayer1; 
  不管是对象、引用还是指针,传递方向一定是单方向的由派生类对象或者地址传递给基类对象或者指针!!!!

13.3 多态公有继承
       希望同一个方法在派生类和基类中的行为是不一样的,换句话说,方法的行为取决于调用该方法的对象。
实现多态公有继承:
1.在派生类中重新定义基类的方法;
2.使用虚方法。
公有继承产生的基类和派生类是一种is-a关系,派生类对象也是一种基类对象,因为基类对象可以执行的类方法操作,派生类对象也可以使用。
is-a关系是一种不可逆关系。
使用虚方法:
1. 对于同一种方法在不同类的不一样的行为,使用关键字virtual。(只定义在基类的类声明中)
2. 如果要在派生类中重新定义基类的方法(如果有这种需要),通常将基类方法声明为虚的。
  这样,程序将根据对象类型而不是引用或者指针类型来选择方法版本。
3. 使用虚方法将产生两种方法版本:基类一个,派生类一个。
4. 如果方法是通过引用或者指针而不是对象调用的(对象调用:哪类对象就调用哪类方法版本),它将确定使用哪一种方法版本。
5. 如果没有使用关键字virtual,程序将根据引用类型或者指针类型选择方法;
6. 如果使用了关键字virtual,程序将根据引用或者指针指向的对象类型来选择方法。
  总之,使用了虚方法后,不管是直接的类对象还是通过引用或指针间接指向的类对象来寻找对应的方法版本。
7. 派生类构造函数在初始化继承的基类私有数据时,采用的是成员初始化列表法;然后在构造函数体中初始化新增的数据项。
8. 在派生类方法中,标准技术是使用作用域解析运算符来调用基类方法。
 (派生类方法版本调用基类方法版本时,表明版本归属哪一类)。非虚函数就不用了,公有的嘛。
9. 为何使用需析构函数???
  如果析构函数不是虚的,则只调用对应于指针类型的析构函数;
  如果析构函数是虚的,则调用相应对象类型的析构函数;使用虚析构函数可以保证正确的析构函数序列被调用。

13.4 静态联编和动态联编
         在编译过程中进行联编称为静态联编;
编译器必须生成能够在程序运行时选择正确的虚方法的代码,称为动态联编。


指针和引用类型的兼容性
     在C++中,动态联编与通过指针和引用调用方法相关,从某种程度上讲,这是由继承控制的。
公有继承建立is-a关系的一种方法是如何处理指向对象的指针和引用。
通常,C++不允许将一种类型的地址赋给另一种类型的指针,引用也是如此;
指向基类的引用或指针可以引用派生类对象,而不必进行显式类型转换。
BrassPlus dilly("anne dill",49332,2000);
Brass * pb = &dilly;
Brass & rb = dilly;
将派生类引用或指针转换为基类引用或指针被称为向上强制转换。向上转换是可传递的
将基类引用或指针转换为派生类引用或指针被称为向下强制转换。如果不是显式类型转换,则向下强制转换是不允许的。原因是is-a关系是不可逆的。
对于使用基类引用或指针作为参数的函数调用,将进行向上转换。
隐式向上强制转换使基类指针或引用可以指向基类对象或者派生类对象。

虚成员函数和动态联编
         编译器对非虚方法使用静态联编;编译器对虚方法使用动态联编。
首先看效率。为使程序能够在运行阶段进行决策,必须采取一些方法来跟踪基类指针或引用指向的对象类型,这增加了额外的处理开销。
如果类不会作为基类,则不需要动态联编。如果派生类不重新定义基类的任何方法,也没有必要使用动态联编。
注: 如果要在派生类中重新定义基类的方法,则将它们设计为虚方法,否则,设置为非虚方法。


虚函数的工作原理:
         1.每个对象都将增大,增大量为存储地址的空间;
2.对于每个类,编译器都创建一个虚函数地址表(数组);
3.对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址。


有关虚函数的注意事项
        1.在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类中是虚的;
2.如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用引用或指针类型定义的方法,这称为动态联编。
3.如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。
4.构造函数不能是虚的;
5.析构函数应当是虚函数,除非类不用做基类。
6.友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。
7.如果派生类没有重新定义函数,将使用该函数的基类版本。
 如果派生类位于派生链中,则将使用最新的虚函数版本吗,例外的情况是基类版本是隐藏的;
8.第一,如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针。
 第二,如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。

13.5 访问控制:Protected
        关键字protected和private相似,在类外只能用公有类成员来访问protected部分的类成员。
private和protected之间的区别只有在基类派生的类中才会体现出来。
派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。
也就是说,对于protected成员,基类方法和派生类方法都具有相同的访问权限,直接访问。(可以直接出现在类方法中)
 对于private成员,基类方法可直接访问私有成员,但是派生类只是继承了这些成员,要想在自己的类方法中使用,必须使用基类方法间接访问。(私有成员不能直接出现在派生类方法,而是通过基类方法间接访问)
     换句话说,protected的使用使得成员变成公有的了,哪个类都可以直接访问了!!!!
最好对数据成员采用private访问控制,不要使用保护访问控制;同时,通过基类方法使派生类能够访问基类数据。
对于基类的成员函数来说,保护访问控制很有用,它让派生类能够访问基类中公众不能使用的内部函数。    
总之,基类使用了protected访问控制,使得其派生类链均可直接使用protected成员,这是一种公有体系!!!      

13.6 抽象基类(ABC)-------将某些类的共性放在一块;
     C++通过使用纯虚函数提供未实现的函数,纯虚函数声明的结尾处为 =0;
当类声明中包含纯虚函数时,则不能创建该类对象;
包含纯虚函数的类只能做基类;
原型中的=0使虚函数成为纯虚函数;
要成为真正的ABC,必须至少包含一个纯虚函数!!!
此外,在原型中使用=0指出类是一个抽象基类,在类定义过程中可以不定义该函数(根据需要决定用不用定义)
由抽象基类派生的类都是一些具体类。
总之,ABC描述的是至少使用一个纯虚函数的接口,从ABC派生出的类将根据派生类的具体特征使用常规虚函数来实现该接口。
ABC要求具体派生类覆盖其纯虚函数-----迫使派生类遵循ABC设置的接口规则。
13.7 继承和动态内存分配
     第一种情况:
基类使用new分配内存,派生类不使用new:基类必须提供显式复制构造函数、赋值运算符函数和析构函数。
派生类是不需要任何操作来显式写出这三个函数的。都是默认的行为。
第二种情况:
基类使用new,派生类使用new:
两者都使用new,则必须都提供显式复制构造函数、赋值运算符函数和析构函数。
1.派生类析构函数自动调用基类的析构函数,故其自身的职责是对派生类构造函数执行工作的清理。
过程是:先清理掉自身新增的成员内存,再调用基类析构函数清理掉继承的基类组件内存。
2.派生类显式复制构造函数在函数体中完成新增指针成员的地址和内容的赋值,对于继承的基类组件如何赋值呢???
使用成员初始化列表复制基类组件部分----可以将派生类对象赋给基类复制构造函数;虽然基类构造函数接受的是基类对象的引用:派生类对象也是基类对象嘛。
3.派生类赋值运算符函数:
同样,在函数体中完成自己的那一部分后,调用基类赋值运算符函数将派生类对象赋给它,完成基类部分的赋值。
总之,当基类和派生类都采用动态内存分配时,派生类的析构函数、复制构造函数、赋值运算符都必须使用相应的基类方法处理基类元素。
析构函数:自动完成的;
复制构造函数:成员初始化列表调用基类的复制构造函数; //传递对象是派生类对象,基类函数可以接收
赋值运算符函数:作用域解析运算符显示调用基类的赋值运算符。//传递对象是派生类对象,基类函数可以接收,这就是处理的基类组件部分。
补充:
派生类如何使用基类的友元:派生类的友元函数不是基类的友元函数,那派生类的友元函数如何访问基类成员呢?答案是使用基类的友元函数;
因为友元函数不是类成员函数,所以不能使用作用域解析运算符来指定要使用哪个函数-------
解决办法是使用强制类型转换,以便匹配原型时选择正确的函数。



  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值