C++ 面对对象程序设计 授课老师:
第10章 继承 本章要点 10.1 继承与派生 10.2 支配规则和赋值兼容性 10.3 虚基类
本章要点 继承与派生的概念 派生类的定义语句格式及使用方法 冲突、支配规则、赋值兼容性的概念 虚基类的概念 虚基类的语句定义格式及使用方法 静态成员的定义格式、初始化方式及作用域
10.1.1继承与派生的基本概念 1.继承与派生的定义 在面相对象程序设计中提供了类的继承机制,允许程序员在保持原有类特性的基础上,进行更具体、更详细的说明。以原有的类为基础产生新的类,我们就说新类集成了原有类的特性,也可以说从原有类派生出新类。原有类称为基类或父类,产生的新类称为派生类或子类。 2.单一继承 3.多重继承
10.1.2派生类的定义 在C++中,定义派生类的一般语法为: class <派生类名> : <继承方式1> <基类名1>,﹝,…,<继承方式n> <基类名n> ﹞ { <派生类新定义成员> }; 声明中的“基类名”是已有的类的名称,“派生类名”是继承原有类的特性而生成的新类的名称。
10.1.2派生类的定义 在派生过程中,派生出来的新类也可作为基类来继续派生新的类,此外,一个基类可以同时派生出多个派生类。也就是说,一个类从父类继承来的特征也可以被其他新的类所继承,一个父类的特征,可以同时被多个子类继承。 A B C Y Z X
<派生类构造函数体> //派生类新增成员的初始化 } 10.1.3派生类的构造函数与析构函数 1.构造函数 派生类的构造函数需要以合适的初值作为参数,隐含调用基类和新增对象成员的构造函数,来初始化它们各自的数据成员,然后加入新的语句对新增普通数据成员进行初始化。 派生类构造函数的一般语法规则为: <派生类名>::<派生类名> (形参表) : <基类名1>(实参表1), ﹝,…,<基类名n>(实参表n)﹞ { <派生类构造函数体> //派生类新增成员的初始化 }
【例10-1】 派生类构造函数实例 #include "stdafx.h" #include "stdio.h" #include "iostream.h" class A { public: A() a=0; cout<<"类A的默认构造函数被调用"<<endl; } //*程序接下页
程序接上页 A(int i) { a=i; cout<<"类A的构造函数被调用"<<endl; } ~A() void Print() const { cout<<a<<","; int Geta() return a; private: int a; };
程序接上页 class B: public A { public: B() b=0; cout<<"B的默认构造函数被调用"<<endl; } B(int i,int j,int k); ~B() cout<<"B的析构函数被调用"<<endl; void Print(); private: int b; A aa; };
程序接上页 B::B(int i,int j,int k) { b=k; cout<<"B的构造函数被调用"<<endl; } void B::Print() A::Print(); cout<<b<<","<<aa.Geta()<<endl; void main() B bb[2]; bb[0]=B(1,2,5); bb[1]=B(3,4,6); for(int i=0; i<2; i++) bb[i].Print(); getchar();
程序运行结果
2析构函数 当对象被删除时,派生类的析构函数被执行。由于析构函数也不能被继承,因此在执行派生类的折构函数时,基类的析构函数也将被调用。执行顺序是先执行派生类的析构函数,再执行基类的析构函数,其顺序与执行构造函数时的顺序正好相反。
【例10-2】 派生类析构函数实例 #include "stdafx.h" #include "stdio.h" #include "iostream.h" class M { public: M() m1=m2=0; } M(int i,int j) m1=i; m2=j; } // 程序接下页
程序接上页 ~M() { cout<<"M的析构函数被调用"<<endl; } void print() { cout<<m1<<","<<m2<<","; private: int m1, m2; }; class N: public M { public: N() { n=0; N(int i,int j,int k); ~N() { cout<<"N的析构函数被调用"<<endl;
程序接上页 void print() { M::print(); cout<<n<<endl; } private: int n; }; N::N(int i,int j,int k):M(i,j),n(k) {} void main() N n1(2,3,4),n2(5,6,7); n1.print(); n2.print(); n1.~N(); n2.~N(); getchar();
程序运行结果如图
10.2冲突、支配规则和赋值兼容性 10.2.1冲突 为了介绍冲突的概念,先看一个例子。 【例10-3】 定义描述圆的类Circle,定义描述矩形的类Rectangle,描述圆的类与描述矩形的类作为基类,多重派生出圆柱体类Cylinder,在Rectangle类中用矩形中心坐标(x,y)、高(High)与宽(Width)作为类的数据成员,讨论多重继承中基类成员的冲突问题。
程序接上页 #include "stdafx.h" #include "stdio.h" #include "iostream.h" class Circle //定义描述圆的基类 { protected: float x, y, r; //(x,y)为圆心坐标,r为半径 public: Circle(float a,float b,float c) x=a; y=b; r=c; } float Area() //计算圆的面积 return (r*r*3.14159); };
程序接上页 class Rectangle //定义描述矩形的基类 { protected: float x, y, h, w; //(x,y)为矩形中心坐标,h,w为矩形的高与宽 public: Rectangle(float a,float b,float c,float d) x=a; y=b; h=c; w=d; } float Area() //计算矩形面积 return h*w; };
程序接上页 class Cylinder:public Circle,public Rectangle { private: float Volume; //圆柱体的体积 public: Cylinder(float a,float b,float c):Circle(a,b,c),Rectangle(10,10,c,c) //A { Volume=Area()*h;} ∥B float GetV() {return Volume;} void Show(void) { cout<<"x="<<x<<'\t'<<"y="<<y<<endl;} ∥C }; void main() { Cylinder cy(3,3,2); cy.Show(); cout<<"Volume="<<cy.GetV()<<endl; getchar(); }
程序分析 从上例可以看出,在派生类中使用两个基类中同名函数Area()或同名数据成员(x,y)时会产出编译性错误,出错的原因是发生了同名冲突。由此,给出如下有关冲突的定义: 派生类使用基类中同名成员时出现不唯一称为冲突。 解决冲突的方法是使用作用域运算符“::”指明同名成员属于哪一个基类,即: <基类名>::<成员名>
10.2.2支配规则 在C++中,允许派生类中新增加的成员名与其基类的成员名相同,这种重名并不产生冲突。若不使用作用域运算符,则派生类中定义的成员名优于基类中的成员名,这种关系称为支配规则。 在【例10-3】的派生类Cylinder中,新增数据成员(x,y,z)表示圆柱体中心坐标。并从基类Rectangle中删除(x,y)。 修改后程序如下:
class Circle //定义描述圆的基类 { protected: float x, y, r; //(x,y)为圆心坐标,r为半径 程序 #include "stdafx.h" #include "stdio.h" #include "iostream.h" class Circle //定义描述圆的基类 { protected: float x, y, r; //(x,y)为圆心坐标,r为半径 public: Circle(float a,float b,float c) x=a; y=b; r=c; }
程序 float Area() //计算圆的面积 { return (r*r*3.14159); } }; class Rectangle //定义描述矩形的基类 protected: float x, y, h, w; //(x,y)为矩形中心坐标,h,w为矩形的高与宽 public: Rectangle(float c,float d) h=c; w=d; float Area() //计算矩形面积 return h*w;
程序 class Cylinder:public Circle,public Rectangle { private: float x,y,z,Volume; public: Cylinder(float a,float b,float c,float d):Circle(a,b,d),Rectangle(d,d) x=a; y=b; z=c; Volume=Circle::Area()*h; } float GetV() return Volume;
void Show() { cout<<"x="<<x<<'\t'<<"y="<<y<<'\t'<<"z="<<z<<endl; } }; void main() Cylinder cy(3,3,3,2); cy.Show(); cout<<"Volume="<<cy.GetV()<<endl; getchar(); 程序执行后输出结果如下: x=3 y=3 z=3 Volume=25.1327
10.2.3赋值兼容规则 赋值兼容规则指的是在需要基类对象的任何地方都可使用公有派生类的对象来替代。通过公有继承,派生类得到基类中除构造函数、析构函数之外的所有成员,并且所有成员的访问控制属性也和基类完全相同。因此,公有派生类已经具备基类的所有功能。 赋值兼容规则中所指的替代包括以下3种情况: ①派生类的对象可以赋值给基类对象。 ②派生类的对象可以初始化基类的引用。 ⑧派生类对象的地址可以赋给基类类型的指针。
【例10-5】 赋值兼容规则应用举例 #include "stdafx.h" #include "stdio.h" #include "iostream.h" class A { public: void f() //公有函数 cout<<"A::f()"<<endl; } }; class B:public A //A的公有派生类B的声明 void f() cout<<"B::f()"<<endl; //对A中的f()进行覆盖
class D:public B { public: void f() cout<<"D::f()"<<endl; //对B中的f()进行覆盖 } }; void fun(A *p) //参数为指向基类对象的指针 p->f();
void main() { A a; B b; D d; A *p; //声明A类指针 p=&a; //A类指针指向A类对象 fun(p); p=&b; //A类指针指向B类对象 p=&d; //A类指针指向D类对象 getchar(); } 运行结果为: A::f()
10.2.4对基类和对象成员的几点说明 (1)任一基类在派生类中只能继承一次。 (2)基类成员与对象成员在使用上的差别。把一个类作为派生类的基类或将一个类的对象作为另一个类的成员,从程序的执行效果上看是相同的,但在使用上是有区别的: 派生类中可直接使用基类的成员(访问权限允许的话); 使用对象成员时,必须在对象名后加上成员运算符“.”和成员名。
10.3虚基类 在派生类的对象中,同名成员在内存中同时拥有多个拷贝,可以使用作用域分辨符来惟一标识并分别访问它们,也可以将共同基类改置为虚基类,这时从不同的路径继承过来的该类成员在内存中只拥有一个拷贝,这样就解决了同名成员的惟一标识问题。 虚基类说明形式为: virtual <继承方式> <基类名> 其中,virtual是虚基类的关键字,关键字virtual与继承方式的位置无关,但必须位于虚基类名之前,且virtual只对紧随其后的基类名起作用。
【例10-6】虚基类应用举例 #include "stdafx.h" #include "stdio.h" #include "iostream.h" class A { public: int a; void fun() cout<<"a="<<a<<endl; } };
class B:virtual public A //A为虚基类,派生B类 { public: //新增外部接口 int b; }; class C:virtual public A //A为虚基类,派生C类 { public: int c; class D:public B,public C //派生类D声明 { int d; void f(int i) { d=i; } void g() { cout<<"d="<<d<<endl;
void main() { D t; t.a=3; //使用直接基类 t.fun(); getchar(); } 运行结果为: a=3 说明:在D类中,通过B,C两条派生路径继承来的基类A中的成员a和fun()只有一份拷贝,在D派生类中只有惟一的数据成员a和函数成员fun()。
10.4静态成员 静态数据成员 静态数据成员在内存中只有一个拷贝。使用静态数据成员可以节省内存空间。静态数据成员的值对于每一个对象都是一样的。 静态数据成员的定义格式如下: static <数据类型> <变量名>; 静态数据成员初始化格式如下: <数据类型类名>::<静态数据成员名>=<初始值>; 引用静态数据成员的格式如下: <类名>::<静态数据成员名>
10.4.2静态成员函数 静态成员函数也是属于该类的所有对象,而不是属于任何对象。静态成员函数的引用不用对象名,而是类名。 静态成员函数一般用于操作该类中的静态数据成员。在静态成员函数的实现中不能直接引用类中的非静态成员,但可以引用类中的静态成员。如果静态成员函数中需要引用非静态成员时,只能通过对象来引用。 静态成员函数调用格式为: <类名>::<静态成员函数名>(<参数表>)
【例10-7】 静态成员函数应用举例 #include "stdafx.h" #include "stdio.h" #include "iostream.h" class Vc { int A; static int B; //静态数据成员说明 public: Vc(int a) A=a; B+=a; }
static void Display(Vc c) { cout<<"A="<<c.A<<",B="<<B<<endl; } }; int Vc:: B=20; //静态数据成员初始化 void main() Vc A(20),B(40); Vc::Display(A); Vc::Display(B); getchar(); 程序运行结果是: A=20,B=80 A=40,B=80
本章小结 本章介绍了类的继承性。面向对象的三大特性是封装性,继承性和多态性。所谓继承是指从已有类出发建立新的类,使新类部分或全部地继承已有类的成员。通过继承已有的一个或多个类产生一个新类称为派生。使用继承与派生可以减少程序编写的工作量,还能使程序更加易于维护。派生类使用基类中同名成员时出现不唯一称为冲突。在多重派生的过程中,欲使公共的基类在派生类中只有一个拷贝,可将此基类说明成虚基类。本章最后还介绍了静态成员,静态数据成员不是属于任何对象,它属于该类的所有对象。静态成员函数和静态数据成员一样,也是属于该类的所有对象,而不是属于任何对象。