《C++Primer笔记--13章-类继承》

面向对象的主要目的之一是提供可供重用的代码,C++可通过继承能从已有的类派生出新的类,而派生类继承类原有类的特征和方法。比如可在已有类添加功能、数据、修改类方法等。

13.1 一个简单的基类
从一个类派生出另一个类时,原始类为基类,继承类为派生类,下面设计个简单的TableTennisPlayer类。
在这里插入图片描述
在这里插入图片描述
该类的构造函数采用了成员初始化列表语法,效果和下面一样:
在这里插入图片描述
这里的构造函数的形参类型为常量引用,即const string & xxx,如果传入参数不是引用而是实例,则形参到实参会调用一次复制构造函数,参数声明为引用可提高效率。加上const关键字不会改变传入实例状态。

13.1.1 派生一个类
俱乐部的一些成员参加过乒乓球比赛,需要一个类包括成员的比赛分数,可将RatedPlayer声明为从TableTennisPlayer派生:

class RatedPlayer : public TableTennisPlayer
{
…
};

冒号指出RatedPlayer类的基类是TableTennisPlayer,上述特殊public表明TableTennisPlayer是一个公有基类,被称为共有派生,基类的公有成员将成为派生类的公有成员,基类的私有部分将成为派生类的一部分,但只能通过基类的公有和保护方法访问。
派生类对象特征:(1)存储了基类数据成员(2)可使用基类方法
派生类需要在继承的特性中添加(1)自己的构造函数(2)额外的数据成员和成员函数
13.1.2 构造函数:访问权限的考虑
创建派生类对象时,程序首先创建基类对象,C++使用成员初始化列表语法完成这种工作,下为RatedPlayer第一个构造函数:
在这里插入图片描述
其中TableTennisPlayer(fn,ln,ht)是成员初始化列表,他是可执行的代码,调用TableTennisPlayer构造函数,程序若声明:

RatedPlayer rplayer1(1140,”Mallory”,”Duck”,true);

则RatedPlayer构造函数把实参”Mallory”,”Duck”,true传递给形参fn,ln,ht,然后将这些参数传递给TableTennisPlayer构造函数,后者将创建一个嵌套TableTennisPlayer对象,并将数据”Mallory”,”Duck”,true存储在该对象中,之后程序进入RatedPlayer构造函数体,完成RatedPlayer对象的创建,并将参数r的值赋值给rating成员。

若省略成员初始化列表,程序将使用默认的基类构造函数,
在这里插入图片描述
即和下面代码等效:
在这里插入图片描述
除非要使用默认的构造函数,否则应显式调用正确的基类构造函数。
下面是第二个构造函数代码:
在这里插入图片描述
这里将TableTennisPlayer的信息传递给了TableTennisPlayer构造函数TableTennisPlayer(tp)
由于tp的类型为TableTennisPlayer&,因此将调用基类的复制构造函数,基类没有定义复制构造函数,会执行隐式复制构造函数。此外也可以对派生类成员使用成员初始化列表:
在这里插入图片描述
派生类构造函数的要点如下:
(1) 创建基类对象
(2) 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数
(3) 派生类构造函数英初始化派生类新增数据成员。
13.1.3使用派生类
要使用派生类程序必须能访问基类声明,程序清单将两种类声明放同一文件中。也可以将每个类放独立头文件中。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
13.1.4 派生类和基类之间的特殊关系
派生类和基类之间有一些特殊关系
(1) 派生类可以使用基类的方法,只要该方法不是私有的。
(2) 基类指针可以在不进行显示转换的情况下指向派生类对象。
(3) 基类引用可以在不进行显示类型转换的情况下引用派生类对象。
在这里插入图片描述
但基类指针或者引用只能用于调用基类方法,因此不能使用rt或pt调用派生类的方法。此外不可将基类对象和地址赋值给派生类引用和指针:
在这里插入图片描述
利用上面的特殊关系,引用兼容属性也可将基类对象初始化为派生类对象,或者将派生类对象赋值给基类对象。

13.2 继承:is-a关系
派生类和基类关系是基于C++继承的底层模型的,C++有三种继承方式:公有继承、私有继承、保护继承,其中公有继承最常用。
(1) 公有继承建立一种is-a关系,派生类属于基类的一种子类,比如香蕉派生于水果。
(2) 公有继承不建立has-a关系,即不建立包含关系,比如水果派生于午餐,但水果通常不作为午餐,只能说午餐有水果。
(3) 公有继承不能建立is-like-a关系,即不建立比喻关系,比如律师和鲨鱼。
(4) 公有继承不建立is-implemented-as-a关系,即不建立实现关系,比如栈派生于数组,但栈也可以由链表实现。
(5) 公有继承不建立use-a关系即不建立使用和被使用的关系,比如打印机派生于计算机也是不恰当的。
13.3 多态公有继承
当希望同一个方法在派生类和基类行为不同,需要实现多态公有继承,通常有两种方法:
(1) 派生类中重新定义基类方法
(2) 使用虚方法
现在需要开发两个支票账户类Brass 和Brass Plus,后者比前者增加了透支保护特性。现在考虑能否从Brass公有派生出Brass Plus?由于他们之间满足is-a的关系,所以是可以的。
13.3.1 开发Brass 和Brass Plus类
在这里插入图片描述
在上面的程序清单中:
(1) Brass Plus在Brass基础上增加了数据成员和成员函数
(2) Brass Plus和Brass都声明了ViewAcct()和Withdraw()方法,但它们的行为不同,基类版本限定名为Brass::ViewAcct(),派生类为Brass::Withdraw (),程序根据使用对象类型确定使用哪个版本
在这里插入图片描述
在这里插入图片描述
(3) Brass在声明ViewAcct()和Withdraw()时使用了关键字virtual,因此它们被称为虚方法。如果方法通过引用或指针类型而不是对象调用,如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了关键字virtual,程序将根据引用或指针指向的类型选择方法。
a. 如果ViewAcct()不为虚函数,则
在这里插入图片描述
b. 如果ViewAcct()为虚函数,则
在这里插入图片描述
两个引用类型都为Brass,但b2_ref引用的是Brass Plus对象。
(4) 基类声明了一个虚析构函数,是为了确保释放派生对象时,按正确顺序调用析构函数。
派生类不能直接访问基类私有数据,派生类的构造函数在初始化基类私有数据时,采用成员初始化列表访问;派生类的的非构造函数通过调用公有基类方法访问基类私有数据。
在派生类的代码中如果出现一个没有作用域运算符的方法,比如ViewAcct(),将默认为BrassPlus:: ViewAcct()。对于派生类没有重定义的方法则不用使用::运算符。
虚方法的使用:假设要同时管理Brass 和Brass Plus账户,由于使用了公有继承,可创建指向Brass的指针数组,既可以指向Brass对象,又可指向Brass Plus对象,这就是多态性。
在这里插入图片描述
在这里插入图片描述
可以看到上面程序中delete释放由free分配的对象,假设基类的析构函数不为虚,则只有Brass的析构函数被调用,即使指针指向的是Brass Plus对象。当基类虚构函数为虚,则会调用对象类型的析构函数。

13.4 静态联编和动态联编
将源码中的函数调用解释为执行特定的函数代码块被称为函数名联编,在编译过程中进行联编称为静态联编,又称早期联编。编译器生成能在程序运行时选择正取的虚方法的代码被称为动态联编或晚期联编。
13.4.1 指针和引用类型的兼容性
将派生类引用或指针转换为基类引用被称为向上强制转换,将基类指针或引用转换为派生类指针或引用称为向下强制转换,如果不使用显式类型转化,则向下强制转换是不允许的。
隐式向上强制转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编,C++使用虚成员函数满足这种需求。
13.4.2 虚成员函数和动态联编

如果基类没有将VIewAcct()声明为虚的,则bp->VIewAcct()会根据指针类型调用Brass:: VIewAcct(),指针类型在编译时已知,将VIewAcct()关联到Brass:: VIewAcct(),即编译器对非虚方法使用静态联编。
如果基类将VIewAcct()声明为虚,则bp->VIewAcct()会根据对象类型调用BrassPlus:: VIewAcct(),编译器在生成的代码将执行时根据对象类型将VIewAcct()关联到Brass:: VIewAcct()或者BrassPlus:: VIewAcct(),即编译器对虚方法使用动态联编。

1.存在两种类型联编原因?
(1) 效率:动态联编需要跟踪基类指针或引用指向类型,增加了开销,所以静态联编效率更高。当类不作为基类或者派生类不需要重新定义基类方法时,就不需要动态联编。
(2) 概念模型:设计类时可能包含一些不在派生类重新定义的成员函数,仅将预期被重定义的的方法声明为虚。

2.虚函数工作原理?
编译器处理虚函数的方法是:给每个对象添加一个指针,存放了指向虚函数表的地址的指针,虚函数表存储了为类对象进行声明的虚函数地址。比如基类对象包含一个指针,该指针指向基类所有虚函数的地址表,派生类对象将包含一个指向独立地址表的指针,如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址,如果派生类没有重新定义虚函数,该虚函数表将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址将被添加到虚函数表中,注意虚函数无论多少个都只需要在对象中添加一个虚函数表的地址。
在这里插入图片描述
调用虚函数时,程序将查看存储在对象中的虚函数表地址,转向相应的虚函数表,使用类声明中定义的第几个虚函数,程序就使用数组的第几个函数地址,并执行该函数。
使用虚函数后:
(1) 对象将增加一个存储地址的空间(32位系统为4字节,64位为8字节)。
(2) 每个类编译器都创建一个虚函数地址表
(3) 对每个函数调用都需要增加在表中查找地址的操作。

13.4.3虚函数的注意事项

  1. 总结前面的内容
    (1) 基类方法中声明了方法为虚后,该方法在基类派生类中是虚的。
    (2) 若使用指向对象的引用或指针调用虚方法,程序将根据对象类型来调用方法,而不是指针的类型。
    (3)如果定义的类被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚。
  2. 构造函数不能为虚函数。
  3. 基类的析构函数应该为虚函数。
  4. 友元函数不能为虚,因为友元函数不是类成员,只有类成员才能是虚函数。
  5. 如果派生类没有重定义函数,则会使用基类版本。
  6. 重新定义继承的方法若和基类的方法不同(协变除外),会将基类方法隐藏;如果基类声明方法被重载,则派生类也需要对重载的方法重新定义,否则调用的还是基类的方法。
    13.5 访问控制:protected
    关键字protected和privated类似,在类外只能使用公有类成员访问protected类成员,但对基类派生类成员则可以直接访问基类protected成员,但不能直接访问privated成员。
    最好对类成员采用私有访问控制,不采用保护访问控制哦,通过基类方法使派生类可访问基类数据。
    13.6抽象基类
    以圆和椭圆举例,如果从椭圆中派生出圆,则椭圆中很多属性和方法将不适用于圆,但二者之间有很多共性,因此将这些共性放入一个抽象基类中,然后再派生出圆和椭圆两个类。这样就可以用基类指针数组同时管理圆和椭圆两种对象了。对于基类中不能实现或者不使用的方法,可以使用纯虚函数(可以不定义),即在函数的声明后加上=0,如下:
    virtual double area() const = 0;
    当类中包含了纯虚函数,则不能建立该类的对象,只能把它用作基类。
    13.7 继承和动态内存分配
    13.7.1派生类不使用new

    假如基类使用了动态内存分配,而派生类不使用new,未包含特殊处理设计特性,则不需要派生类定义显示析构函数、复制构造函数和赋值运算符。因为派生类析构函数无需执行任何特殊操作;复制类成员或继承的组件是使用该类的复制构造函数完成的,派生类的默认复制构造函数使用显示基类的复制构造函数来复制派生类的基类部分;类的默认赋值运算符将自动使用基类的赋值运算符来对基类组件进行赋值。派生类的这些属性同样也适用于本身是对象的类成员。
    13.7.2派生类使用new
    这种情况下必须为派生类定义显示析构函数、复制构造函数和赋值运算符,因为派生类必须释放该类成员指针管理的内存(基类指针指向类存由派生类中包含的基类的析构函数释放)
    在这里插入图片描述
    派生类的复制构造函数只能访问hasDMA的数据,因此它必须调用baseDMA复制构造函数来处理共享的baseDMA数据:
    在这里插入图片描述
    在这里插入图片描述
    成员初始化列表将hasDMA引用传递给baseDMA构造函数,因为复制哦构造函数baseDMA有一个baseDMA引用参数,而基类引用可以指向派生类型,因此baseDMA复制构造函数将使用hasDMA参数的baseDMA部分来构造新对象的baseDMA部分。
    由于hasDMA也使用动态内存分配,因此它也需要一个显式赋值运算符。此外派生类hasDMA通过显式调用基类赋值运算符完成赋值。
    在这里插入图片描述
    在这里插入图片描述
    其中baseDMA::operator=(hs);通过函数表示法可以使用作用域解析运算符,该句代码含义如下:*this=hs;
    总之,当基类和派生类都采用动态内存分配时,派生类的析构函数、复制构造函数、赋值运算符都必须使用相应的基类方法处理基类元素。对于析构函数,将自动完成。构造函数是通过初始化成员列表中调用基类复制构造函数完成,对于赋值函数则是通过作用于运算符显式调用基类赋值运算符完成。

13.7.3 使用动态内存分配和友元的继承示例
派生类使用基类友元的方法:
(1) 如果该函数使用基类的友元函数访问基类的成员。
(2) 友元函数通过强制类型转换选择正确的函数。如代码将参数const hasDMA&转换成类型为const baseDMA&的参数:
在这里插入图片描述
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/HuYingJie_1995/article/details/88088932