340 likes | 485 Views
第 11 章 类间关系的实现. 学习目的: ① 掌握类间关系的 C++ 实现; ② 了解多态性与虚函数的概念。. 11.1 一般 — 特殊关系的实现 11.2 多态性与虚函数 11.3 整体 — 部分关系的实现 11.4 关联关系的实现 11.5 关于类层次的总结. 11.1 一般 — 特殊关系的实现. 11.1.1 类的继承与派生 11.1.2 赋值兼容规则 11.1.3 两义性与作用域分辨.
E N D
第11章 类间关系的实现 学习目的: ① 掌握类间关系的C++实现; ② 了解多态性与虚函数的概念。 11.1一般—特殊关系的实现 11.2多态性与虚函数 11.3整体—部分关系的实现 11.4关联关系的实现 11.5关于类层次的总结
11.1 一般—特殊关系的实现 11.1.1 类的继承与派生 11.1.2 赋值兼容规则 11.1.3 两义性与作用域分辨 C++提供了描述一般—特殊关系的语法,在C++中称为类的派生或继承,通常分为单一继承和多重继承。在C++ 中常把一般—特殊关系中的一般类称为父类,而把特殊类称为子类。
矩形 长 宽 位置 正方形 边 长 求面积 求周长 求位置 求面积 求周长 求位置 11.1.1 类的继承与派生 派生类说明格式: class <DerivedClassName > : <AccessSpecifier> <BaseClassName> { … … }; 1. 单一继承 类体中的成员为子类所特有的数据成员和成员函数,虽然没有在子类中写明所继承的父类成员,但是父类成员在一定限制下属于子类。 派生类初始化构造函数格式如下: ClassName::ClassName(ArgList0) : DerivedClassName(ArgList1) { … … } [例11.1] 描述由矩形、正方形组成的平面图形系统 #include "iostream.h" class CRectangle //矩形类 { public: CPoint m_cpLocation; //图形所在位置 int m_nWidth; //图形的宽度 int m_nHeight; //图形的高度 CRectangle(int nX, int nY, int nWidth, int nHeight); int GetArea(); //求面积 int GetPerimeter(); //求周长 CPoint& GetPosition(); //返回图形位置 };
CRectangle:: CRectangle(int nX,int nY,int nW,int nH):m_cpLocation(nX,nY) { m_nWidth=nW; m_nHeight=nH; } int CRectangle::GetArea() { return m_nWidth*m_nHeight; } int CRectangle::GetPerimeter() { return m_nWidth+m_nWidth+m_nHeight+m_nHeight; } CPoint& CRectangle::GetPosition() { return this->m_cpLocation; } class CSquare : public CRectangle //正方形类,派生自矩形类 { public: CSquare(int nX, int nY, int nEdge); int GetEdge(); //返回边长 }; CSquare::CSquare(int nX, int nY, int nEdge) : CRectangle(nX, nY, nEdge, nEdge) { } int CSquare::GetEdge() { return m_nWidth; } void main() { CRectangle r(1,1,2,3); CSquare s(0,0,2); cout<<r.GetArea()<<endl<<s.GetArea()<<endl;
11.1.1 类的继承与派生 有两个因素同时控制着派生类对基类成员的访问权限,这两个因素就是基类类体中类成员的访问说明符,及派生类的派生方式。 2. 基类成员访问控制 基类的private成员将不被子类继承,且不能被子类成员访问。 private派生方式: 基类成员(private类除外)作为子类的private类 型成员。 public派生方式: 基类成员(private类除外)作为子类的相同类型 成员。 protected派生方式: 基类成员(private类除外)作为子类的protected 类型成员。
11.1.1 类的继承与派生 多重继承在C++中实现方式如下: class<ClassName0> : <AccessSpecifier1><ClassName1>, <AccessSpecifier2><ClassName2>, … <AccessSpecifiern><ClassNamen> { … … }; 3 多重继承 派生类构造函数应该调用所有基类的构造函数对基类数据成员进行初始化,格式如下: <ClassName0>::<ClassName0>(ArgList0) : <ClassName1>(ArgList1), … <ClassNamen>(ArgListn) { … … } 4 继承与派生示例 #include "iostream.h" #include "stdlib.h" #include "string.h" class CWnd; // 引用性说明 #define MAXTEXTBUFFER 0xffff class CPoint { private: int m_x; int m_y;
public: CPoint(int x=0, int y=0) { m_x=x; m_y=y; } int GetX() { return m_x; } int GetY() { return m_y; } }; class WNDSTRUCT { //本对象中含有窗口公共数据 protected: char* m_pczWndName; //窗口名字 //下述四个量表示窗口左上角和右下角的坐标 CPoint m_cpTopLeft; CPoint m_cpBottomRight; //下述三个量用于建立窗口系统的树结构 CWnd* m_pParentWindow; //指向本窗口的父窗口 CWnd** m_pChildFirst; //CWnd的指针数组,放着本窗口的子窗口 CWnd** m_pSiblingFirst; //指向本窗口的兄弟窗口 char* m_pEditTextBuffer; //指向窗口编辑区文本缓冲区 WNDSTRUCT(const WNDSTRUCT& rWndArch) { m_pczWndName=new char[strlen(rWndArch.m_pczWndName)+1]; if(m_pczWndName==0) { cout<<"No enough space!"<<endl; exit(0); } strcpy(m_pczWndName, rWndArch.m_pczWndName); }
~WNDSTRUCT() { delete[ ] m_pczWndName; } }; class CScreenObject : virtual private WNDSTRUCT { public: void MoveToWindow(const CPoint& cpWndPos, int nWidth, int nHeight) { HideWindow(); m_cpTopLeft=cpWndPos; m_cpBottomRight=CPoint(m_cpTopLeft.GetX()+nWidth, m_cpBottomRight.GetY()+nHeight); RedrawWindow(); } ~CScreenObject() { delete[ ] m_pczWndName; } void HideWindow() { /*隐藏当前窗口*/ } void RedrawWindow() { /*绘制并显示当前窗口*/ } };
class CEditText : virtual private WNDSTRUCT { public: CEditText(WNDSTRUCT& rWndArch) : WNDSTRUCT(rWndArch) { m_pEditTextBuffer=new char[MAXTEXTBUFFER+1]; if(m_pEditTextBuffer==0) { cout<<"No enough space"; exit(1); } memset(m_pEditTextBuffer, '\0', MAXTEXTBUFFER+1); } ~CEditText() { delete[ ] m_pEditTextBuffer; } void TextCut(int nStart, int nEnd) { char* TempStr=new char[MAXTEXTBUFFER-nEnd]; memset(m_pEditTextBuffer+nEnd, '\0', MAXTEXTBUFFER-nEnd); memcpy(m_pEditTextBuffer+nEnd, TempStr, MAXTEXTBUFFER-nEnd); delete[ ] TempStr; //此处应该调用适当函数重新绘制编辑区显示内容 } /* TextPaste(), TextCopy() 等函数 */ };
class CWindowTree : virtual private WNDSTRUCT { public: CWindowTree(WNDSTRUCT& rWndArch) : WNDSTRUCT(rWndArch) { } void AddChild(CWnd* pChild) { while(*m_pChildFirst!=0) ++m_pChildFirst; *m_pChildFirst=pChild; } /* 其他关于窗口树的操作 */ }; class CWnd : public CWindowTree, public CEditText, public CScreenObject { … };
11.1.1 类的继承与派生 例11.4 class CBase { public: int b0; CBase() { b0=0x01; } }; class CBase1 : public CBase { public: int b1; CBase1() { b1=0x11; } }; class CBase2 : public CBase { public: int b2; CBase2() { b2=0x12; } }; class CDerived : public CBase1, public CBase2 { public: int b; CDerived() { b=0x21; } }; void main() { CDerived obj; } 5. 派生类对象内存映像 CBase2 对象 CDerived 对象 CBase1 对象
11.1.2 赋值兼容规则 对于[例11.4]中的类,下列语句合法: CBase b; CBase1 b1; b=b1; 1. 派生类对象可以赋值给父类对象 2. 派生类的对象可以用于基类引用的初始化 对于[例11.4],下列语句合法: CBase1 b1; CBase& refBase=b1; 3. 派生类对象的地址可以赋值给指向基类的指针 对于[例11.4]下列语句合法: CBase1 b1; CBase *pBaseObj=&b1; 通过派生类CBasel对象的内存映像图可以看到这种赋值的物理意义。 CBase1对象 CBase对象
11.1.3 两义性与作用域分辨 如类的多个父类中具有相同名数据成员或成员函数,在引用该成员时可使用作用域分辨符 :: 来区分所引用的名字究竟属于哪个父类。 1. 作用域分辨 [例11.5] class CBase1 { public: void MyFunc() { cout<<"This is CBase1's MyFunc"<<endl; } }; class CBase2 { public: void MyFunc() { cout<<"This is CBase2's MyFunc"<<endl; } }; class CDerived : public CBase1, public CBase2 { public: void func() { MyFunc(); } //错误! 两义性! }; void main() { CDerived obj; obj.func(); } 显然,派生类CDerived中函数func()对父类成员函数MyFunc()的引用是具有二义性的,编译器无法判断所要调用的是哪一个父类的成员函数,因此相应的语句出现语法错误。解决这种错误的办法是在程序中使用作用域分辨符直接指明所要引用的是哪个类的MyFunc(),因此将派生类CDerived的定义改写如下: class CDerived : public CBase1, public CBase2 { public: void func() { CBase1::MyFunc(); //调用CBase1类的成员函数MyFunc() CBase2::MyFunc(); //调用CBase2类的成员函数MyFunc() } };
CBase CBase1 CBase2 CBase int b int b CDerived int func() 11.1.3 两义性与作用域分辨 如果类Y是类X的一个基类,则X中的成员name支配基类中的同名成员。如果在程序中要访问被支配的名字,可以使用作用域分辨符。 2. 支配规则 class A { public: int a(); }; class B : public virtual A { public: int a(); }; class C : public virtual A { … }; class D : public B, public C { public: D() { a();// 无二义性. B::a() 支配 A::a. } }; 从同一个类直接继承两次以上
11.1.3 两义性与作用域分辨 多个父类由同一类派生,创建对象时内存中会有爷爷类多个实例(例11.4)。可采用两种方式消除二义性,其一使用 ::,其二将爷爷类作为虚基类,使创建对象时内存中只有爷爷类的一个实例。 3. 虚基类 [例11.7] 派生类的两个父类具有一个共同的虚基类。 class CBase { public: int b0; CBase() { b0=0x01; } }; class CBase1 : public virtual CBase { public: int b1; CBase1() { b1=0x11; } }; class CBase2 : public virtual CBase { public: int b2; CBase2() { b2=0x12; } }; class CDerived :public CBase1, public CBase2 { public: int b; CDerived() { b=0x21; } }; void main() { CDerived obj; }
11.1.3 两义性与作用域分辨 3. 虚基类
11.2 多态性与虚函数 广义的多态性可以理解为一个名字具有多种语义。面向对象中的多态性是指不同类的对象对于同一消息的处理具有不同的实现,在C++中表现为同一形式的函数调用,可能调用不同的函数实现。 C++的多态性可分为两类,一类称为编译时刻多态性,另一类称为运行时刻多态性。与之相应的概念有静态联编(亦称静态绑定、静态集束、静态束定等)、动态联编(亦称动态绑定、动态集束、动态束定等)。 11.2.1 编译时刻的多态性 11.2.2 运行时刻的多态性 11.2.3 虚函数 11.2.4 纯虚函数与抽象类
11.2.1 编译时刻的多态性 函数重载为一种常见的编译时刻多态性,编译时通过参数类型匹配,定位所调用函数的具体实现,然后用该实现代码调用代替源程序中的函数调用。 [例11.9] 编译时刻多态性。 #include "iostream.h" const float PI=float(3.14); class CPoint { private: int m_x; int m_y; public: CPoint(int x=0, int y=0); void Area() { cout<<"Here is a point's area: " <<0<<endl; } }; CPoint::CPoint(int x, int y) { m_x=x; m_y=y; } class CCircle : public CPoint { private: float m_nRadius; public: CCircle(int x=0, int y=0, float r=0) : CPoint(x, y) { m_nRadius=r; } void SetRadius(float r) { m_nRadius=r; } void Area() { cout<<"Here is a circle's area: " <<PI*m_nRadius*m_nRadius<<endl; } }; void main() { CCircle c1; c1.Area(); }
11.2.2 运行时刻的多态性 运行时刻多态性的实现机制是动态联编,在程序运行时刻确定所要调用的是哪个具体函数实现,这种联编形式的程序运行效率低于静态联编,因为要花额外开销去推测所调用的是哪一个函数。虽然动态联编的运行效率低于静态联编,但是动态联编为程序的具体实现带来了巨大的灵活性,使得对变化万千的问题空间对象的描述变得容易,使函数调用的风格比较接近人类的习惯。 [例11.10] 运行时刻的多态性。 #include "iostream.h" const float PI=float(3.14); class CPoint { private: int m_x; int m_y; public: CPoint(int x=0, int y=0); void Area() { cout<<"Here is a point's area: "<<0<<endl; } }; CPoint::CPoint(int x, int y) { m_x=x; m_y=y; }
class CCircle : public CPoint { private: float m_nRadius; public: CCircle(int x=0, int y=0, float r=0) : CPoint(x, y) { m_nRadius=r; } void SetRadius(float r) { m_nRadius=r; } void Area() { cout<<"Here is a circle's area: "<<PI*m_nRadius*m_nRadius<<endl; } }; void main() { CPoint *p; CCircle c(0, 0, 2); p=&c; p->Area(); }
11.2.3 虚函数 类的一个成员函数被说明为虚函数表明它目前的具体实现仅仅是一种假设,只是一种适用于当前类的实现,在未来类的派生链条中有可能重新定义这个成员函数的实现 (override)。虚函数的使用方法如下: class <ClassName> { … vitual void MyFunction(); … }; void <ClassName>::MyFunction() { … } 当某一个成员函数在基类中被定义为虚函数,那么只要同名函数出现在派生类中,如果在类型、参数等方面均保持相同,那么,即使在派生类中的相同函数前没有关键字virtual,它也被缺省地看作是一个虚函数,但为保证风格统一,建议在派生类的虚函数前仍然添加关键字virtual。
11.2.3 虚函数 1 虚函数的实现 不同语言环境实现虚函数的机制不同,下面通过类CDerived的派生介绍Visual C++中如何实现虚函数。 (程序见备注)
11.2.3 虚函数 2 虚函数的使用 • 虚函数的实现机制和调用方式与非虚函数不同,因此虚函数的使用具有特殊性。 • 虚函数的访问权限派生类中虚函数的访问权限并不影响虚函数的动态联编,例如下面的程序实例[例11.11],其中派生类CDerived中重新定义了虚函数Func4(),在程序的运行中由于虚函数的机制,在CBase::Func3()中调用Func3()时会调用CDerived::Func3(),而该函数的访问权限是私有的。 • 成员函数中调用虚函数在成员函数中可以直接调用相应类中定义或重新定义的虚函数,分析这类函数的调用次序时要注意成员函数的调用一般是隐式调用,应该将之看做是通过this指针的显式调用,参见下例:
[例11.11] 在成员函数中调用虚函数。 #include "iostream.h" class CBase { public: void Func1() { cout<<"=> CBase::Func1=> "; Func2(); } void Func2() { cout<<"CBase::Func2=> "; Func3(); } virtual void Func3() { cout<<"CBase::Func3=> "; Func4(); } virtual void Func4() { cout<<"CBase::Func4=> out"<<endl; } };
class CDerived : public CBase { private: virtual void Func4() { cout<<"Derived::Func4=> out "<<endl; } public: void Func1() { cout<<"=> Derived::Func1=> "; CBase::Func2(); } void Func2() { cout<<"=> Derived::Func2=> "; Func3(); } }; void main() { CBase* pBase; CDerived dObj; pBase=&dObj; pBase->Func1(); dObj.Func1(); }
11.2.3 虚函数 3. 多重继承与虚函数 class CBase1 { public: virtual void MyFunc() { cout<<"CBase1::MyFunc"<<endl; } }; class CBase2 { public: virtual void MyFunc() { cout<<"CBase2::MyFunc"<<endl; } }; class CDerived : public CBase1, public CBase2 { public: virtual void MyFunc() { cout<<"CDerived::MyFunc"<<endl; } }; void main() { CBase1* pB1=new CDerived; CBase2* pB2=new CDerived; pB1->MyFunc(); pB2->MyFunc(); } 程序中指向父类CBase1、CBase2的指针pB1、pB2分别被赋予了派生类CDerived对象的地址,由于MyFunc( )函数为虚函数,因此通过两个指针调用该函数的结果是都调用了派生类的函数 CDerived::MyFunc( ) 。 采用这种方式能够使前期程序设计人员调用后期程序设计人员所实现的具体函数。
11.2.3 虚函数 3. 多重继承与虚函数 可以说明具有虚函数的虚基类,适当地使用这种方式能够提供作为父类的两个兄弟类实例之间的通信,是一种较好的通信方式 。 class CBase { public: virtual void MyFunc1() {} }; class CDerived1 : virtual public CBase { public: virtual void MyFunc1() { cout<<"CDerived1::MyFunc1"<<endl; } }; class CDerived2 : virtual public CBase { public: virtual void MyFunc2() { cout<<"CDerived2::MyFunc2"<<endl; MyFunc1(); } }; class CDerived : virtual public CDerived1, virtual public CDerived2 { }; void main() { CDerived dObj; dObj.MyFunc2(); }
11.2.3 虚函数 4. 虚析构函数 析构函数可以被说明为虚函数,利用虚析构函数,删除对象时不必考虑对象的类型(父类或子类),虚函数机制将保证调用适当的析构函数。 #include "iostream.h" #include "string.h" class CBase { public: virtual ~CBase() { cout<<"CBase::~CBase()"; } }; class CDerived : public CBase { public: virtual ~CDerived() { cout<<"CDerived::~CDerived()"<<endl; } }; void main() { CBase* pB=new CDerived; delete pB; }
11.2.4 纯虚函数与抽象类 软件系统的功能由类层次中的各类所实现,不同的类提供了相应层次的功能实现,通过类的用户接口可以调用这些功能,人们通常所习惯的不是将功能在不同类层次的实现用不同的接口表示,而是将概念上相似的功能用一个统一的接口在最顶层表示,例如:一个系统可能提供了“打印”这一功能,但“打印”对其各组成部分的含义不同,可能包括打印文本文件、打印照片、打印图形等,这些打印功能的具体实现由各个类提供,但对整个系统来讲它们应该具有相同的接口,在调用时应能够根据具体情况调用其具体实现。虚函数可以帮助我们做到这一点,若干概念上相似的操作可以用一个虚函数描述,该虚函数在较高层次上表示一种功能的接口,而在不同类中对该虚函数的重新定义就是该项功能不同层次上的实现,虚函数调用机制可保证虚函数的某个恰当的实现被调用。也就是说,利用虚函数,可以使系统中多个相似的功能具有统一的接口,改善了类的用户接口。方式如下: class <ClassName> { virtual <ReturnType> <FunctionName>(ArgList)=0; … };
CClass 班号:int m_nClassID 学生:CStudent* m_pStudents ...... CStudent 设 置 学 生 :SetStudent(CStudent*) 构造空班nID :CClass(int nID) ...... CClass 组号:int m_nGroupID 学生:CStudent* m_pStudents ...... 设 置 学 生 :SetStudent(CStudent*) 构造空组nID :CGroup(int nID) ...... 小组成员 5 25 班级成员 ...... ...... 11.3 整体—部分关系的实现 class CButton { … CButton() { …… } … }; class CIcon { … CIcon() { …… } … }; C++对整体—部分关系提供支持手段,对复合聚合,采用嵌入式对象的方式,即属性的类型为类,例如消息窗口类可以如下定义: class CStudent { … CStudent(const char* pStudentName) { … } … }; class CGroup { private: CStudent* m_pStudents; int m_nGroupID; …
public: void SetStudent(CStudent* pStudents) { m_pStudent=pStudent; } CGroup(int nID) { m_nGroupID=nID; m_pStudents=new CStudnet[5]; … //小组由5名学生组成 } … }; class CClass { private: CStudent* m_pStudents; int m_nClassID; … public: CClass(int nID) { m_nClassID=nID; m_pStudents=new CStudnet[35]; //班级由35名学生组成 … }
CStudent* GetStudent() { return m_pStudents; } void SetStudent(CStudent* pStudents) { m_pStudent=pStudent; } … … }; void main() { CStudent theWhole[2550]={ CStudent("Marry"), … , CStudent("Tom") }; CClass MyClass3(3); //成立班级,但具体由哪些学生组成尚未确定 MyClass3.SetStudent((theWhole+70)); //本班学生由学校学生名册中第71位开始的35人组成 CGroup Group6(6); Goup6.SetStudent((MyClass3.GetStudent()+25)); //第6学习小组由班级学生名册中第26名开始的5个人组成 … }
公 司 由...掌管 雇佣 名称:char* m_pName 老板:CPerson box ... ... 老板 0..1 为...工作 ... ... box * 人 工 作 单 位 姓名:char* m_pName 年龄:int m_nAge ... ... 妻子 ... ... 丈夫 结婚 11.4 关联关系的实现 同许多面向对象编程语言一样,对关联的实现C++没有提供专用语法,编程者可以使用指向类的指针、成员对象等语法实现分析设计阶段描述的关联结构,与实现整体-部分结构类似,实际上整体-部分结构在UML中是以关联特例的身份出现的。 class CCompany { private: char* m_pName; //按图中要求 CPerson* box; //图中标出老板可有可无,故用指针表示 CPerson m_Employee[20] //公司员工最多20 }; class CPerson { private: char* m_pName; int m_nAge; CCompany* comp; //comp为一个CCompany的数组,因为可为多个公司工作 CPeron consort; //按图中的关系添加了这一个属性标明配偶 int sex; //为标明配偶为男女,所以引入本人的性别 };
11.5 关于类层次的总结 11.5.1 认知规律与类层次 11.5.2 构造函数的一般形式 11.5.3 成员函数的特征 (略)