Presentation is loading. Please wait.

Presentation is loading. Please wait.

C++语言程序设计 第七章 继承与派生 清华大学 郑 莉.

Similar presentations


Presentation on theme: "C++语言程序设计 第七章 继承与派生 清华大学 郑 莉."— Presentation transcript:

1 C++语言程序设计 第七章 继承与派生 清华大学 郑 莉

2 本章主要内容 类的继承 类成员的访问控制 单继承与多继承 派生类的构造、析构函数 类成员的标识与访问

3 类的继承与派生 保持已有类的特性而构造新类的过程称为继承。 在已有类的基础上新增自己的特性而产生新类的过程称为派生。
被继承的已有类称为基类(或父类)。 派生出的新类称为派生类。

4 继承与派生问题举例 类的继承与派生

5 继承与派生问题举例 类的继承与派生 猫科

6 继承与派生问题举例 类的继承与派生

7 继承与派生问题举例 类的继承与派生

8 继承与派生的目的 类的继承与派生 继承的目的:实现代码重用。
派生的目的:当新的问题出现,原有程序无法解决(或不能完全解决)时,需要对原有程序进行改造。

9 派生类的声明 类的继承与派生 class 派生类名:继承方式 基类名 { 成员声明; }

10 继承方式 类成员的访问控制 不同继承方式的影响主要体现在: 三种继承方式 派生类成员对基类成员的访问权限 通过派生类对象对基类成员的访问权限
公有继承 私有继承 保护继承

11 公有继承(public) 类成员的访问控制
基类的public和protected成员的访问属性在派生类中保持不变,但基类的private成员不可直接访问。 派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。 通过派生类的对象只能访问基类的public成员。

12 例7-1 公有继承举例 类成员的访问控制 class Point //基类Point类的声明 {public: //公有函数成员
void InitP(float xx=0, float yy=0) {X=xx;Y=yy;} void Move(float xOff, float yOff) {X+=xOff;Y+=yOff;} float GetX() {return X;} float GetY() {return Y;} private: //私有数据成员 float X,Y; };

13 class Rectangle: public Point //派生类声明 { public: //新增公有函数成员
void InitR(float x, float y, float w, float h) {InitP(x,y);W=w;H=h;}//调用基类公有成员函数 float GetH() {return H;} float GetW() {return W;} private: //新增私有数据成员 float W,H; }; 13

14 #include<iostream> #include<cmath> using namecpace std;
int main() { Rectangle rect; rect.InitR(2,3,20,10); //通过派生类对象访问基类公有成员 rect.Move(3,2); cout<<rect.GetX()<<',' <<rect.GetY()<<',' <<rect.GetH()<<',' <<rect.GetW()<<endl; return 0; } 14

15 私有继承(private) 类成员的访问控制
基类的public和protected成员都以private身份出现在派生类中,但基类的private成员不可直接访问。 派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。 通过派生类的对象不能直接访问基类中的任何成员。

16 例7-2 私有继承举例 类成员的访问控制 class Rectangle: private Point //派生类声明
{public: //新增外部接口 void InitR(float x, float y, float w, float h) {InitP(x,y);W=w;H=h;} //访问基类公有成员 void Move(float xOff, float yOff) {Point::Move(xOff,yOff);} float GetX() {return Point::GetX();} float GetY() {return Point::GetY();} float GetH() {return H;} float GetW() {return W;} private: //新增私有数据 float W,H; };

17 #include<iostream> #include<cmath> using namecpace std;
int main() { //通过派生类对象只能访问本类成员 Rectangle rect; rect.InitR(2,3,20,10); rect.Move(3,2); cout<<rect.GetX()<<',' <<rect.GetY()<<',' <<rect.GetH()<<','<<rect.GetW()<<endl; return 0; } 17

18 保护继承(protected) 类成员的访问控制
基类的public和protected成员都以protected身份出现在派生类中,但基类的private成员不可直接访问。 派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。 通过派生类的对象不能直接访问基类中的任何成员

19 protected 成员的特点与作用 类成员的访问控制 对建立其所在类对象的模块来说,它与 private 成员的性质相同。
对于其派生类来说,它与 public 成员的性质相同。 既实现了数据隐藏,又方便继承,实现代码重用。

20 例7-3 protected 成员举例 类成员的访问控制 class A { protected: int x; } int main()
A a; a.x=5; //错误

21 class A { protected: int x; } class B: public A{ public:
void Function(); }; void B:Function() { x=5; //正确 21

22 类型兼容规则 类型兼容 一个公有派生类的对象在使用上可以被当作基类的对象,反之则禁止。具体表现在:
派生类的对象可以被赋值给基类对象。 派生类的对象可以初始化基类的引用。 指向基类的指针也可以指向派生类。 通过基类对象名、指针只能使用从基类继承的成员

23 例7-4 类型兼容规则举例 类型兼容 #include <iostream> using namecpace std;
例7-4 类型兼容规则举例 类型兼容 #include <iostream> using namecpace std; class B0 //基类B0声明 { public: void display(){cout<<"B0::display()"<<endl;} //公有成员函数 }; 派生类对象总是可以充当基类

24 void display(){cout<<"B1::display()"<<endl;} };
class B1: public B0 { public: void display(){cout<<"B1::display()"<<endl;} }; class D1: public B1 void display(){cout<<"D1::display()"<<endl;} void fun(B0 *ptr) { ptr->display(); //"对象指针->成员名" } 晚绑定——见第8章 24

25 int main() //主函数 { B0 b0; //声明B0类对象 B1 b1; //声明B1类对象 D1 d1; //声明D1类对象
B0 *p; //声明B0类指针 p=&b0; //B0类指针指向B0类对象 fun(p); p=&b1; //B0类指针指向B1类对象 p=&d1; //B0类指针指向D1类对象 } 通过基类的指针只能调用从基类继承来的属性,而无法调用派生类的新的属性。 无法实现的原因: 静态绑定——将真正的函数体绑定。在编译时决定。无法预料到执行时接收什么指针,所以…… 不要重新定义继承来的非虚函数。 运行结果: B0::display() 25

26 基类与派生类的对应关系 单继承与多继承 单继承 多继承 多重派生 多层派生 派生类只从一个基类派生。 派生类从多个基类派生。
由一个基类派生出多个不同的派生类。 多层派生 派生类又作为基类,继续派生新的类。 多继承给语法带来复杂性

27 多继承时派生类的声明 单继承与多继承 class 派生类名:继承方式1 基类名1, 继承方式2 基类名2,... { 成员声明; }
注意:每一个“继承方式”,只用于限制对紧随其后之基类的继承。

28 多继承举例 单继承与多继承 class A{ public: void setA(int); void showA(); private:
int a; }; class B{ void setB(int); void showB(); private: int b; }; class C : public A, private B{ public: void setC(int, int, int); void showC(); int c;

29 void C::setC(int x, int y, int z) { //派生类成员直接访问基类的 setA(x); setB(y);
void A::setA(int x) { a=x; } void B::setB(int x) { b=x; } void C::setC(int x, int y, int z) { //派生类成员直接访问基类的 //公有成员 setA(x); setB(y); c=z; } //其他函数实现略 int main() { C obj; obj.setA(5); obj.showA(); obj.setC(6,7,9); obj.showC(); // obj.setB(6); 错误 // obj.showB(); 错误 return 0; } 29

30 继承时的构造函数 派生类的构造、析构函数 基类的构造函数不被继承,派生类中需要声明自己的构造函数。
声明构造函数时,只需要对本类中新增成员进行初始化,对继承来的基类成员的初始化,自动调用基类构造函数完成。 派生类的构造函数需要给基类的构造函数传递参数 思考:派生类继承的成员在什么时候初始化?只要构造派生类时,就会自动构造基类对象。 但是如果基类初始化需要参数,参数何时给?派生类的构造函数需要给基类的构造函数传递参数!!!

31 单一继承时的构造函数 派生类的构造、析构函数 派生类名::派生类名(基类所需的形参,本类成员所需的形参):基类名(参数表) {
本类成员初始化赋值语句; }; 注意:基类名(参数表)不是对构造函数调用,而是说明哪些参数给基类构造函数。

32 单一继承时的构造函数举例 派生类的构造、析构函数 #include<iostream> using namecpace std;
class B{ public: B(); B(int i); ~B(); void Print() const; private: int b; };

33 cout<<"B's default constructor called."<<endl; }
B::B() { b=0; cout<<"B's default constructor called."<<endl; } B::B(int i) { b=i; cout<<"B's constructor called." <<endl; B::~B() { cout<<"B's destructor called."<<endl; } void B::Print() const { cout<<b<<endl; } 可以试试:基类构造函数需要参数,派生类构造函数不要参数。编译会出错。 33

34 class C:public B { public: C(); C(int i,int j); ~C();
void Print() const; private: int c; }; 34

35 cout<<"C's default constructor called."<<endl; }
C::C() { c=0; cout<<"C's default constructor called."<<endl; } C::C(int i,int j):B(i) { c=j; cout<<"C's constructor called."<<endl; C::~C() { cout<<"C's destructor called."<<endl; } void C::Print() const { B::Print(); cout<<c<<endl; } int main() { C obj(5,6); obj.Print(); } 35

36 多继承时的构造函数 派生类的构造、析构函数
派生类名::派生类名(基类1形参,基类2形参,...基类n形参,本类形参):基类名1(参数), 基类名2(参数), ...基类名n(参数) { 本类成员初始化赋值语句; };

37 派生类与基类的构造函数 派生类的构造、析构函数
当基类中声明有默认形式的构造函数或未声明构造函数时,派生类构造函数可以不向基类构造函数传递参数。 若基类中未声明构造函数,派生类中也可以不声明,全采用默认形式构造函数。 当基类声明有带形参的构造函数时,派生类也应声明带形参的构造函数,并将参数传递给基类构造函数。 第三条很重要,经常考!!!

38 多继承且有内嵌对象时 的构造函数 派生类的构造、析构函数
派生类名::派生类名(基类1形参,基类2形参,...基类n形参,本类形参):基类名1(参数), 基类名2(参数), ...基类名n(参数),对象数据成员的初始化 { 本类成员初始化赋值语句; }; 每个子对象都需要初始化。 基类的次序无关。

39 构造函数的调用顺序 派生类的构造、析构函数 1. 调用基类构造函数,调用顺序按照它们被继承时声明的顺序(从左向右)。
2. 调用成员对象的构造函数,调用顺序按照它们在类中声明的顺序。 3. 派生类的构造函数体中的内容。

40 拷贝构造函数 派生类的构造、析构函数 若建立派生类对象时调用默认拷贝构造函数,则编译器将自动调用基类的默认拷贝构造函数。
若编写派生类的拷贝构造函数,则需要为基类相应的拷贝构造函数传递参数。例如: C::C(C &c1):B(c1) {…}

41 例7-5 派生类构造函数举例 派生类的构造、析构函数 #include <iostream>
using namecpace std; class B1 //基类B1,构造函数有参数 {public: B1(int i) {cout<<"constructing B1 "<<i<<endl;} }; class B2 //基类B2,构造函数有参数 B2(int j) {cout<<"constructing B2 "<<j<<endl;} class B3 //基类B3,构造函数无参数 B3(){cout<<"constructing B3 *"<<endl;} 考试必靠!!!与书上例题类似。

42 class C: public B2, public B1, public B3 { public: //派生类的公有成员
C(int a, int b, int c, int d): B1(a),memberB2(d),memberB1(c),B2(b) {} private: //派生类的私有对象成员 B1 memberB1; B2 memberB2; B3 memberB3; }; int main() { C obj(1,2,3,4); } 运行结果: constructing B2 2 constructing B1 1 constructing B3 * constructing B1 3 constructing B2 4 构造子对象的先后顺序为:B2,B1,B3。与B1(a),memberB2(d),memberB1(c),B2(b) 的顺序无关。 注意:先构造继承对象,再构造本类内嵌对象。 memberB2(d),memberB1(c),不能放在大括号里面!!!因为要先构造出来,再用。 写在大括号里面的是赋值,而不是初始化!!!所以初始化时,要写在初始化列表里面。这样效率较高! 42

43 继承时的析构函数 派生类的构造、析构函数 析构函数也不被继承,派生类自行声明 声明方法与一般(无继承关系时)类的析构函数相同。
不需要显式地调用基类的析构函数,系统会自动隐式调用。 析构函数的调用次序与构造函数相反。 没有参数传递 有时要写析构函数。比如构造时用到动态内存分配。

44 例7-6 派生类析构函数举例 派生类的构造、析构函数 #include <iostream>
例7-6 派生类析构函数举例 派生类的构造、析构函数 #include <iostream> using namecpace std; class B1 //基类B1声明 { public: B1(int i) {cout<<"constructing B1 "<<i<<endl;} ~B1() {cout<<"destructing B1 "<<endl;} }; class B2 //基类B2声明 {public: B2(int j) {cout<<"constructing B2 "<<j<<endl;} ~B2() {cout<<"destructing B2 "<<endl;} class B3 //基类B3声明 B3(){cout<<"constructing B3 *"<<endl;} ~B3() {cout<<"destructing B3 "<<endl;}

45 class C: public B2, public B1, public B3 {public:
C(int a, int b, int c, int d): B1(a),memberB2(d),memberB1(c),B2(b){} private: B1 memberB1; B2 memberB2; B3 memberB3; }; int main() { C obj(1,2,3,4); } 45

46 例7-6 运行结果 constructing B2 2 constructing B1 1 constructing B3 *
destructing B3 destructing B2 destructing B1 注意析构函数调用顺序

47 同名隐藏规则 派生类成员的标识与访问 当派生类与基类中有相同成员时: 若未强行指名,则通过派生类对象使用的是派生类中的同名成员。
如要通过派生类对象访问基类中被覆盖的同名成员,应使用基类名限定。

48 例7-7 多继承同名隐藏举例 派生类成员的标识与访问 #include <iostream>
例7-7 多继承同名隐藏举例 派生类成员的标识与访问 #include <iostream> using namecpace std; class B1 //声明基类B1 { public: //外部接口 int nV; void fun() {cout<<"Member of B1"<<endl;} }; class B2 //声明基类B2 void fun(){cout<<"Member of B2"<<endl;} class D1: public B1, public B2 { public: int nV; //同名数据成员 void fun(){cout<<"Member of D1"<<endl;} //同名函数成员

49 d1.nV=1; //对象名.成员名标识, 访问D1类成员 d1.fun();
int main() { D1 d1; d1.nV=1; //对象名.成员名标识, 访问D1类成员 d1.fun(); d1.B1::nV=2; //作用域分辨符标识, 访问基类B1成员 d1.B1::fun(); d1.B2::nV=3; //作用域分辨符标识, 访问基类B2成员 d1.B2::fun(); } 基类的nV被隐藏了 若派生类中没有nV,两个基类中有同名成员,则要用类名限定。 49

50 二义性问题 派生类成员的标识与访问 在多继承时,基类与派生类之间,或基类之间出现同名成员时,将出现访问时的二义性(不确定性)——采用虚函数(参见第8章)或同名隐藏规则来解决。 当派生类从多个基类派生,而这些基类又从同一个基类派生,则在访问此共同基类中的成员时,将产生二义性——采用虚基类来解决。

51 二义性问题举例(一) 派生类成员的标识与访问 class C: public A, piblic B { public: void g();
void h(); }; 如果声明:C c1; 则 c1.f(); 具有二义性 而 c1.g(); 无二义性(同名覆盖) class A { public: void f(); }; class B void f(); void g()

52 二义性的解决方法 派生类成员的标识与访问 解决方法一:用类名来限定 c1.A::f() 或 c1.B::f()
解决方法二:同名覆盖 在C 中声明一个同名成员函数f(),f()再根据需要调用 A::f() 或 B::f()

53 二义性问题举例(二) 派生类成员的标识与访问 class B { public: class C : public B1,public B2
int b; } class B1 : public B { private: int b1; class B2 : public B int b2; }; class C : public B1,public B2 { public: int f(); private: int d; } 还有冗余问题

54 派生类C的对象的存储结构示意图: 有二义性: 无二义性: C c; c.B1::b c.b c.B2::b c.B::b b b1 C类对象
d B类成员 B1类成员 B2类成员 C类对象 有二义性: C c; c.b c.B::b 无二义性: c.B1::b c.B2::b 54

55 虚基类 虚基类的引入 声明 作用 注意: 用于有共同基类的场合
以virtual修饰说明基类 例:class B1:virtual public B 作用 主要用来解决多继承时可能发生的对同一基类继承多次而产生的二义性问题. 为最远的派生类提供一的基类成员,而不重复产生多次拷贝 注意: 在第一级继承时就要将共同基类设计为虚基类。 什么时候需要都用虚方式继承?这样只能在小规模编程使用。

56 虚基类举例 虚 基 类 class B{ private: int b;};
虚 基 类 class B{ private: int b;}; class B1 : virtual public B { private: int b1;}; class B2 : virtual public B { private: int b2;}; class C : public B1, public B2{ private: float d;} 下面的访问是正确的: C cobj; cobj.b;

57 虚基类的派生类对象存储结构示意图: B B1 B2 C b1 b2 d B1类成员 B2类成员 C类对象 b B类成员 57

58 例7-8虚基类举例 虚 基 类 D1 nV :int nVd:int B1::nV1:int B2::nV2:int fund():void
虚 基 类 D1 nV :int nVd:int B1::nV1:int B2::nV2:int fund():void fun():void B1 nV1 :int B2 nV2 :int nVd :int <<virtual>> B0 fun() 注意:D1只有一份nV

59 B0 B1新增成员 B2新增成员 D1新增成员 B1 B2 D1 nV,fun() 59

60 #include <iostream> using namecpace std; class B0 //声明基类B0
{ public: //外部接口 int nV; void fun(){cout<<"Member of B0"<<endl;} }; class B1: virtual public B0 //B0为虚基类,派生B1类 { public: //新增外部接口 int nV1; class B2: virtual public B0 //B0为虚基类,派生B2类 { public: //新增外部接口 int nV2; 60

61 class D1: public B1, public B2 //派生类D1声明 { public: //新增外部接口 int nVd;
void fund(){cout<<"Member of D1"<<endl;} }; int main() //程序主函数 { D1 d1; //声明D1类对象d1 d1.nV=2; //使用最远基类成员 d1.fun(); } 61

62 虚基类及其派生类构造函数 虚 基 类 建立对象时所指定的类称为最(远)派生类。
虚 基 类 建立对象时所指定的类称为最(远)派生类。 虚基类的成员是由最派生类的构造函数通过调用虚基类的构造函数进行初始化的。 在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中给出对虚基类的构造函数的调用。如果未列出,则表示调用该虚基类的默认构造函数。 在建立对象时,只有最派生类的构造函数调用虚基类的构造函数,该派生类的其他基类对虚基类构造函数的调用被忽略。

63 有虚基类时的构造函数举例 虚 基 类 #include <iostream> using namecpace std;
虚 基 类 #include <iostream> using namecpace std; class B0 //声明基类B0 { public: //外部接口 B0(int n){ nV=n;} int nV; void fun(){cout<<"Member of B0"<<endl;} }; class B1: virtual public B0 { public: B1(int a) : B0(a) {} int nV1; class B2: virtual public B0 B2(int a) : B0(a) {} int nV2;

64 class D1: public B1, public B2 { public:
D1(int a) : B0(a), B1(a), B2(a){} int nVd; void fund(){cout<<"Member of D1"<<endl;} }; int main() D1 d1(1); d1.nV=2; d1.fun(); } 注意: D1(int a) : B0(a), B1(a), B2(a){}中要给B0传递参数 64

65 综合实例 例7-10(课后阅读) 这个程序有两点不足: ①基类的成员函数pay() 的函数体为空,在实现部分仍要写出函数体,显得冗余。
②在main()函数中,建立了四个不同类的对象,对它们进行了类似的操作,但是却重复写了四遍类似的语句,程序不够简洁。 一定阅读!!!

66 小结与复习建议 主要内容 达到的目标 实验任务 类的继承、类成员的访问控制、单继承与多继承、派生类的构造和析构函数、类成员的标识与访问
理解类的继承关系,学会使用继承关系实现代码的重用。 实验任务 实验七


Download ppt "C++语言程序设计 第七章 继承与派生 清华大学 郑 莉."

Similar presentations


Ads by Google