C++ primer plus 第13章 类继承

面向编程的主要目的之一是提供可重用的代码。C++提供了更高层的重用性,目前,很多厂商提供了类库,类库由类声明和实现构成。因为类组合了数据表示和类方法。因此提供了比函数库更加完整的程序包。类库是以源代码的方式提供的,这意味着可以对其进行修改。而C++提供了比修改代码更好的方法来扩展和修改类——类继承。它能够从已有的类派生出新的类,而派生类继承了原有类(称为基类)的特征,包括方法。下面是可以通过继承完成的一些工作:(1)可以在已有类的基础上添加功能。(2)可以给类添加数据。(3)可以修改类方法的行为。

class RatedPlayer:public TableTennisPlayer
{
    //RatedPlayer类声明为从TableTennisPlayer类派生而来
}

派生类对象包含基类对象。使用公有派生,基类的公有成员将成为派生类的公有成员,基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。(1)派生类对象存储了基类的数据成员(派生类继承了基类的实现);(2)派生类对象可以使用基类的方法(派生类继承了基类的接口)。

(1)派生类需要自己的构造函数;(2)派生类可以根据需要添加额外的数据成员和成员函数。派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。必须使用基类的公有方法来访问私有的基类成员。具体的说,派生类构造函数必须使用基类构造函数。

必须首先创建基类对象,如果不调用基类构造函数,程序将使用默认的基类构造函数,除非要使用默认的构造函数,否则应显式调用正确的基类构造函数。有关派生类构造函数的要点:(1)首先创建基类对象;(2)派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数;(3)派生类构造函数应初始化派生类新增的数据成员。 

派生类与基类之间有一些特殊关系,其中之一是派生类对象可以使用基类的方法。条件是方法不是私有的,另外两个重要的盥洗室:基类指针可以在不进行显式类型转换的情况下指向派生类对象;基类引用可以在不进行显式类型转换的情况下引用派生类对象。

RatedPlayer rplayer1(1140,"Mallory","Duck",true);
TableTennisPlayer &rt=rplayer;
TableTennisPlayer *pt=&rplayer;
rt.Name();
pt->Name();

基类指针或引用只能用于调用基类方法。C++要求引用和指针类型与赋给的类型匹配,但这一规则对继承来说是例外。然而这种例外只是单向的,不可以将基类对象和地址赋给派生类引用和指针:

TableTennisPlayer player("Betsy","Bloop",true);
RatePlayer &rr=player;//NOT ALLOWED
RatedPlayer *pr=Player;//NOT ALLOWED

C++有3种继承方式:公有继承、保护继承和私有继承。公有继承:即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行。

1.多态公有继承

希望同一个方法在派生类和基类中的行为是不同的。换句话来说,方法的行为应取决于调用该方法的对象。这种较复杂的行为成为多态——具有多种形态,即同一个方法的行为随上下文而异。有两种重要的机制可用于实现多态公有继承:(1)在派生类中重新定义基类的方法。(2)使用虚方法(使用关键字visual)。

如果在派生类中重新定义基类的方法,通常应将基类方法声明为虚的,这样程序将根据对象类型而不是引用或指针的类型来选择方法版本。为基类声明一个虚析构函数也是一种惯例。

派生类构造函数在初始化基类私有数据时,采用的是成员初始化列表句法。非构造函数不能使用成员初始化列表语法,但派生类方法可以调用公有的基类的方法,在派生类方法中,标准技术是使用作用域解析运算符来调用基类方法。多态性是由下述代码提供的:

for(i=0;i<CLIENTS;i++)
{
    p_clients[i]->ViewAcct();
    cout<<endl;
}

虚析构函数:可以确保正确的析构函数序列被调用。

//如果ViewAcct()不是虚的
Brass dom("Dominic Banker",12118,2592.00);
BrassPlus dot("Dorothy Banker",12118,2592.00);
Brass &b1_ref=dom;
Brass &b2_ref=dot;
b1_ref.ViewAcct();//use Brass::ViewAcct()
b2_ref.ViewAcct();//use Brass::ViewAcct()
//如果ViewAcct()是虚的
Brass dom("Dominic Banker",12118,2592.00);
BrassPlus dot("Dorothy Banker",12118,2592.00);
Brass &b1_ref=dom;
Brass &b2_ref=dot;
b1_ref.ViewAcct();//use Brass::ViewAcct()
b2_ref.ViewAcct();//use BrassPlus::ViewAcct()

2.静态联编和动态联编

将源代码中的函数调用解释为执行特定的函数代码被称为函数名联编。静态联编(早期联编):在编译过程中进行联编;动态联编(晚期联编):编译器必须生成能够在程序运行时选择正确的虚方法的代码。编译器对非虚方法使用静态联编。编译器对虚方法使用动态联编。如果类不会用作基类,则不需要动态联编,如果派生类不重新定义基类的任何方法,也不需要使用动态联编。静态联编的效率更高。仅将那些预期将被重新定义的方法声明为虚的。

动态联编与通过指针和引用调用方法相关,将派生类引用或指针转换为基类引用或指针被称为向上强制转换,将基类指针或引用转换为派生类指针或引用——称为向下强制转换。派生类可以新增数据成员,因此使用这些数据成员的类成员函数不能应用于基类。对于使用基类引用或指针作为参数的函数调用,将进行向上装换,这里假定每个函数都调用虚方法ViewAcct():

void fr(Brass &rb);// uses rb.ViewAcct()
void fp(Brass *pb);// uses pb->ViewAcct()
void fv(Brass b);// uses b.ViewAcct()
int main()
{
    Brass b("Billy Bee",123432,10000.0);
    BrassPlus bp("Betty Beep",232313,12345.0);
    fr(b);// uses Brass::ViewAcct()
    fr(bp);// uses BrassPlus::ViewAcct()
    fp(b);// uses Brass::ViewAcct()
    fp(bp);// uses BrassPlus::ViewAcct()
    fv(b);// uses Brass::ViewAcct()
    fv(bp);// uses Brass::ViewAcct()
}

隐式向上强制转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编。C++使用虚成员函数来满足这种需求。

编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针,这种数组称为虚函数表——虚函数表中存储了为类对象进行声明的虚函数的地址。

使用虚函数时,在内存和执行速度方面有一定的成本,包括;(1)每个对象都将增大,增大量为存储地址的空间;(2)对于每个类,编译器都创建一个虚函数地址表(数组);(3)对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址。

虚函数要点:

(1)在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类中是虚的。

(2)如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。

(3)如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。

构造函数不能是虚函数,析构函数应当是虚函数,除非不用做基类。友元不能是虚函数,因为友元不是类成员,只有成员才能是虚函数。如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本。

重新定义将隐藏基类版本。如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。

3.访问控制:protected

在类外只能用公有类成员来访问protected部分中的类成员。private和protected之间的区别只有在基类派生的类中才会表现出来。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。

4.抽象类继承

C++通过使用纯虚函数提供未实现的函数。纯虚函数声明的结尾处为=0

5.继承和动态内存分配

(1)派生类不使用new,不需要显式析构函数、复制构造函数和赋值运算符

(2)派生类使用new,必须为派生类定义显式析构函数、复制构造函数和复制运算符。

当基类和派生类都采用动态内存分配时,派生类的析构函数、复制构造函数、赋值运算符都必须使用相应的基类方法来处理基类元素。

6.类设计回顾

(1)默认构造函数:要么没有参数,要么所有的参数都是默认值。如果没有定义任何构造函数,编译器将定义默认构造函数,让您能创建对象。自动生成的默认构造函数的另一项功能是,调用基类的默认构造函数以及调用本身是对象的成员所属类的默认构造函数。如果派生类构造函数的成员初始化列表中没有显式调用基类的构造函数,则编译器将使用基类的默认构造函数来构造派生类对象的基类部分。在这种情况下,如果基类没有构造函数,将导致编译阶段错误。如果定义了某种构造函数,编译器将不会定义默认构造函数。如果需要默认构造函数,则必须自己提供。提供构造函数的动机之一是确保对象总能被正确的初始化。另外,如果类包含指针成员,则必须初始化这些成员。最好提供一个显式默认构造函数,将所有的类数据成员都初始化为合理的值。

(2)复制构造函数:接受其所属类的对象作为参数,在如下情况将使用复制构造函数:(a)将新对象初始化为一个同类对象;(b)按值将对象传递给函数;(c)函数按值返回对象;(d)编译器生成临时对象。如果成员为类对象,则初始化改成员时,将使用相应类的复制构造函数。在某些情况下,成员初始化时不合适的。例如,使用new初始化的成员指针通常要求执行深度复制,或者类可能包含需要修改的静态变量。在上述情况下,需要定义自己的复制构造函数。

(3)赋值运算符:用于处理同类对象之间的赋值。默认赋值为成员赋值,如果成员为类对象,则默认成员赋值将使用相应类的赋值运算符。

(4)构造函数:构造函数不同于其他类方法,因为它创建新的对象,而其他类方法只是被现有的对象调用。这是构造函数不被继承的原因之一。继承意味着派生类对象可以使用基类的方法,然后,构造函数在完成其工作之前,对象并不存在。

(5)析构函数:一定要定义显式析构函数来释放构造函数使用new分配的所有内存,并完成类对象所需的任何特殊的清理工作,对于基类,即使它不需要析构函数,也应提供一个虚析构函数。

(6)转换:使用一个参数就可以调用的构造函数定义了从参数类型到类类型的转换。在带一个参数的构造函数原型中使用explicit将禁止进行隐式转换,但仍允许显示转换。

(7)编写使用对象作为参数的函数时,应按引用而不是按值传递对象——为了提高效率。按值传递对象涉及到生成临时拷贝,即调用复制构造函数,然后调用析构函数。调用这些函数需要时间,复制大型对象比传递引用花费的时间多得多,如果函数不修改对象,应将参数声明为const引用。按引用传递对象的另一个原因是,在继承使用虚函数时,被定义为接受基类引用参数的函数可以接受派生类。

(8)返回对象和返回引用:有些成员函数直接返回对象,而另一些则返回引用。有时方法必须返回对象,但如果可以不返回对象,则应返回引用。返回对象的时间成本包括调用复制构造函数来生成副本所需的时间和调用析构函数删除副本所需的时间。返回引用可节省时间和内存,直接返回对象与按值传递对象相似,它们都生成临时副本。同样返回引用与按引用传递对象相似:调用和被调用的函数对同一个对象进行操作。

有关使用基类方法的说明:

(1)派生类对象自动使用继承而来的基类方法,如果派生类没有重新定义该方法。

(2)派生类的构造函数自动调用基类的构造函数

(3)派生类的构造函数自动调用基类的默认构造函数,如果没有在成员初始化列表中指定其他构造函数。

(4)派生类构造函数显式地调用成员初始化列表中指定的基类构造函数。

(5)派生类方法可以使用作用域解析运算符来调用公有的和受保护的基类方法。

(6)派生类的友元函数可以通过强制类型转换,将派生类引用或指针转换为基类引用或指针,然后用该引用或指针来调用基类的友元函数。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值