90
继继继继继继继

继承与类的派生

Embed Size (px)

DESCRIPTION

继承与类的派生. 继承的概念. 目前,不加修改地直接复用已有软件比较困难。 已有软件的功能与新软件所需要的功能总是有差别的。解决这个差别有下面的途径: 修改已有软件的源代码, 它的缺点是: 需读懂源代码 可靠性差、易出错 源代码难以获得 继承( Inheritance ):在定义一个新的类时,先把一个或多个已有类的功能全部包含进来,然后再给出新功能的定义或对已有类的某些功能重新定义。. 根据软件需求从软件所要模拟的现实世界中 抽象 出 组成软件系统的 对象类 是面向对象程序设计的 基础 。 面向对象的 封装性 使这些 对象类 的属性和行为细节得 - PowerPoint PPT Presentation

Citation preview

Page 1: 继承与类的派生

继承与类的派生

Page 2: 继承与类的派生

继承的概念• 目前,不加修改地直接复用已有软件

比较困难。已有软件的功能与新软件所需要的功能总是有差别的。解决这个差别有下面的途径:– 修改已有软件的源代码,它的缺点是:

• 需读懂源代码• 可靠性差、易出错• 源代码难以获得

– 继承( Inheritance ):在定义一个新的类时,先把一个或多个已有类的功能全部包含进来,然后再给出新功能的定义或对已有类的某些功能重新定义。

Page 3: 继承与类的派生

根据软件需求从软件所要模拟的现实世界中抽象出

组成软件系统的对象类是面向对象程序设计的基础。

面向对象的封装性使这些对象类的属性和行为细节得

到了合理的保护和隐藏,并为类对象之间的通讯(方

法调用)提供了安全方便的接口。

在封装性的基础上,面向对象的继承性允许一个对

象类包含另一个或几个对象类的属性和行为,并使它

们成为自己的属性和行为,充分地反映了现实世界中

对象类之间的层次结构,为程序的代码重用提供了方

便、有效的实现机制。

Page 4: 继承与类的派生

在面向对象程序设计中,借助继承性的实现方法,

允许在既有类的基础上定义新类。被定义的新类可以

从一个或多个既有类中继承属性和行为,并允许重新

定义这些既有类中原有的属性和行为,还允许为新类

增加新的属性和行为,从而形成了类的建造层次。

既有类被称为基类或父类

新类被称为派生类、导出类或子类。 C++ 分为:单一继承:一个派生类仅有一个基类;多重继承:一个派生类有两个或两个以上的基类。

Page 5: 继承与类的派生

• 图中的箭头是从派生类指向基类。– 人类:描述人的共有特征 (如编号、姓名、年龄、性别等)。

– 学生类:单一继承“人类”,并增加描述学生特征的信息(如专业和课程等)。

– 职工类:单一继承“人类”,并增加描述职工特征的信息(如工作部门、所教课程等)。

– 在职研究生类:多重继承“学生类”和“职工类”。

– 类族:人类、学生类、职工类、在职研究生类。

Page 6: 继承与类的派生

派生类的概念

继承是对象类之间的一种包含关系,这种包含关系

是通过对象类的建造层次关系实现的。因此,具有继

承关系的类之间必定拥有以下基本性质:

① 类间的共享特性;

② 类间的细微区别;

③ 类间的层次结构。

Page 7: 继承与类的派生

例如: 简单的汽车分类图

汽车

运输汽车 专用汽车

客车 货车 消防车 洒水车

Page 8: 继承与类的派生

使用继承的必要性 试想如果组成一个系统的对象类均为互不包含的独

立对象类,则将不可避免出现对象属性和行为的重复冗余,并且这种无层次关系的对象类既不符合现实世界的对象关系,也使对象类的定义、创建、使用和维护复杂化。继承为代码重用和建立类定义的层次结构提供方便

有效的手段。例如在一个公司的管理软件设计中需要定义一个客

户类 Customer 和雇员类 Employee :

Page 9: 继承与类的派生

class Customer

{

private:

char name[15]; // 姓名

int age; // 年龄

char sex[8]; // 性别

double income; // 收入

public:

void print(); // 显示输出状态

};

Page 10: 继承与类的派生

class Employment

{

private:

char name[15]; // 姓名

int age; // 年龄

char sex[8]; // 性别

char department[20]; // 部门

double salary; // 工资

public:

void print(); // 显示输出状态

};

Page 11: 继承与类的派生

比较两个类的定义,不难发现,两个类的数据成员和成员函数有许多相同之处。显然,如此定义两个类,造成的代码重复是不可避免的。 如果将 Customer 和 Employee 类定义中的相同成员

抽取出来,定义一个新类 Person : class Person {

private:

char name[15]; // 姓名int age; // 年龄char sex[8]; // 性别

public:

void print(); // 显示输出状态 };

Page 12: 继承与类的派生

Customer 和 Employee 都定义为 Person 的派生类,那些在 Person 中已经定义的共同数据成员在

Customer

和 Employee 中就不需要再定义了,只需要在各自的定

义中增加自己的独有数据成员;而成员函数 print 也只

需要在 Person 所定义的行为操作基础上重新定义自己

的行为操作。class Customer : public Person

{

private:

double income; // 收入public:

void print(); // 显示输出状态};

Page 13: 继承与类的派生

class Employee : public Person

{

private:

char department[20]; // 部门

double salary; // 工资

public:

void print(); // 显示输出状态

};

显然通过继承可以从基类 Person 派生出一组具有层

次结构的新类,构成一个公司管理系统的主要对象类

型。例如:

Page 14: 继承与类的派生

使用继承机制和方法设计和建造类定义的层次结构对于建立一个面向对象的软件系统是不可缺少的。返回

Person

Employee Customer Vendor

Salaried Hourly Partner Client

PartTime FullTime

Page 15: 继承与类的派生

单一继承• 格式:

class ClassName:【 Access 】 BaseClassName{

… // 派生类中新增成员,可为空};

规定基类成员在派生类中的访问权限,可取:• public:公有派生• private:私有派生• protected:保护派生• 缺省:private(对于结构体则为public)

派生类的类名

已定义的类名

Page 16: 继承与类的派生

例如:基类 Person 和派生类的定义class Person {

private:

char name[15]; // 姓名int age; // 年龄char sex[8]; // 性别

public:

void print(); // 显示输出状态};

Page 17: 继承与类的派生

class Customer : public Person

{

private:

double income; // 新增加的数据成员“收入”public:

void print(); // 重新定义基类的“显示输出状态”};

从形式上比较,派生类定义与非派生类定义的差别仅在于定义首行中由 “ :” 引出的派生表达式。其中:① 派生方式:指明派生类继承基类成员的方式,方式 的种类有 public 、 private 和 protected 。如果不指明方式名,则缺省指定派生方式为 private 。

② 基类名:指明派生类所继承的类。

Page 18: 继承与类的派生

派生类的构成 派生类的构成可以有下图示意:

派生类名基类的所有成员

派生类的新增加成员

Page 19: 继承与类的派生

例如整数链表类 list 的定义: class list // 链表类名超前声明

class node // 结点类定义

{

int val;

node *next;

public:

friend class list;

};

Page 20: 继承与类的派生

class list // 整数链表类定义

{

node *elems // 链表头指针

public:

list();

~list();

bool insert(int); // 在表头插入一个结点

bool deletes(int); // 从表中删除一个结点

bool contains(int); // 在表中查找一个结点

};

Page 21: 继承与类的派生

一个链表结构的整数集合可以看成是不含重复元素的特殊整数链表,因此整数集合类可以从整数链表类派生。整数集合类在继承了整数链表类的所有成员的基础上,需要新增加一个能指示集合中元素个数的数据成员,同时还需要重新定义整数链表类的插入操作insert ,禁止重复元素被插入。

class set : public list

{

int card; // 集合中的元素个数 public:

bool insert(int); // 重新定义插入函数…

}; 返回

Page 22: 继承与类的派生

派生类成员的访问属性 派生类成员的访问属性我们分为类内访问属性和类外访问属性两种情况讨论。

1 类内访问属性 由于派生类的成员分为继承的基类成员和自身的新 增成员两种,这两种成员的类内访问属性是有所区 别的。 ⑴ 基类成员的访问属性

基类成员在派生类定义中被访问的限定原则:① 私有成员:不允许被访问,与派生类从基类的继承方式无关。

② 公有成员:允许被访问,与派生类从基类的继承方式无关。

Page 23: 继承与类的派生

⑵ 新增成员(自己的)的访问属性 所有的新增成员均允许被访问,与新增成员被设定的访问属性(公有或私有)无关。

2 类外访问属性 类成员的类外访问是指在类对象定义域外访问对象 的成员。因此,派生类成员在类定义中声明的访 问属性确定了派生类成员的类外访问属性:

Page 24: 继承与类的派生

⑴ 基类成员的访问属性① 私有成员:不允许被访问,与派生类从基

类 的继承方式无关。② 公有成员:依据继承方式的不同,在基类

中 被设定的公有属性会发生不同的变化。 ·私有继承:基类的公有成员变为派生类的

私有成员,因此在类外不允许被访问。 ·公有继承:基类的公有成员在派生类中仍

保持公有属性,因此在类外允许被访问。 ⑵ 新增加成员的访问属性

类成员在类定义中被声明的访问属性确定了类成

员的类外访问属性。

Page 25: 继承与类的派生

class base

privatepublic

class derived2: public derived1

privatepublic

class derived1:base

privatepublic

OK

OK

NO

NO

NO

NO

derived2

类内类外

OK

NO

OK

NO

NO

NO

Page 26: 继承与类的派生

派生类的构造函数和析构函数 1 派生类的构造函数 与一般非派生类相同,系统会为派生类定义一个缺省(无参数、无显式初始化表、无数据成员初始化代码)构造函数用于完成派生类对象创建时的内存分配操作。但如果在派生类对象创建时需要实现以下两种操作或其中之一,就无法使用缺省构造函数完成。① 派生类对象的直接基类部分创建需要传递参数。② 派生类对象的新数据成员需要通过参数传递初值。为了满足上述对象创建操作的需要,就必须显式定义派生类构造函数。

Page 27: 继承与类的派生

派生类构造函数声明和定义的一般形式:

注意:① 构造函数名后面的参数表列中包含了初始化表中创 建对象的基类部分、新增数据成员和在函数体中为 新数据成员赋初始值所需要的全部参数。

构造函数名 (参数表列 );

类名 :: 构造函数名 (参数表列 ): 基类构造函数名 (参数表列 ),

新数据成员名 1(参数表列 ), …

新数据成员名 n(参数表列 )

{ 其他初始化代码 }

Page 28: 继承与类的派生

② 初始化表中创建对象的基类部分的表达式必须使用

基类构造函数名调用基类构造函数,而创建数据成

员表达式必须使用数据成员名调用数据成员类的构

造函数。

派生类构造函数的执行顺序:基类构造函数 对象成员 1

类构造函数派生类构造函数定义体

对象成员 n类构造函数

Page 29: 继承与类的派生

2 派生类的析构函数 与一般非派生类相同,系统会为派生类定义一个缺省(无数据成员的清理代码)析构函数用于完成派生类对象撤消时的内存回收操作。但如果在派生类对象撤消时需要对某些新增数据成员进行内存回收之前的清理操作(例如,指针数据成员所指向的动态内存的回收),就无法使用缺省析构函数完成。为了满足上述对象数据成员清理操作的需要,就必须显式定义派生类析构函数。析构函数的执行顺序:

派生类析构函数定义体

对象成员 n类析构函数 基类析构函数对象成员 1

类析构函数

Page 30: 继承与类的派生

• 例 派生类的构造函数和析构函数。#include<iostream>using namespace std;

class B1{protected:

int x;public:

B1(int x){ this - >x=x; cout<<"基类B1的构造函数! \ n"; }~B1( ){ cout<<"基类B1的析构函数! \ n"; }

};

class B2{protected:

int y;public:

B2(int y){ this - >y=y; cout<<"基类B2的构造函数! \ n"; }~B2( ){ cout<<"基类B2的析构函数! \ n"; }

};

Page 31: 继承与类的派生

class D: public B1,public B2{protected:

int z;public:

D(int x, int y, int z): B1(x),B2(y){ this - >z=z; cout<<"派生类D的构造函数! \ n"; }~D(){ cout<<"派生类D的析构函数! \ n"; }

};int main(void){ D d(1,2,3) ; return 0; }

程序运行结果:基类B1的构造函数!基类B2的构造函数!派生类D的构造函数!派生类D的析构函数!基类B2的析构函数!基类B1的析构函数!

• 基类构造函数的调用顺序:与继承基类的顺序有关与初始化成员列表中的顺序无关

• 说明派生类的对象:先调用各基类的构造函数,后执行派生类的构造函数。若某个基类仍是派生类,则这种调用基类构造函数的过程递归进行。

• 撤消派生类的对象:析构函数的调用顺序正好与构造函数的顺序相反。

Page 32: 继承与类的派生

• 派生类含对象成员:其构造函数的初始化成员列表既要列举基类成员的构造函数,又要列举对象成员的构造函数。

• 例 派生类中包含对象成员。#include<iostream>using namespace std;

class B1{protected: int x;public:

B1(int x){ this - >x=x; cout<<"基类B1的构造函数! \ n"; }~B1( ){ cout<<"基类B1的析构函数! \ n"; }

};

class B2{protected: int y;public:

B2(int y){ this - >y=y; cout<<"基类B2的构造函数! \ n"; }~B2( ){ cout<<"基类B2的析构函数! \ n"; }

};

Page 33: 继承与类的派生

class D: public B1,public B2{int z;B1 b1,b2;

public:D(int x, int y, int z): B1(x),B2(y) , b1(2),b2(x+y)

{ this - >z=z; cout<<"派生类D的构造函数! \ n"; }~D( ){ cout<<"派生类D的析构函数! \ n"; }

};int main(void){ D d(1,2,3) ;

return 0;}

• 对象成员的构造函数的调用顺序与对象成员的说明顺序有关,而与其在初始化成员列表中的顺序无关。

• 对象成员的初始化必须使用对象名

• 基类成员的初始化必须使用基类名

• 程序运行结果:基类B1的构造函数!基类B2的构造函数!基类B1的构造函数!基类B1的构造函数!派生类D的构造函数!派生类D的析构函数!基类B1的析构函数!基类B1的析构函数!基类B2的析构函数!基类B1的析构函数!

• 问题:请写出产生上述输出结果的基类成员名或对象成员名。

• 从结果看:在创建类D的对象d时,先调用基类的构造函数,再调用对象成员的构造函数,最后执行派生类的构造函数。

Page 34: 继承与类的派生

几点讨论:1 如果派生类构造函数定义中无显式初始化表,则意 味着派生类对象的基类部分创建时,调用基类构造 函数无须参数;新增数据成员创建时,调用相应数 据类构造函数也无须参数。因此,如果基类和相应 的数据类没有定义无参数或有缺省参数值的构造函 数,将会导致编译错误。由此可见,一般情况在类 的定义中保留一个无须传递参数的构造函数是十分 必要的,除非需要禁止无参数创建类的对象。

Page 35: 继承与类的派生

无显式初始化表的派生类构造函数的一般形式:

系统的缺省构造函数是这种形式的一个特例,即无

参数,无显式初始化表和空定义体的类构造函数。

类名 :: 构造函数名 (参数表列 )

{ 新增数据成员赋初始值代码 }

类名 :: 构造函数名 ()

{ }

Page 36: 继承与类的派生

2 一般情况下,类数据成员的赋初始值操作均可以在 数据成员创建(分配内存)的同时进行,因此可以 通过初始化表同时完成数据成员的创建和赋初始值 操作。在这种情况下,如果对数据成员不需要其他 创建之后的初始化操作,就可能出现具有空定义体 的构造函数。 具有空定义体的构造函数的一般形式:

类名 :: 构造函数名 (参数表列 ): 基类构造函数名 (参数子表列 ),

新数据成员名 1(参数子表列 ), …

新数据成员名 n(参数子表列 )

{ }

Page 37: 继承与类的派生

3 在多层次派生类构造函数的初始化表中的基类部分

表达式一般只涉及直接基类和新增数据成员的创建

和初始化操作,而间接基类的创建和初始化操作则

由直接基类的构造函数定义完成。这种分层次的构

造定义有利于简化程序编码和提高源代码的可读

性。当然,在某些特殊情况下,为了满足某种特定

要求,也允许在派生类构造函数的初始化表中对间

接基类部分进行必要的创建和初始化操作,但不提倡滥用。

Page 38: 继承与类的派生

对派生类成员访问属性的进一步讨论 前面我们已经对派生类成员的基本访问属性进行了讨论,从讨论中我们发现,要使派生类与继承的基类成员更加 “无缝” 结合、更加灵活可控地继承、有两个问题还需要进一步讨论并加以解决。这两个问题是:⑴ 基类私有成员在派生类中不可直接访问性与派生类 新增成员函数需要能直接访问基类私有成员提高行 为操作效率和灵活性之间的矛盾。⑵ 继承方式对基类成员的设定访问属性修改的局限性 与派生类期望能更加灵活、可控制地从基类继承之 间的矛盾。

Page 39: 继承与类的派生

保护成员与保护继承1 类成员的保护访问属性 解决基类私有成员在派生类中只能通过基类的接口 (公有成员函数)访问而不允许直接访问的思路 是:在不破坏派生类封装性的前提下,“突破”基类的封装边界。解决的方法之一是增加一种新的类成员访问属性 —— 保护访问属性: ⑴ 一般形式: protected 类型名 数据成员名 ;

protected 类型名 成员函数名 (参数表列 );

⑵ 访问权限:可以在类内和派生类内被访问,而在 类外和派生类外不允许被访问。

Page 40: 继承与类的派生

⑶ 访问权限的继承:① 私有派生:基类的保护成员在派生类中将

变 成私有成员。② 公有派生:基类的保护成员在派生类中保

持 保护访问属性。

具有保护访问属性的类成员称为保护成员。将派生 类需要直接访问的基类私有成员定义为基类保护成 员,既可以提高这些基类成员在派生类内的访问效 率和方便性,又保持了这些类成员在派生类外不能 被直接访问的数据隐藏性。

Page 41: 继承与类的派生

2 类派生的保护继承方式 类派生的继承方式的作用是确定了基类成员被继承 到派生类中成为派生类成员时,其访问属性被限定 修改的规则。增加保护继承方式的目的是使派生类 成员的类外访问属性与私有继承方式相同,而当派 生类被再次派生时,直接访问间接基类成员提供可 能性。 ⑴ 一般形式:

class 派生类名 : protected 基类名{ 类成员定义代码 };

Page 42: 继承与类的派生

⑵ 基类成员访问属性修改规则:① 私有成员:与公有继承方式和私有继承方

式 相同,在派生类内外均不允许被访问。② 保护成员:基类的保护成员在派生类中保

持 保护访问属性。③ 公有成员:基类的公有成员在派生类中变

为 保护成员。

下 面 用 图 表 来 归 纳 和 描 述 基 类 的 private , protected

和 public 三种类成员在以 private , protected 和 public

三种继承方式派生的新类中的访问属性的变化。

Page 43: 继承与类的派生

⑴ 私有派生方式继承

protected:

public:

interCode

NameAddressAreaCodephone

Person()~Person()Person inputPerson()void prPerson()

protected:

public:

NameAddressAreaCodePhonePerson inputPerson ()void prPerson()department

yrsWork

Employee()~Employee()int testYears()

class Person class Employee:private Person

Page 44: 继承与类的派生

⑵ 保护派生方式继承

protected:

public:

interCode

NameAddressAreaCodephone

Person()~Person()Person inputPerson()void prPerson()

protected:

public:

NameAddressAreaCodePhonePerson inputPerson ()void prPerson()custBalance

Costomer()~Costomer()void PrtCust()

class Person class Costomer:protected Person

custNum

Page 45: 继承与类的派生

⑶ 公有派生方式继承

protected:

public:

interCode

NameAddressAreaCodephone

Person()~Person()Person inputPerson()void prPerson()

protected:

public:

NameAddressAreaCodePhonevendOwed

Vendor()~Vendor()Person inputPerson ()void prPerson()void PrtVend()

class Person class Vendor:public Person

vendNum

Page 46: 继承与类的派生

• 例 公有继承。#include<iostream>using namespace std;

class Point { // 三维直角坐标点类float x;

protected:float y ;

public:float z;

Point(float x,float y,float z){ this ->x=x; this ->y=y; this - >z=z; }void Setx(float x){ this - >x=x; }void Sety(float y){ this - >y=y; }float Getx ( ){ return x; }float Gety ( ){ return y; }void ShowP( ){ cout <<'('<<x<<','<<y<<','<<z<<')'; }

};

• 保护成员具有双重作用:对于派生类,它是公有的对于其外部,它是私有的

Page 47: 继承与类的派生

class Sphere:public Point{float radius;// 球的半径

public:Sphere(float x,float y,float z,float r) :Point(x,y,z)

{ radius=r; }void ShowS( ){ cout<<'('<<Getx ( ) <<','<<y<<','<<z<<"),"<<radius<<'\ n'; }

};

int main(void){ Sphere s(1,2,3,4);

s.ShowS( );cout<<'('<<s.Getx ( ) <<','<<s.Gety ( )<<','<<s. z<<")\n";return 0;

}

• 基类成员的初始化:在派生类的构造函数中用成员初始化列表调用基类的构造函数。

• 在公有派生类Sphere内:直接访问基类的保护成员y和公有成员z,但不能直接访问基类的私有成员x。若将Getx() 改为x,则将出现编译错误。

• 在公有派生类Sphere外:派生类对象s只能访问类中的公有成员如Getx( ) 、Gety( ) 和z。但不能访问类中的私有成员如x、radius和保护成员如y。

Page 48: 继承与类的派生

私有派生

• 基类中公有成员和保护成员在私有派生类中均变为私有的,在派生类中仍可直接访问,但在派生类之外均不可直接访问。

• 基类中的私有成员在私有派生类中不可直接访问,当然在派生类之外,更不直接访问。

Page 49: 继承与类的派生

• 例 私有继承。#include<iostream>using namespace std;

class Point { // 三维直角坐标点类float x;

protected:float y;

public:float z;

Point(float x,float y,float z){ this ->x=x; this ->y=y; this ->z=z; }void Setx(float x){ this - >x=x; }void Sety(float y){ this - >y=y; }float Getx ( ){ return x; }float Gety ( ){ return y; }void ShowP ( ){ cout <<'('<<x<<','<<y<<','<<z<<')'; }

};

Page 50: 继承与类的派生

class Sphere:private Point{// 私有继承Point类,派生Sphere类float radius; // 球的半径

public:Sphere(float x,float y,float z,float r):Point(x,y,z)

{ radius=r; }void ShowS( ){ cout<<'('<<Getx( ) <<','<<y<<','<<z<<"),"<<radius<<'\ n'; }

};

int main(void){ Sphere s(1,2,3,4);

s.ShowS( ) ;return 0;

}

• 在私有派生类Sphere内:基类的公有和保护成员在派生类中均变为私有,仍可直接使用,如y和z。

• 在私有派生类Sphere外:派生类对象s只能直接访问添加的公有成员ShowS( ) ,而不可直接访问基类的公有成员Getx( ) 、Gety( ) 和z等。

Page 51: 继承与类的派生

保护继承

• 基类的公有成员和保护成员在派生类中均变为保护的,在派生类中可直接使用,但在派生类之外均不可直接使用。

• 基类的私有成员,在派生类中不可直接使用,只能通过基类的公有或保护成员函数间接使用它们。当然在派生类之外,更是不能直接使用基类中的私有成员。

Page 52: 继承与类的派生

• 例 保护继承。#include<iostream>using namespace std;

class Point { // 三维直角坐标点类float x;

protected:float y;

public:float z;

Point(float x,float y,float z){ this - >x=x; this - >y=y; this - >z=z; }

void Setx (float x){ this - >x=x; }void Sety (float y){ this - >y=y; }float Getx ( ){ return x; }float Gety ( ){ return y; }void ShowP ( ){ cout <<'('<<x<<','<<y<<','<<z<<')'; }

};

Page 53: 继承与类的派生

class Sphere:protected Point{ // 保护继承Point类,派生Sphere类float radius; // 球的半径

public:Sphere(float x,float y,float z,float r):Point(x,y,z)

{ radius=r; }void ShowS( ){ cout<<'('<<Getx ( ) <<','<<y<<','<<z<<"),"<<radius<<'\ n'; }

};

int main(void){ Sphere s(1,2,3,4);

s.ShowS( );return 0;

}

• 在保护派生类Sphere内:基类的公有和保护成员在保护派生类中变为保护成员,在派生类中可直接使用,例如y和z。

• 比较:两例的继承方式不同,但对派生类的使用方法却是同样的。而私有继承和保护继承的区别,在派生类作为新的基类,继续继承时才能表现出来。

Page 54: 继承与类的派生

• 实际编程中,公有派生用得最普遍,私有派生和保护派生用得较少。

• 在派生类内外,对继承基类成员的访问权限,如表 所示。

Page 55: 继承与类的派生

派生友元类 如果希望基类的私有成员只在派生类中能被直接访问,而不希望这种直接被访问的属性从派生类向下一层次的派生类中延续,则在基类定义中将要派生的类声明为基类的友元,即从基类派生友元类。当然,也可以将基类的私有成员定义为保护成员,然后使用私有继承方式定义派生类的方法得到相同效果。例如:

class set;

struct node

{

int val;

node *next;

};

Page 56: 继承与类的派生

class list

{

node *elems;

public:

friend class set;

};

class set : public list

{

int card;

public:

set operator + (set&); // 允许访问 list 的私有成员

set operator * (set&); // 允许访问 list 的私有成员};

Page 57: 继承与类的派生

访问域声明 所谓访问域声明是在私有继承方式定义的派生类中

对基类的公有成员和保护成员进行声明,调整它们在派生类中访问属性,使这些基类成员保持它们在基类定义中设定的访问属性。 显然,在保护继承方式定义的派生类中,访问域声明只对基类的公有成员有效,因为基类的保护成员在派生类中已经保持了基类定义中原有访问属性。而在公有继承方式定义的派生类中,访问域声明是没有意义的,因为基类的公有成员和保护成员在派生类中都保持了基类定义中原有访问属性。

Page 58: 继承与类的派生

使用访问域声明可以有效地控制在派生类外,基类

的某些公有成员可以被访问,而某些公有成员被隐

藏。还可以使派生类能够向下一层次的派生类有选

择地提供其基类的保护成员和公有成员。

对基类成员进行访问域声明必须遵守以下规则:

1 访问域声明仅能调整对基类成员名,而不能为基类

成员重新说明类型,即便所说明的类型与基类成员

的原有类型相同,也是不允许的。如果声明的是成

员函数,则声明的也只是函数名而不准带有参数。

例如:

Page 59: 继承与类的派生

class x

{

int a;

public:

int b;

int f(int i, int j);

};

class y : x

{

public:

int x::b; // 错误x::f(int i, int j); // 错误

};

Page 60: 继承与类的派生

正确的访问域声明如下: class y : x

{

public:

x::b; // 正确x::f; // 正确

};

2 访问域声明只能使基类的保护和公有成员在派生类 中保持它们在基类定义的设定的访问属性,而不能 改变基类的私有成员在派生类中的访问属性,任何 试图这样做的行为都被视为破坏封装性,是非法 的。例如:

Page 61: 继承与类的派生

class x

{

int a;

public:

};

class y : x

{

x::a; // 非法public:

};

Page 62: 继承与类的派生

3 访问域声明仅用于在派生类中保持基类(公有或保护)成员的原有访问属性,不允许修改它们的访问属性。也就是说,基类的保护成员只能在派生类的保护段中进行声明;而基类的公有成员只能在派生类的公有段中进行声明。例如: class x

{

int a;

protected:

int b;

public:

int c;

};

Page 63: 继承与类的派生

class y : x {

public:

x::b; // 错误 protected:

x::c; // 错误 };

正确的访问域声明应为: class y : x {

public:

x::c;

protected:

x::b;

};

Page 64: 继承与类的派生

4 在派生类中对基类的重载成员函数名的访问域声明 将调整基类中所有以该名命名的成员函数的访问属 性。例如: class x

{

public:

f();

f(int);

f(char*);

};

Page 65: 继承与类的派生

class y : x

{

public:

x::f;

};

在派生类中说明了 x::f 后,基类 x 中所有以 f

命名的

成员函数在派生类中都保持原有的公有访问属性。

若基类中的这些重载成员函数处在不同的访问域,

那么,在派生类中就不能进行访问域声明。例如:

Page 66: 继承与类的派生

class x

{

f(float);

protected:

f(double);

public:

f();

f(int);

f(char*);

};

Page 67: 继承与类的派生

class y : x

{

public:

x::f; // 错误 };

导致错误的原因: ⑴ f(float) 是基类私有成员,试图改变该成员函数的

私有访问属性是绝对不允许的; ⑵ f(double) 是基类保护成员,试图改变该成员函数

的保护访问属性也是不允许的。

Page 68: 继承与类的派生

5 如果在派生类中具有与基类中同名的类成员,则基

类中的此成员不允许在派生类中进行访问域声明,否则将产生二义性错误。例如: class x

{

public:

f();

f(int);

f(char*);

};

Page 69: 继承与类的派生

class y : x

{

public:

void f(float);

x:f; // 二义性错误 };

返回

Page 70: 继承与类的派生

课后实验课后实验 11:: 一个远程网络中记录两台机器之间的平均传输时间如下表所示:

编写一个能按照网络通讯的出发地和目的地输

入、保存传输时间,并能按照上述表格形式输出

已经保存的网络通讯时间。

Beijing Shanghai Qingdao

Beijing 0 3.55 12.45

shanghai 2.34 0 10.31

Qingdao 15.36 9.32 0

Page 71: 继承与类的派生

1 问题分析: 使用矩阵结构保存二维表格数据是最为恰当的。但 由于本需求中二维表中的行、列位置的不是整数下 标而是字符串,所以描述该表格不能直接使用通用 矩阵类,而需要使用以字符串指示位置的特殊矩阵 类。其中,作为行、列下标的字符串可以通过向量 类将字符串映射为指示通用矩阵元素的整数下标。 显然该特殊矩阵是由向量和通用矩阵协同工作实现 的。为此,定义通用矩阵类 Matrix 用于完成通讯网 络表的基础操作,再定义向量类 AssocVec 用于将 通讯地址描述串转换为矩阵元素下标,而网络通讯 传输表类 Table 可以从这两个类派生。这些类之间 的关系如下图所示:

Page 72: 继承与类的派生

Matrix#rows:int#cols:int#elems:double*

+operator()(in r:int, in c:int):double&+Print(in r:int)+Print()

AssocVec#dim:int#used:int#elems:VecElem*

+operator[](in idx:char*):int&

VecElem+index:char*+value:int

0…n *elems

Table

+operator()(in src:char*, in dest:char*):double&+Print()

Page 73: 继承与类的派生

2 类的设计实现

⑴ 向量结构 VecElem :struct VecElem

{

char* index; // 索引字符串

int value; // 映射变量

};

Page 74: 继承与类的派生

⑵ 辅助向量类 AssocVec :class AssocVec

{

public:

AssocVec(int dim); // 构造函数 ~AssoVec(); // 析构函数 int& operator[] (char* idx);

// 重载下标运算符,通过索引字串访问映射。protected:

VecElem* elems; // 指向动态创建的向量数组的指针

int dim; // 向量数组的维数 int used; // 向量数组中被使用的元素个数};

Page 75: 继承与类的派生

下标运算符 AssocVec::operator [] (char* idx) 算法:

for i = 0 to used

索引字串复制到 elems[used+1].index使 elems[used+1].value = used+1 ,并返回 elems[used+1].value 的引用

返回值为 -1的静态哑变量引用,指示调用失败

elems[i].index = 参数指定字串?

Yes No

used < dim 并且为参数指定的 索引字串动态分配存储 空间成功?

Yes No根据参数指定的索引字串查询已有向量,返回匹配映射变量的引用

Page 76: 继承与类的派生

⑶ 通用矩阵类 Matrix :class Matrix {

public:

Matrix(int rows, int cols); // 构造函数 ~Matrix(); // 析构函数 double& operator()(short r, short c);

// 重载调用运算符,实现矩阵元素的访问

void print(short r); // 矩阵行显示操作 void print(); // 矩阵显示操作protected:

double* elems; // 指向动态创建的矩阵数组的指针

short rows, cols; // 二维矩阵的行、列索引} ;

Page 77: 继承与类的派生

调用运算符 Matrix::operator () (short r, short c) 的算

法:

返回参数指定的要访问的矩阵元素:elems[(r-1)*(c-1)] 的引用

返回静态哑变量的引用

参数指定的要访问的矩阵元素的 行、列索引值 r 和 c

是否合法?Yes No

Page 78: 继承与类的派生

⑷ 网络通讯传输表类 Table :

class Table : AssocVec, Matrix

{

public:

Table(short entries); // 构造函数

double& operator()(char* src, char* dest);

// 重载调用运算符,通过索引字串访问矩阵元素

Print();

} ;

Page 79: 继承与类的派生

调用运算符 Table::operator () (char* src, char*

dest)

的算法:调用 AssocVec::operator[] 将参数索引字串 src 和 dest 映射为整数索引 r 和 c

调用 Matrix::operator(r, c) 返回 r 和 c 确定的矩阵元素的引用

Page 80: 继承与类的派生

课后实验课后实验 22::用高斯消元法来求线性方程组。1 问题分析所谓高斯消元法就是通过线性方程组的系数矩阵对方程组进行一系列等价变换,使得变换后的系数矩阵为一个对角线元素均为 1 的三角矩阵,然后通过逐步回代,求得方程组的解。例如,下面的三元一次方程组:

2x + 4y + 5z = 55

-2x + 5y – 2z = 20

5x + 5y – z = 81

使用高斯消元法对该线性方程组的系数矩阵进行等价变换的过程和逐步回代求解的过程如下所示:

Page 81: 继承与类的派生

2 4 5 55

-2 5 -2 20

5 5 -1 81

5 5 -1 81

2 4 5 55

-2 5 -2 20

1 1 -0.2 16.2

2 4 5 55

-2 5 -2 20

1 1 -0.2 16.2

-2 5 -2 20

2 4 5 55

1 1 -0.2 16.2

0 7 -2.4 52.4

2 4 5 55

1 1 -0.2 16.2

0 7 -2.4 52.4

0 2 5.4 22.6

1 1 -0.2 16.2

0 1 -0.34 7.5

0 2 5.4 22.6

1 1 -0.2 16.2

0 1 -0.34 7.5

0 0 6.1 7.6

调整 1-3 行顺序使系数A11 在第 1列中最大

等价变换第 1 行使系数 A11 = 1

调整 2-3 行顺序使系数A22 在第 2列中最大

等价变换第 2 行使系数 A21 = 0

等价变换第 3 行使系数 A31 = 0

等价变换第 2 行使系数 A22 = 1

等价变换第 3 行使系数 A33 = 1

等价变换第 3 行使系数 A32 = 0

Page 82: 继承与类的派生

1 1 -0.2 16.2

0 1 -0.34 7.5

0 0 1 1.2

z = 1.2

y = 7.5+1.2*0.34 = 7.9

x = 16.2–7.9+0.2*1.2 = 8.5

回代求出方程组的解:

Page 83: 继承与类的派生

为实现上述操作功能,需要定义了矩阵类 matrix 作为对线性方程的系数矩阵进行操作的基类,它所提供的操作功能: 构造函数:根据指定的行和列构造相应的矩阵; 重载调用运算符 operator() :根据索引的行、列值, 引用相应的矩阵元素; 输出显示函数:格式显示矩阵的全部元素值。matrix

-rows:short-cols:short-elems:double

+operator ()(in rows:short, in cols:short):double&+print()

Page 84: 继承与类的派生

线性方程组类 lineqns 从 matrix 派生,主要操作有: 构造函数:用传递的方程个数和解进行初始化; 参数产生:产生方程组的各变量系数值和常量值, 从而构造方程组; 高斯求解函数:使用消元法求解方程组。

lineqns 和 matrix 派生关系:

lineqns-neqns:int-solution:double*

+generate(in coef:int)+solve()

matrix

lineqns

Page 85: 继承与类的派生

2 详细设计 ⑴ 类设计 ① matrix 类

ⅰ类定义:class matrix {

short rows, cols;

double *elems;

public:

matrix(short rows, short cols);

~matrix();

double& operator() (short row, short col);

void print();

};

Page 86: 继承与类的派生

② lineqns 类ⅰ类定义:class lineqns public matrix

{

int neqns;

double *solution;

public:

lineqns(int n, double *soln);

~lineqns()

void generate(int coef);

void solve();

};

ⅱ算法描述:

Page 87: 继承与类的派生

generate :用于产生方程的变元系数和常数generate(coef)

参数 coef 指定系数的值域范围BEGIN

计算系数的中值 mid = coef / 2;

for i = 1 to 方程个数 n, step = 1

设置方程组矩阵中的常数 (i, n+1) 的初值为 0;

for j = 1 to 变量个数 n, step = 1

计算系数 (i, j) = mid – rand() % coef;

计算常数 (i, n+1) += 系数 (i, j);

endfor

endfor

END

Page 88: 继承与类的派生

solve :高斯消元求解使用 N-S 流程图描述,图中的符号约定说明: diag 系数矩阵主对角线元素的行、列标识; piv 同列系数中最大元素值的行标识; neqns 方程组中的方程个数标识; r 行序号循环标识; c 列序号循环标识; factor 用于消去指定系数元的变换因子标识; print 显示系数矩阵的功能函数标识; soln 线性方程组的解矩阵标识; sum 求解过程中累加和标识。 a 系数标识。 C 常数标识。

Page 89: 继承与类的派生

for diag = 1 to neqns

piv = diag

for r = diag + 1 to neqns

r = r+1

系数 a(piv,diag)> 系数 a(r,diag)Yes No

piv = r

系数 a(piv,diag) ≠ 0Yes No

C = a(piv,neqns+1) ≠ 0Yes No

输出 " 方程组无解 " 输出 " 方程组有无穷解终止求解,并退出

piv ≠ diagYes No

交换第 diag 行和第 piv 行中存放的方程系数和常数第 diag 行中的系数和常数逐个除以系数 a(diag,diag) ,使系数 a(diag,diag)=1

for r = diag+1 to neqns factor = - 系数 a(r, diag), 使系数 a(r, diag) - a(r, diag) = 0 for c = diag+1 to neqns+1 系数 a(r, c) = 系数 a(r, c) – 系数 a(diag, c) * factor, c = c+1 r = r+1

调用(基类的) print 成员函数输出消元后的方程组系数矩阵

Page 90: 继承与类的派生

⑵ 类应用 main 函数的算法:

创建存放方程解的数组: soln = new double[neqns]

从系数矩阵直接获取最后一个变元的解: soln[neqns-1] = a(neqns,neqns+1)

for r = 1 to neqns

输出显示 soln[r]存放的变元的解

输入线性方程组的方程个数和相应的解

根据指定的方程个数和解,创建 lineqns 类对象 eqn

调用 eqn.generate 为方程组设置系数和常数

调用 eqn.print 输出显示所建方程组的系数矩阵

调用 eqn.solve 求解所建线性方程组

for r = neqns-1 to 1 sum = 0 for diag = r+1 to neqns

sum = sum + 系数 a(r, diag) * soln[diag-1]

soln[r-1] = 常数 C(r, neqns+1) - sum