第十三章 类继承
2020-02-24 10:40:44 11 举报
AI智能生成
C++ plus
作者其他创作
大纲/内容
类继承
前言
继承作用
在已有类的基础上添加功能
eg:对于数组类,可以添加数学运算
给类添加数据
修改类方法的行为
一个简单的基类
基类
代码TabletennisPlayer
using std::string;class TableTennisPlayer{private:\tstring name;\tbool hasTable;public:\tTableTennisPlayer(const string & n=\"none\
using std::cout;\tTabletennisPlayer player1(\"Ben\
说明
初始化列表
如果构造函数不用初始化列表会调用默认构造函数(string的)再用复制构造函数将name初始化为name
TabletennisPlayer player1(\"Ben\
可以将string对象或C-风格字符串作为构造函数TableTennisPlayer的参数。
派生类
代码
声明
格式
class RatedPlayer : public TableTennisPlayer{ ......}
表明TableTennisPlayer是一个公有基类。派生类对象包含基类对象。
公有派生:基类公有成员函数成为派生类的公有成员,基类的保护成员函数称为派生类的保护成员函数。私有部分称为派生类的一部分。(只能通过基类的公有保护方法访问)!!!
派生类对象特征
派生类对象存储了基类的数据成员
可以使用基类的方法。
不能继承的函数
构造函数(包括复制构造函数)
析构函数
赋值运算符
在继承的特性上需要
派生类需要自己的构造函数
派生类需要自己的析构函数
子主题
赋值函数(其实可有可无)//基类赋值函数不会继承
系统默认的会把基类部分调用基类的赋值函数
可以根据需要添加额外的数据成员和成员函数
派生构造函数
调用原理
创建派生类对象时,先创建基类对象(先调用基类对象的构造函数),再进入派生类的构造函数中。
一般做法
派生类的构造函数要给基类和派生类的新成员提供数据。基类用初始化列表的方法赋初值
如果省略成员初始化列表。程序先给基类调用默认的构造函数。(反正都要用到构造函数),派生类的构造函数总是调用一个基类的构造函数
对比类中含有其它类对象,也是这样子的!!!
引(成员初始化列表)
法1
class Test2{private: int a ; const int b;}
b是常量一定要这样做
法2
class Test1{ ...... }class Test2{private: Test1 t1;.......}
Test2::Test2( Test1 & tt ): t1( tt );
法3:如左//只能类继承吗?
好像是的
派生类对象过期时,先调用派生类对象的析构函数,再调用基类的析构函数。
除虚基类外,类只能将值传递回相邻的基类。//???
使用
TableTennisPlayer player1(\"Tara\
派生类和基类之间的特殊关系
关系
派生类可以使用基类的方法(除了私有的//怎么使用?使用方法?
class TableTennis{public: void f1(); void f2();}class RatePlayer:public TableTennis{public: void f1(); void ex();}//在派生类实现中void ex(){ f2(); TableTennis::f1();}
可以不带对象直接调用。如果基类与派生类有同名的函数,又需要用基类的函数。要有作用限定域。没有同名的基类函数,可直接调用。
基类指针可以(在不进行显式类型转换的情况下)指向派生类对象
基类引用可以(在不进行显式类型转换的情况下)引用派生类对象
例子
特点
基类指针(引用)只能用于调用基类的方法。//在没有任何虚函数的情况下
注意派生类指针(引用)不能指向(引用)基类
应用!
基类引用定义的函数或者指针参数可用于积累对象或者派生类对象//??看不懂,直接看例子把
void Show( const TableTennisPlayer & rt)//注意是引用或者指针。{ ...........}TableTennisPlayer player1(\"Tara\
将基类对象初始化为派生类对象
原理
赋值给player2时,调用复制构造函数 TableTennisPlayer(const TableTennisPlayer & )引用可以接受基类和派生类
将派生类对象赋给基类对象
ditto
继承is-a 关系
继承方式
公有继承
is -a 关系
保护继承
私有继承
is - a 关系含义
派生类是基类的一种类型派生类 is a kind of 基类
例子:基类:fruit香蕉类: banana
不能描述的关系
has a
派生类 has a 基类
is like a
is implemented as a( 作为....来实现 )
栈可以通过数组来实现,但是从数组中派生出栈不合适。
use a
计算机可以使用激光打印机,从Printer中派生Computer不合适
多态公有继承
含义
同一个方法在派生类和基类中的行为不同。取决于调用方法的对象。
方式(机制)
在派生类中重新定义基类的方法
使用虚方法
class Brass{private: ....public: ... virtual void Withdraw( double amt ); ... virtual void ViewAcct() const; ... virtual ~Brass() {}};class BrassPlus{private: ....public: ... virtual void Withdraw( double amt ); ... virtual void ViewAcct() const; ...\t ~BrassPlus();};
virtual void Withdraw( double amt );
在基类中的类声明函数前缀中 + virtual
一般也在派生类声明中 + virtual 表明是虚函数
作用
一般成员函数
不加virtual
方法通过常规变量调用
Brass dom(....);Brassplus dot(.....);dom.ViewAcct(); //Brass 的ViewAcct;dot.ViewAcct();// Brassplus 的ViewAcct;
根据对象的类型来调用类相关的方法
方法通过指针/引用调用
Brass & b1_ref = dom;Brass & b2_ref = dot;b1_ref.ViewAcct() ; //Brass::ViewAcct;b2_ref.ViewAcct() ; //Brass::ViewAcct;
根据指针/引用的类型来调用类相关的方法
加virtual
Brass & b1_ref = dom;Brass & b2_ref = dot;b1_ref.ViewAcct() ; //Brass::ViewAcct;b2_ref.ViewAcct() ; //BrassPlus::ViewAcct;
virtual ~Brass();~Brassplus();
如果不是虚
Brass * p1;p1=new BrassPlus(.....);delete p1;
结果
只会调用brass 的析构函数,不会调用BrassPlus 的析构函数
注意
如果BrassPlus包含一个执行某些操作的构造函数,则Brass必须有一个虚构函数,即使该析构函数不执行任何操作
使用(配合使用)
会根据p_clients 指向的类型来显示。
p_clients[i].ViewAcct( ) ; 根据对象分配的类型来显示。new 分配的类型
静态联编和动态联编
相关概念
联编
将源代码中的函数调用解释为执行特定的函数代码块
动态联编
在编译过程中进行联编
C中的函数
C++中的重载函数虽然复杂了,但是也是静态联编
静态联编
编译器生成能够在程序运行时选择正确的虚方法代码
虚方法
指针和引用类型的兼容性
与动态联编关系
动态联编与通过指针和引用调用方法相关,公有继承建立的is -a 关系的一种方法是如何处理指向对象的指针和引用某种程度上,动态联编由继承控制。
(一般)C++不允许将一种类型的地址赋给另一种类型的指针
允许将指向基类的引用和指针可以指向、引用派生类对象
向上强制转换
指向基类的引用和指针指向、引用派生类对象
不需要显式转换
void fr( Brass & rb );void fp( Brass * pb );voud fv( Brass pv ); int main(){ Brass b(....); BrassPlus bp( .... ); fr(b); fr(bp); fp(b); fp(bp); fv(b); fv(bp); //valid;}
按值传递只将 BrassPlus对象的Brass部分传递给函数 fv().
向下强制转换
指向派生类的引用和指针指向、引用基类对象
需要显式转换,也没有意义
Brass sally( ... );BrassPlus *ptr = &sally //invalidBrassPlus *ptr= ( BrassPluds *) sally; //valid
虚成员函数和动态联编
编译器对非虚函数——静态联编;虚函数——动态联编
默认联编方式
原因
效率
p503
概念模型
仅将在派生类中重新定义基类的方法,设置为虚方法,否则,设置为非虚方法
虚函数工作原理
给每个对象添加一个(指向函数地址数组的指针) 的隐藏成员。数组称为——虚函数表,存储为类对象进行声明的虚函数的地址。
图片p204
调用虚函数时,程序查看存储在对象中的vtbl地址,然后转向相应的函数地址表。
如果在基类定义了虚函数,在派生类没有重新定义,虚函数表保存函数原始版本的地址
(使用虚函数时的结果)总结
每个对象都将增大,增大量为存储地址的空间
对于每个类,编译器都创建一个虚函数地址表(数组)
对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址
虚函数的注意事项
构造函数
要求
不能为虚函数,因为派生类不继承基类的构造函数
析构函数应该为虚函数,除非类不用做基类
不是虚类问题
Employee *pe = new Singer;delete pe;//只释放Employee 部分指向的内存,不释放Singer组件指向的内存。
即使基类的析构函数不做任何事情,也需要提供一个虚类析构函数
给类定义一个虚析构函数,即使类不作为基类也没有问题,只是效率问题。
友元
不能是虚函数,只有成员才能是虚类。可以通过让友元函数使用虚成员函数解决问题。
没有重新定义
使用该函数的基类版本,如果派生类位于派生链中,将使用最新的虚函数版本。例外,基类版本是隐藏的。
重新定义将隐藏方法
class Dwelling {public: virtual void showpos(int a) const;....};class Hovel:public Dwelling{public: virtual void showpos() const;...}; Hovel trump; trump.showperks(); //valid trump.showperks(5); //invalid
重新定义不会生成函数的两个重载版本,隐藏了接受int 参数的基本版本。重新定义继承的方法不是重载,如果同名,(即使)参数列表不一样,会覆盖基类的声明
教训
1.
重新定义继承方法,确保与原来的原型完全相同,(参数、函数名、返回类型(小例外))
但如果返回类型是基类的引用或者指针,可以修改为指向派生类的引用或指针。
class Dwelling {public:\tvirtual Dwelling & bulid(int n);....};class Hovel:public Dwelling{public:\tvirtual Hovel & bulid(int n);...};
2
如果 基类声明被重载了,则应在派生类中重新定义所有的基类版本
问题
如果不设置为虚函数,这样可以吗?
不可以,因为是同名
//可以h1.Dwelling::show(); h1.show(3);
重载的话,要在同一个类中重载,不能一个在基类,一个在派生类
class Dwelling {public:....\tvirtual void showperks(int a) const;\tvirtual void showperks(double x) const;\tvirtual void showperks() const;\t....};class Hovel:public Dwelling{public:virtual void showperks(int a) const;\tvirtual void showperks(double x) const;\tvirtual void showperks() const;};
void Hovel::showperks() const { Dwelling::showperks() ;}
访问控制:protected
位置
外部世界
保护成员的行为与私有成员相似
对于派生类
保护成员的行为与公有成员相似
应用
数据成员作为保护对象
最好不要,会用问题。??什么问题,
最好对类数据成员采用私有访问控制,不要使用保护访问控制;同时通过基类方法使派生类能够访问类数据。
成员函数成为保护对象
保护访问控制很有用,让派生类能够访问公众不能使用的内部函数
抽象基类(ABC)
满足 is-a 关系,但是派生的类很笨拙,派生类不需要基类的一些数据,eg:圆(circle)属于特殊的椭圆(Ellipse),但从椭圆中直接派生出圆显得笨拙。
两个有关系的类分别定义显示不了他们的共性——把他们的共同点抽离出来,成为(圆、椭圆)的基类。
什么是抽象类?
声明了纯虚函数的类,称为抽象类。抽象类不能创建类对象。
纯虚函数
含义、作用
纯虚函数是在基类中只声明虚函数而不给出具体的函数定义体,将它的具体定义放在各派生类中,称此虚函数为纯虚函数.通过该基类的指针或引用就可以调用所有派生类的虚函数,基类只是用于继承,仅作为一个接口,具体功能在派生类中实现.
virtual 函数原型=0;
声明为纯虚函数的函数体可以不定义,也可以定义(主要作用为声明基类为抽象类)
抽象类特点
1.抽象类中可以有多个纯虚函数( 至少有一个 )2.不能声明抽象类的对象,但可以声明指向抽象类的指针变量和引用变量3.抽象类也可以定义其他非纯虚函数4.如果派生类中没有重新定义基类中的纯虚函数,则在派生类中必须再将该虚函数声明为纯虚函数5.从抽象类可以派生出具体或抽象类,但不能从具体类派生出抽象类6.在一个复杂的类继承结构中,越上层的类抽象程度越高,有时甚至无法给出某些成员函数的实现,显然,抽象类是一种特殊的类,它一般处于类继承结构的较外层7.引入抽象类的目的,主要是为了能将相关类组织在一个类继承结构中,并通过抽象类来为这些相关类提供统一的操作接口
抽象基类
类声明
class AcctABC{private: std::string fullName; long acctNum; double balance;protected: struct Formatting { std::ios_base::fmtflags flag; std::streamsize pr; }; const std::string & FullName() const {return fullName;} //返回私有成员数据 fullName long AcctNum() const {return acctNum;} //返回私有成员数据 acctNum Formatting SetFormat() const; //设置输出格式函数 void Restore(Formatting & f) const; //重设格式函数public: AcctABC(const std::string & s = \"Nullbody\
protect 中
把设置格式放在protected 中,派生类中可以使用,外部不可使用
把得到基类的私有数据放在protected 中,函数的方式得到。
类定义
virtual void Withdraw(double amt) = 0;virtual void ViewAcct() const = 0;
void AcctABC::Withdraw(double amt){ balance -= amt;}
取款粗略计算
virtual void ViewAcct() const = 0;//没有定义,定义留在派生类中
派生类1
class Brass :public AcctABC{public: Brass(const std::string & s = \"Nullbody\
void Brass::Withdraw(double amt){ if (amt < 0) cout << \"Withdrawal amount must be positive; \" << \"withdrawal canceled.\\"; else if (amt <= Balance()) AcctABC::Withdraw(amt); //引用基类的函数,因为是同名函数,所以有作用限定域 else cout << \"Withdrawal amount of $\" << amt << \" exceeds your balance.\\" << \"Withdrawal canceled.\\";}void Brass::ViewAcct() const{ Formatting f = SetFormat(); //引用基类函数,不是同名函数,所以不用,也不用通过对象去引用。 cout << \"Brass Client: \" << FullName() << endl; cout << \"Account Number: \" << AcctNum() << endl; cout << \"Balance: $\" << Balance() << endl; Restore(f);}
派生类2
class BrassPlus : public AcctABC{private: double maxLoan; double rate; double owesBank;public: BrassPlus(const std::string & s = \"Nullbody\
无
代码。还不与看电脑。
// usebrass3.cpp -- polymorphic example// compile with acctacb.cpp#include <iostream>#include <string>#include \"acctabc.h\"const int CLIENTS = 4;int main(){ using std::cin; using std::cout; using std::endl; AcctABC * p_clients[CLIENTS]; //建立基类,brass,bassplus都可以存储在里面。 std::string temp; long tempnum; double tempbal; char kind; for (int i = 0; i < CLIENTS; i++) { cout << \"Enter client's name: \
暂无
虚基类
问题产生一般情况
多继承或直接间接继承中,特别是在如下的模型中。
类 A 派生出类 B 和类 C,类 D 继承自类 B 和类 C
分析
在上述中,在D中m_a会产生两个副本,一个是来自B的m_a,一个是来自C的m_a在D中m_a没有说明是来自B还是C,这样也会有冗余的情况
改正
void seta(int a){ m_a = a; } 改为:void seta(int a){ B::m_a = a; / /C::m_a = a;}正确
解决
要使D类对象只产生一个m_a数据,将基类设置为虚基类,使得在派生类中只保留一份间接基类的成员。
void seta(int a){ m_a = a; } //可以
虚基类作用
见上解决
虚基类定义
观察这个新的继承体系,我们会发现虚继承的一个不太直观的特征:必须在虚派生的真实需求出现前就已经完成虚派生的操作。如上,当定义 D 类时才出现了对虚派生的需求。 换个角度讲,虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。
就是,D的多个继承中,若有其中两个类B,C继承同一 一个基类,需要把B,C的共同直接类A(派生时)设置为虚基类。这样A的数据成员就不会有多个副本。
继承和动态分配
class baseDMA{private: char * label; int rating; public: baseDMA(const char * l = \"null\
派生类不使用new
代码lackDMA
class lacksDMA :public baseDMA{private: enum { COL_LEN = 40}; char color[COL_LEN];public: lacksDMA(const char * c = \"blank\
用户自定义
调用的是基类的构造函数,所以可以
复制构造函数
调用的是基类的复制构造函数,所以可以
系统默认
默认复制构造函数
成员复制将根据数据类型采用相应的复制方式,复制继承的类的组件时,使用该类的复制构造函数完成的
默认析构函数
执行自身的代码后调用基类的析构函数。(先派生类的析构函数,再基类的析构函数)
赋值函数
自动使用类的赋值运算符来对基类组件进行赋值。
综上结论
不需要显式使用New 分配动态空间给基类的指针成员,如果不是需要,也不用显式定义构造函数,赋值函数。
派生类使用new
结论
需要显式定义复制构造函数,析构函数,赋值函数
只需处理派生类的动态分配成员即可。构造函数,复制构造函数,赋值函数必需使用相应的基类方法来处理基类函数析构函数,是自动完成的。
class hasDMA :public baseDMA{private: char * style; //要使用new的成员public: hasDMA(const char * s = \"none\
hasDMA::hasDMA(const hasDMA & hs): baseDMA(hs) // invoke base class copy constructor//直接把hasDMA传递给baseDMA
hasDMA::~hasDMA(){ delete [] style;}
baseDMA::operator=(hs); // copy base portion基类部分给基类的赋值构造函数处理即可.需要作用域解析运算符。
书中说相当于*this = hs; 不懂??
其他(友元)
派生类如何使用友元函数
基类中
lackDMA
hasDMA
在派生类中,只能访问派生类的数据成员,怎么输出基类的数据成员?
调用基类的友元函数。就是说,派生类也可以不通过对象调用基类的友元函数。
基类的友元函数不是成员函数,不能使用作用解析运算符指出使用哪个函数
使用强制类型转换法(如上),转换成基类的<<中接收的类型。
类设计回顾
对于基类,即使它不需要析构函数,也应提供一个虚析构函数(基类的析构函数最好是虚函数)
派生类赋值给基类
可以。
基类赋值给派生类
父节点
调用的是BrassPlus::operator= ( const BrassPlus & )函数但派生类引用不能自动引用基类对象,因此代码不能执行。
方法1
根据gp创建一个临时BrassPlus对象,然后用作赋值运算符参数。
方法2
定义一个用于将基类赋给派生类的赋值运算符BrassPlus &BrassPlus::operator=( const Brass & ){ .... }
传递的参数类型
前提
Brass/Brass plus的ViewAcct()已经声明为虚函数
BrassPlus buzz(....)show(buzz)inadequate(buzz);void show(const Brass & rha)//按引用传递对象{ rba.ViewAcct(); cout<<endl;}void inadequate(Brass ba)//按值传递对象{ ba.viewAcct(); cout<<endl;}
第一个函数,显示buzz的全部第二个函数,只显示buzz的brass部分
0 条评论
回复 删除
下一页