《面向对象编程基础.ppt》由会员分享,可在线阅读,更多相关《面向对象编程基础.ppt(103页珍藏版)》请在金锄头文库上搜索。
1、第第3章章 面向对象编程基础面向对象编程基础3.1 面向对象概念面向对象概念对象是面向对象编程的基本成分。什么是对象?可以说任何事物都是对象。对象可以是现实世界中的一个物理对象,也可以是抽象的概念或规则。对象可用它本身的一组属性和操作功能来定义。例如:对于自行车对象,它的颜色、车轮个数等是对象的状态,它能行驶、能变速是对象的功能;对于电视机对象,它的屏幕大小、色彩、频道数等是它的属性,能打开、关闭、更换频道、调整音量等是它的操作。3.1.1 对象、类、实例化对象、类、实例化在面向对象程序设计技术中,对象是具有属性(又称状态)和操作(又称方法、行为方式和消息等)的实体。对象的属性表示了它所处于的
2、状态;对象的操作则用来改变对象的状态达到特定的功能。对象有一个惟一的标识名以区别于其他对象,对象有固定的对外接口,是对象在约定好的运行框架和消息传递机制中与外界通信的通道。对象是面向对象技术的核心,是构成系统的基本单元,所有的面向对象的程序都是由对象来组成的。类是在对象之上的抽象,它为属于该类的全部对象提供了统一的抽象描述。所以类是一种抽象的数据类型,它是对象的模板,对象则是类的具体化,是类的实例。例如:“一台PANDA电视机”等价于“这是电视机类的一个实例”。 类与对象的关系如图3.1所示。3.1.1 对象、类、实例化对象、类、实例化图图3.1 类与对象关系类与对象关系3.1.2 面向对象程
3、序设计语言的三大原则面向对象程序设计语言的三大原则一个面向对象的语言在处理对象时,必须遵循的三个原则是:封装、继承、多态。1. 封装封装所谓“封装”,就是用一个框架把数据和代码组合在一起,形成一个对象。遵循面向对象数据抽象的要求,一般数据都被封装起来,也就是外部不能直接访问对象的数据,外部能见到的只有提供给外面访问的公共操作(也称接口,对象之间联系的渠道)。在C#中,类是支持对象封装的工具,对象则是封装的基本单元。封装的对象之间进行通讯的一种机制叫做消息传递。消息是向对象发出的服务请求,是面向对象系统中对象之间交互的途径。消息包含要求接收对象去执行某些活动的信息,以及完成要求所需的其他信息(参
4、数)。发送消息的对象不需要知道接收消息的对象如何对请求予以响应。接收者接收了消息,它就承担了执行指定动作的责任,作为消息的答复,接收者将执行某个方法,来满足所接收的请求。3.1.2 面向对象程序设计语言的三大原则面向对象程序设计语言的三大原则2. 继承继承继承是面向对象编程技术的一块基石,通过它可以创建分等级层次的类。例如,创建一个汽车的通用类,它定义了汽车的一般属性(如:车轮、方向盘、发动机、车门)和操作方法(如:前进、倒退、刹车、转弯等)。从这个已有的类可以通过继承的方法派生出新的子类,卡车、轿车、客车等,它们都是汽车类的更具体的类,每个具体的类还可增加自己一些特有的东西。如图3.2所示,
5、更一般地表示如图3.3所示。图图3.2 汽车类的派生汽车类的派生图图3.3 类的继承类的继承2. 继承继承继承是父类和子类之间共享数据和方法的机制,通常把父类称为基类,子类称为派生类。一个基类可以有任意数目的派生类,从基类派生出的类还可以被派生,一群通过继承相联系的类就构成了类的树型层次结构。如果一个类有两个或两个以上直接基类,这样的继承结构被称为多重继承或多继承。在现实世界中这种模型屡见不鲜,如:一些组合功能的产品像沙发床,它既有沙发的功能,又有床的功能,沙发床应允许同时继承沙发和床的特征。如图3.4所示,更一般地表示如图3.5所示图图3.4多继承示意多继承示意图图3.5 类的多继承类的多继
6、承2. 继承继承尽管多继承从形式上看比较直观,但在实现上多继承可能引起继承操作或属性的冲突。当今的很多语言已不再支持多继承,C#语言也对多继承的使用进行了限制,它通过接口来实现。接口可以从多个基接口继承。接口可以包含方法、属性、事件和索引器。一个典型的接口就是一个方法声明的列表,接口本身不提供它所定义的成员的实现。所以接口不能被实例化,一个实现接口的类再以适当的方式定义接口中声明的方法。如图3.6所示。图图3.6类的接口类的接口3.1.2 面向对象程序设计语言的三大原则面向对象程序设计语言的三大原则3. 多态性多态性多态性就其字面上的意思是:多种形式或多种形态。在面向对象编程中,多态是指同一个
7、消息或操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。 在面向对象编程中,多态性有两种,一种是静态多态,一种是动态多态。当在同一个类中,直接调用一个对象的方法时候,系统在编译时,根据传递的参数个数、参数类型以及返回值的类型等信息决定实现何种操作,这就是所谓的静态绑定。当在一个有着继承关系的类层次结构中,间接调用一个对象的方法时候,也就是说,调用经过基类的操作,这种调用只有到系统运行时,才能根据实际情况决定实现何种操作,这就是所谓的动态绑定。C#支持这两种类型的多态,在实现多态上C#可以有几种方式:接口多态性、继承多态性、通过抽象类实现的多态性。 3.2 类类正如前节所述,对象是面向
8、对象语言的核心,数据抽象和对象封装是面向对象技术的基本要求,而实现这一切的主要手段和工具就是类。从编程语言的角度讲,类就是一种数据结构,它定义数据和操作这些数据的代码。把握面向对象编程的重要一步就是区分类与对象,类是对其成员的一种封装,对类进行对象实例化,并在其数据成员上实施操作才是完成现实任务的根本。实例化后的类为对象,其核心特征便是拥有了一份自己特有的数据成员拷贝。这些为对象所持有的数据成员称之为实例成员。不为对象所持有的数据成员称之为静态成员,在类中用static修饰符声明。类的成员包含数据成员(常量、域、事件)和函数成员(方法、属性、索引器、操作符、构造函数、析构函数等)。 3.2.1
9、 类的声明类的声明要定义一个新的类,首先要声明它。语法形式:语法形式:属性集信息opt 类修饰符opt class 类名 : 类基 opt 类主体 opt其中:其中:属性集信息:是C#语言提供给程序员为程序中定义的各种实体附加一些说明信息,这是C#语言的一个重要特征。类修饰符:可以是以下表3.1所列的几种之一或者是它们的有效组合,但在类声明中,同一修饰符不允许出现多次。 表表3.1 类的修饰符类的修饰符 3.2.1 类的声明类的声明类基:它定义该类的直接基类和由该类实现的接口。当多于一项时,用逗号“,”分隔。如果没有显式地指定直接基类,那么它的基类隐含为object 。最简单的类声明语法形式:
10、语法形式: class 类名 类成员 例如:class Point / Point类的访问权限缺省为public int x, y; 3.2.2 类的成员类的成员 类的定义包括类头和类体两部分,其中类体用一对大花括号 括起来,类体用于定义该类的成员。类体语法形式:语法形式: 类成员声明 opt 类成员由两部分组成,一个是类体中以类成员声明形式引入的类成员,另一个则是直接从它的基类继承而来的成员。类成员声明主要包括:常数声明、字段声明、方法声明、属性声明、事件声明、索引器声明、运算符声明、构造函数声明、析构函数声明、静态构造函数、类型声明等。当字段、方法、属性、事件、运算符和构造函数声明中含有s
11、tatic修饰符时,则表明它们是静态成员,否则就是实例成员。类成员声明中可以使用以下5种访问修饰符中的一种:public,private,protected,internal,protected internal 。当类成员声明不包含访问修饰符时,缺省约定访问修饰符为private。 3.2.2 类的成员类的成员1常数声明常数声明常数声明一般语法形式语法形式:属性集信息 opt 常数修饰符 opt const 类型 标识符 = 常数表达式,标识符 = 常数表达式 0+其中:其中:常数修饰符可以是:new、public、protected、internal、private。类型必须是:sbyte
12、、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、bool、string、枚举类型或引用类型。常数表达式的值类型应与目标类型相一致,或者可以通过隐式转换转换成目标类型。2字段声明字段声明字段声明的一般语法形式语法形式:属性集信息 opt 字段修饰符 opt 类型 变量声明列表;其中其中:变量声明列表:标识符或者用逗号“,”分隔的多个标识符,并且变量标识符还可用赋值号“=”设定初始值。 3.2.2 类的成员类的成员【例例3.1】通过构造函数对只读字段赋值。using System;public class Areap
13、ublic readonly double Radius; / Radius是只读字段private double x, y ;public double Size ; public static double Sum=0.0 ;public Area ( ) Radius=1.0; / 通过构造函数对radius赋值class Testpublic static void Main( )Area s1 = new Area( );Console.WriteLine (Radius=0, Size=1,Sum=2,s1.Radius,s1.Size , Area.Sum);/ 静态字段通过类访
14、问Area.Sum,实例字段通过对象访问s1.SizeConsole.Read ();【例3.1】运行结果如图3.7所示。图图3.7 对只读字段赋值的运行结果对只读字段赋值的运行结果3.2.3 构造函数构造函数 当定义了一个类之后,就可以通过new运算符将其实例化,产生一个对象。为了能规范、安全地使用这个对象,C#提供了对对象进行初始化的方法,这就是构造函数。在C#中,类的成员字段可以分为实例字段和静态字段,与此相应的构造函数也分为实例构造函数和静态构造函数。1实例构造函数声明实例构造函数声明实例构造函数的声明语法形式语法形式:属性集信息 opt 构造函数修饰符 opt 标识符 ( 参数列表
15、opt ) : base ( 参数列表 opt ) opt : this ( 参数列表 opt ) opt 构造函数语句块 其中:其中:构造函数修饰符:public、protected、internal、private、extern。一般地,构造函数总是public类型的。如果是private类型的,表明类不能被外部类实例化。 标识符( 参数列表 opt ):标识符是构造函数名,必须与这个类同名,不声明返回类型,并且没有任何返回值。这与返回值类型为void的函数不同。构造函数参数可以没有,也可以有一个或多个。这表明构造函数在类的声明中可以有函数名相同,但参数个数不同或者参数类型不同的多种形式,
16、这就是所谓的构造函数重载。 3.2.3 构造函数构造函数【例例3.2】Time类的构造函数及其重载。using System;public class Timeprivate int hour, minute, second;public Time ( ) hour=minute=second=0;public Time (int h)hour=h;minute=second=0;public Time (int h, int m)hour=h;minute=m;second=0;public Time (int h, int m, int s) hour=h;minute=m;second=
17、s;class Teststatic void Main() Time t1, t2, t3, t4 ; / 对t1, t2, t3, t4分别调用不同的构造函数t1= new Time (); t2 = new Time(8);t3 = new Time(8, 30);t4 = new Time(8,30,30);3.2.3 构造函数构造函数【例例3.3】构造函数初始化。实例对象创建时,根据不同的参数调用相应的构造函数完成初始化。using System;class Point public double x, y; public Point( ) x = 0; y = 0; public
18、Point(double x, double y) this.x = x; / 当this在实例构造函数中使用时, this.y = y; / 它的值就是对该构造的对象的引用。 class Testpublic static void Main( ) Point a = new Point( ); Point b = new Point(3, 4); / 用构造函数初始化对象Console.WriteLine (a.x=0, a.y=1, a.x, a.y );Console.WriteLine (b.x=0, b.y=1, b.x, b.y );Console.Read (); 运行结果如图
19、3.8所示。图图3.8 用不同的构造函数完成的对象初始化的运行结果用不同的构造函数完成的对象初始化的运行结果3.2.3 构造函数构造函数上例中声明了一个类Point,它提供了两个重载的构造函数。一个是不带参数的Point构造函数和一个是带有两个double参数的Point构造函数。如果类中没有提供这些构造函数,那么CLR会自动提供一个缺省构造函数的。但一旦类中提供了自定义的构造函数,系统则不提供缺省构造函数,这一点需要特别注意,否则系统编译会报出错。如图3.9所示。图图3.9 程序编译出错信息程序编译出错信息3.2.3 构造函数构造函数【例例3.4】Point类只定义了一个带两个参数的构造函数
20、,在创建Point对象a时,编译会报告出错信息。using System;class Pointpublic double x, y;public Point(double x, double y) this.x = x; / 当this在实例构造函数中使用时, this.y = y; / 它的值就是对该构造的对象的引用。class Testpublic static void Main()Point a = new Point( ); / 出错Point b = new Point(3, 4); / 用构造函数初始化对象Console.WriteLine (a.x=0, a.y=1, a.x
21、, a.y );Console.WriteLine (b.x=0, b.y=1, b.x, b.y );3.2.3 构造函数构造函数【例例3.5】派生类构造函数及其调用。using System;class Pointprivate int x, y;public Point()x = 0; y = 0;Console.WriteLine (Point() constructor : 0 , this );public Point(int x, int y)this.x = x; this.y = y; Console.WriteLine (Point(x,y) constructor : 0
22、 , this ); class Circle : Pointprivate double radius;public Circle () / 缺省约定调用基类的无参构造函数Point() Console.WriteLine (Circle () constructor : 0 , this );public Circle (double radius ): base ( )this.radius = radius;Console.WriteLine (Circle (radius) constructor : 0 , this );public Circle (int x, int y, d
23、ouble radius ): base (x, y ) this.radius = radius;Console.WriteLine (Circle (x, y, radius) constructor : 0 , this );class Test static void Main() Point a = new Point(); Circle b = new Circle (3.5); Circle c = new Circle (1, 1, 4.8); Console.Read (); 运行结果如图3.10所示。 图图3.10 派生类对象在初始化时,调用构造函数的次序派生类对象在初始化
24、时,调用构造函数的次序3.2.3 构造函数构造函数2. 静态构造函数声明静态构造函数声明静态构造函数的声明语法形式语法形式:属性集信息 opt 静态构造函数修饰符 opt 标识符 ( ) 静态构造函数体 其中:其中:静态构造函数修饰符: extern static 或者 static extern 如果有extern修饰,则说明这是一个外部静态构造函数,不提供任何实际的实现,所以静态构造函数体仅仅是一个分号。标识符( ):标识符是静态构造函数名,必须与这个类同名,静态构造函数不能有参数。静态构造函数体:静态构造函数的目的是用于对静态字段进行初始化,所以它只能对静态数据成员进行初始化,而不能对非
25、静态数据成员进行初始化。静态构造函数是不可继承的,而且不能被直接调用。只有创建类的实例或者引用类的任何静态成员时,才能激活静态构造函数,所以在给定的应用程序域中静态构造函数至多被执行一次。如果类中没有声明静态构造函数,而又包含带有初始设定的静态字段,那么编译器会自动生成一个默认的静态构造函数。3.2.3 构造函数构造函数【例例3.6】静态构造函数。using System;class Screen static int Height; static int Width; int Cur_X , Cur_Y ; static Screen ( ) / 静态构造函数,对类的静态字段初始化 Heig
26、ht=768; Width=1024; 3.2.4 析构函数析构函数一般来说,创建一个对象时需要用构造函数初始化数据,与此相对应释放一个对象时就用析构函数。所以析构函数是用于实现析构类实例所需操作的方法。声明语法形式:语法形式:属性集信息 opt extern opt 标识符 ( ) 析构函数体其中:其中:标识符必须与类名相同,但为了区分构造函数,前面需加“”表明它是析构函数。析构函数不能写返回类型,也不能带参数,因此它不可能被重载,当然它也不能被继承,所以一个类最多只能有一个析构函数。一个类如果没有显式地声明析构函数,则编译器将自动产生一个缺省默认的析构函数。析构函数不能由程序显式地调用,而
27、是由系统在释放对象时自动调用。如果这个对象是一个派生类对象,那么在调用析构函数时也会产生链式反应,首先执行派生类的析构函数,然后执行基类的析构函数,如果这个基类还有自己的基类,这个过程就会不断重复,直到调用Object类的析构函数为止,其执行顺序正好与构造函数相反。3.2.4 析构函数析构函数【例例3.7】析构函数的调用次序。using System; public class Point private int x, y; Point() Console.WriteLine (Points destructor ); public class Circle : Point private d
28、ouble radius; Circle () / 缺省约定调用基类的无参构造函数Point()。 Console.WriteLine (Circles destructor ); class Test public static void Main() Circle b = new Circle (); b=null; GC.Collect(); GC.WaitForPendingFinalizers(); Console.Read (); 运行结果如图3.11所示。 图图3.11 析构函数的调用次序析构函数的调用次序3.3 方法方法3.3.1 方法的声明方法的声明方法是按照一定格式组织的一
29、段程序代码,在类中用方法声明的方式来定义。方法声明语法形式:语法形式:属性集信息 opt 方法修饰符 opt 返回类型 方法名 ( 形参表 opt ) 方法体 其中:其中:方法修饰符有如表3.2所示。表表3.2 方法的修饰符方法的修饰符3.3.1 方法的声明方法的声明方法修饰符中public、protected、private、internal、protected internal属于访问修饰符,用于表示访问的级别,默认情况下,方法的访问级别为public。访问修饰符也可以和其它的方法修饰符有效地组合在一起,但某些修饰符是互相排斥的。表3.3所列的组合被视为非法。 表表3.3 修饰符的无效组合
30、修饰符的无效组合3.3.1 方法的声明方法的声明【例例3.8】下面程序中的StackTp类定义了几个方法以模拟实现一个压栈操作。using System;class StackTp int MaxSize;int Top;int StkList;public StackTp ( ) / 构造函数MaxSize=100;Top=0;StkList=new int MaxSize;public StackTp(int size) / 构造函数MaxSize=size;Top=0;StkList=new int MaxSize;public bool isEmptyStack() / 方法if (T
31、op=0) return true; else return false;public bool isFullStack()if (Top=MaxSize)return true;elsereturn false;public void push(int x) StkListTop=x;Top+;class Test public static void Main() StackTp ST = new StackTp(20); string s1; if ( ST.isEmptyStack() ) / 调用方法isEmptyStack() s1=Empty; else s1=not Empty
32、; Console.WriteLine (Stack is + s1); for (int i=0; iy) tmp=x; x=y; y=tmp; if (xz) tmp=x; x=z; z=tmp; if (yz) tmp=y; y=z; z=tmp; class Test static void Main() Myclass m = new Myclass ( ); int a, b, c; a=30; b=20; c=10; m.Sort (a, b, c); Console.WriteLine (a=0, b=1, c=2 , a, b, c ); Console.Read (); 运
33、行结果如图3.13所示:图图3.13 方法的值参数传递的运行结果方法的值参数传递的运行结果【例例3.9】3.3.2 方法的参数方法的参数【例例3.10】下面程序演示的是当方法传递的是一个引用对象(例如数组时)时,对形参的修改会影响到实参。using System;class Myclass public void SortArray (int a) int i, j, pos,tmp; for (i=0; ia.Length-1 ; i+) for (pos=j=i; ja j) pos=j; if (pos != i ) tmp=ai; ai=apos; apos=tmp; class Te
34、ststatic void Main() Myclass m = new Myclass ( );int score = 87,89,56,90,100,75,64,45, 80, 84 ;m.SortArray(score);for ( int i=0; iy) tmp=x; x=y; y=tmp; if (xz) tmp=x; x=z; z=tmp; if (yz) tmp=y; y=z; z=tmp; 【例3.11】 class Teststatic void Main() Myclass m = new Myclass ( );int a, b, c;a=30; b=20; c=10;
35、m.Sort (ref a, ref b, ref c);Console.WriteLine (a=0, b=1, c=2 , a, b, c ); Console.Read();运行结果如图3.15所示。图图3.15 引用参数传递方式的运行结果引用参数传递方式的运行结果2. 引用参数引用参数使用使用ref参数的注意点:参数的注意点:(1) ref关键字仅对跟在它后面的参数有效,而不能应用于整个参数表。例如Sort方法中x, y, z都要加ref修饰。(2) 在调用方法时,也用ref修饰实参变量,因为是引用参数,所以要求实参与形参的数据类型必须完全匹配,而且实参必须是变量,不能是常量或表达式。
36、(3) 在方法外,ref参数必须在调用之前明确赋值,在方法内,ref参数被视为初始值已赋过。3.3.2 方法的参数方法的参数3. 输出参数输出参数在参数前加out修饰符的被称为输出参数,它与ref参数很相似,只有一点除外,就是它只能用于从方法中传出值,而不能从方法调用处接受实参数据。在方法内out参数被认为是未赋过值的,所以在方法结束之前应该对out参数赋值。【例例3.12】在下面程序4-7中,求一个数组元素中的最大值、最小值以及平均值。希望得到三个返回值,显然用方法的返回值不能解决,而且这三个值必须通过计算得到,初始值没有意义,所以解决方案可以定义三个out参数。using System;c
37、lass Myclass public void MaxMinArray (int a, out int max, out int min, out double avg ) int sum ; sum = max = min = a0 ;for (int i=1; imax) max=ai;if (aimin) min=ai;sum+=ai;【例例3.12】 avg=sum / a.Length;class Teststatic void Main() Myclass m = new Myclass ( );int score = 87,89,56,90,100,75,64,45, 80,
38、84 ;int smax, smin;double savg;m.MaxMinArray(score, out smax, out smin, out savg);Console.Write (Max=0, Min=1, Avg=2 , smax, smin, savg ); Console.Read ();【例例3.12】运行结果如图3.16所示。 ref和out参数的使用并不局限于值类型参数,它们也可用于引用类型来传递对象。3. 输出参数输出参数【例例3.13】下面程序定义了两个方法,一个是Swap1,一个是Swap2,它们都有两个引用对象作参数,但Swap2的参数加了ref修饰,调用这两
39、个方法产生的结果是不一样的。using System;class Myclass public void Swap1 (string s, string t) string tmp; tmp=s; s=t; t=tmp; public void Swap2 (ref string s, ref string t) string tmp; tmp=s; s=t; t=tmp; 【例例3.13】class Teststatic void Main()Myclass m = new Myclass ( );string s1=ABCDEFG, s2=134567;m.Swap1(s1,s2);Con
40、sole.WriteLine (s1=0 , s1 ); / s1,s2的引用并没有改变Console.WriteLine (s2=0 , s2 ); m.Swap2(ref s1, ref s2); / s1,s2的引用互相交换了Console.WriteLine (s1=0 , s1 );Console.WriteLine (s2=0 , s2 ); Console.Read ();运行结果如图3.17所示。图图3.17 Swap1和和Swap2两个不同参数传递方式的运行结果两个不同参数传递方式的运行结果3.3.2 方法的参数方法的参数4. 参数数组参数数组一般而言,调用方法时其实参必须与
41、该方法声明的形参在类型和数量上相匹配,但有时候我们更希望灵活一些,能够给方法传递任意个数的参数,比如在三个数中找最大、最小和在5个数中找最大、最小甚或任意多个数中找最大、最小能使用同一个方法。C#提供了传递可变长度的参数表的机制,即使用params关键字来指定一个参数可变长的参数表。【例例3.14】下面程序演示了Myclass类中的方法MaxMin有一个参数数组类型的参数,那么在调用这个方法是所具有的灵活性。using System;class Myclass public void MaxMin (out int max, out int min, params int a ) if (a.
42、Length=0) / 如果可变参数为零个,可以取一个约定值或产生异常max=min=-1; return ;【例例3.14】max = min = a0 ;for (int i=1; imax) max=ai;if (ai=y ? x : y ;public double max (double x, double y) return x=y ? x : y ;public int max (int x, int y, int z) return max ( max (x, y), z) ;public double max ( double x, double y, double z)【例
43、3.16】 return max ( max (x, y), z) ; class Test static void Main() Myclass m = new Myclass ( ); int a, b, c; double e, f, g ; a=10; b=20; c=30; e = 1.5; f = 3.5 ; g = 5.5; / 调用方法时,编译器会根据实参的类型和个数调用不同的方法。 Console.WriteLine (max(0,1)= 2 ,a,b, m.max(a,b); Console.WriteLine (max(0,1,2)= 3 ,a,b,c, m.max(a,
44、b,c); Console.WriteLine (max(0,1)= 2 , e,f,m.max(e,f); Console.WriteLine (max(0,1,2)= 3 ,e,f,g, m.max(e,f,g); Console.Read (); 【例3.16】运行结果如图3.20所示。图图3.20 重载重载max方法的运行结果方法的运行结果从上例可以看出,方法max是求若干参数中最大值,类Myclass中有四个同名的方法max,它们或参数个数不一样,或参数类型不一样。在调用max方法时,编译器会根据调用时给出的实参个数及类型调用相应的方法,这就是编译时实现的多态。多态是面向对象编程语言
45、的特性之一,重载是多态的形式之一,在C#中,最常用的重载就是方法重载。3.3.4 方法的重载与覆盖方法的重载与覆盖【例例3.17】下面程序定义了一个基类Shape,含有字段域width和height分别表示形状的宽和高,并定义了一个area方法求形状的面积。它的派生类Triangle和Trapezia都用关键字new修饰了area方法。using System;class Shape protected double width; protected double height; public Shape( ) width=height=0; public Shape(double x) wi
46、dth=height=x; public Shape(double w, double h ) width=w; height=h; public double area ( ) return width*height; 【例3.17】class Triangle : Shape / 三角形 public Triangle (double x, double y):base(x,y)new public double area ( ) / 派生类方法与基类方法同名,编译时会有警告信息return width*height/2 ; class Trapezia : Shape / 梯形doubl
47、e width2 ;public Trapezia(double w1, double w2, double h) : base (w1, h)width2=w2; new public double area ( ) / 加new隐藏基类的area方法【例3.17】 return (width+width2)*height/2 ; class Teststatic void Main() Shape A = new Shape ( 2, 4 );Triangle B = new Triangle (1,2);Trapezia C = new Trapezia (2, 3, 4);Consol
48、e.WriteLine (A.area= 0 , A.area(); / 调Shape的area方法Console.WriteLine (B.area= 0 , B.area(); / 调Triangle的area方法Console.WriteLine (C.area= 0 , C.area(); / 调Trapezia的area方法A=B; / 在C#中,基类的引用也能够引用派生类对象Console.WriteLine (A.area= 0 , A.area(); / 调Shape的area方法A=C;Console.WriteLine (A.area= 0 , A.area(); / 调S
49、hape的area方法Console.Read ();【例3.17】运行结果如图3.21所示。图图3.21 继承结构中的方法重载的运行结果继承结构中的方法重载的运行结果从例中可以看出,使用关键字new修饰方法,可以在一个继承的结构中隐藏有相同签名的方法。但是正如程序中演示的基类对象A被引用到派生类对象B时,它访问的仍是基类的方法。更多的时候,我们期望根据当前所引用的对象来判断调用哪一个方法,这个判断过程是在运行时进行的。 3.3.4 方法的重载与覆盖方法的重载与覆盖【例例3.18】将例3.17改写,在Shape类中方法area用virtual修饰,而派生类Triangle和Trapezia用关
50、键字override修饰area方法,这样就可以在程序运行时决定调用哪个类的area方法。using System;class Shape protected double width; protected double height; public Shape( ) width=height=0; public Shape(double x) width=height=x; public Shape(double w, double h ) width=w; height=h; public virtual double area ( ) / 基类中用virtual修饰符声明一个虚方法 re
51、turn width*height; 【例3.18】class Triangle: Shape / 三角形 public Triangle (double x, double y):base(x,y)public override double area ( ) / 派生类中用override修饰符覆盖基类虚方法return width*height/2 ; class Trapezia : Shape / 梯形double width2 ;public Trapezia(double w1, double w2, double h) : base (w1, h)width2=w2; publ
52、ic override double area ( ) / 派生类中用override修饰符覆盖基类虚方法 【例3.18】 return (width+width2)*height/2 ; class Teststatic void Main() Shape A = new Shape ( 2, 4 );Triangle B = new Triangle (1,2);Trapezia C = new Trapezia (2, 3, 4);Console.WriteLine (A.area= 0 , A.area(); / 调Shape的area方法Console.WriteLine (B.ar
53、ea= 0 , B.area(); / 调Triangle的area方法Console.WriteLine (C.area= 0 , C.area(); / 调Trapezia的area方法A=B; Console.WriteLine (A.area= 0 , A.area(); / 调Triangle的area方法A=C;Console.WriteLine (A.area= 0 , A.area(); / 调Trapezia的area方法Console.Read (); 【例3.18】return (width+width2)*height/2 ; class Teststatic void
54、 Main() Shape A = new Shape ( 2, 4 );Triangle B = new Triangle (1,2);Trapezia C = new Trapezia (2, 3, 4);Console.WriteLine (A.area= 0 , A.area(); / 调Shape的area方法Console.WriteLine (B.area= 0 , B.area(); / 调Triangle的area方法Console.WriteLine (C.area= 0 , C.area(); / 调Trapezia的area方法A=B; Console.WriteLin
55、e (A.area= 0 , A.area(); / 调Triangle的area方法A=C;Console.WriteLine (A.area= 0 , A.area(); / 调Trapezia的area方法Console.Read (); 【例3.18】运行结果如图3.22所示。 图图3.22 用用virtual和和override实现的方法重载的运行结果实现的方法重载的运行结果从例中可以看到,由于area方法在基类被定义为虚方法又在派生类中被覆盖,所以当基类的对象引用A被引用到派生类对象时,调用的就是派生类覆盖的area方法。一个重载方法可以通过base引用被重载的基方法。例如:cla
56、ss Triangle : Shape / 三角形 public override double area ( ) / 派生类中用override修饰符覆盖基类虚方法 double s; s = base.area( ); / 调用基类的虚方法return s/2 ; 3.3.4 方法的重载与覆盖方法的重载与覆盖在类的层次结构中,只有使用override修饰符,派生类中的方法才可以覆盖(重载)基类的虚方法,否则就是隐藏基类的方法。具体使用过程应注意以下几点:(1) 不能将虚方法声明为静态的,因为多态性是针对对象的,不是针对类的。不能将虚方法声明为私有的,因为私有方法不能被派生类覆盖。(3) 覆
57、盖方法必须与它相关的虚方法匹配,也就是说,它们的方法签名(方法名称、参数个数、参数类型)、返回类型以及访问属性等都应该完全一致。(4) 一个覆盖方法覆盖的必须是虚方法,但它本身又是一个隐式的虚方法,所以它的派生类还可以覆盖这个方法。不过尽管如此还是不能将一个覆盖方法显式地声明为虚方法。 3.4 属性属性 为了实现良好的数据封装和数据隐藏,类的字段成员的访问属性一般设置成private或protected,这样在类的外部就不能直接读写这些字段成员了,通常的办法是提供public级的方法来访问私有的或受保护的字段。【例例3.19】下面程序中的TextBox类提供公共方法set_text和get_t
58、ext来访问私有数据text。using System;class TextBox private string text; private string fontname; private int fontsize; private bool multiline; public TextBox ( )【例例3.19】 text=text1; fontname=宋体; fontsize=12; multiline=true; public void set_text(string str) text = str; public string get_text( ) return text; 【
59、例例3.19】class Test static void Main() TextBox Text1= new TextBox (); Console.WriteLine (Text1.text= 0 , Text1.get_text( ); Text1.set_text(这是文本框); Console.WriteLine (Text1.text= 0 , Text1.get_text( ); Console.Read (); 运行结果如图3.23所示。图图3.23 程序的运行结果程序的运行结果3.4 属性属性但C#提供了属性(property)这个更好的方法,把字段域和访问它们的方法相结合。
60、对类的用户而言,属性值的读写与字段域语法相同,对编译器来说,属性值的读写是通过类中封装的特别方法get访问器和set访问器实现的。属性的声明语法形式:语法形式: 属性集信息 opt 属性修饰符 opt 类型 成员名 访问器声明 其中:其中:属性修饰符:属性修饰符与方法修饰符相同,包括new、static、virtual、abstract、override和四种访问修饰符的合法组合,它们遵循相同的规则。类型:指定该声明所引入的属性的类型。成员名:指定该属性的名称。访问器声明:声明属性的访问器,可以是一个get访问器或一个set访问器,或者两个都有。3.4 属性属性访问器声明语法形式:语法形式:g
61、et / 读访问器 / 访问器语句块set / 写访问器 / 访问器语句块get访问器的返回值类型与属性的类型相同,所以在语句块中的return语句必须有一个可隐式转换为属性类型的表达式。set访问器没有返回值,但它有一个隐式的值参数,其名称为value,value的类型与属性的类型相同。同时包含get和set访问器的属性是读写属性,只包含get访问器的属性是只读属性,只包含set访问器的属性是只写属性。3.4 属性属性【例例3.20】改写例3.19,对TextBox类的text、fontname、fontsize、multiline域提供属性方式的读写访问。using System;clas
62、s TextBoxprivate string text;private string fontname;private int fontsize;private bool multiline;public TextBox ( )text=text1;fontname=宋体;fontsize=12;multiline=false; 【例3.20】 public string Text / Text属性,可读可写get return text;set text=value;public string FontName / FontName属性,只读属性get return fontname;pu
63、blic int FontSize / FontSize属性,可读可写get return fontsize;set fontsize = value;【例3.20】 public bool MultiLine / MultiLine属性,只写set multiline = value; class Teststatic void Main() TextBox Text1= new TextBox ();/ 调用Text属性的get访问器Console.WriteLine (Text1.Text= 0 , Text1.Text); Text1.Text = 这是文本框 ; / 调用Text属性
64、的set访问器Console.WriteLine (Text1.Text= 0 , Text1.Text); 【例3.20】 Console.WriteLine (Text1.Fontname= 0 , Text1.FontName); Text1.FontSize=36;Text1.MultiLine = true;Console.WriteLine (Text1.FontSize= 0 , Text1.FontSize); Console.Read ();运行结果如图3.24所示。图图3.24 程序的运行结果程序的运行结果3.4 属性属性属性是字段的自然扩展,当然属性也可作为特殊的方法使用
65、,并不要求它和字段域一一对应,所以属性还可以用于各种控制和计算。例如:【例例3.21】下面程序中定义的Label类设置了Width和Heigh属性用于计算两点之间的宽和高。using System;class Point int x, y;public int Xget return x; public int Ygetreturn y; 【例例3.21】public Point ( ) x=y=0;public Point (int x, int y)this.x=x;this.y=y;class Label Point p1=new Point ( );Point p2=new Point
66、 (5, 10);public int Width / 计算两点之间的宽度get return p2.X-p1.X;【例例3.21】 public int Height / 计算两点之间的高度get return p2.Y-p1.Y;class Teststatic void Main() Label Label1= new Label ();Console.WriteLine (Label1.Width= 0 , Label1.Width); Console.WriteLine (Label1.Height= 0 , Label1.Height);Console.Read (); 【例例3.
67、21】运行结果如图3.25所示。图图3.25 程序的运行结果程序的运行结果最后在使用属性时应注意,尽管属性与字段域有相同的使用语法,但它本身并不代表字段域。属性不直接对应存储位置,所以不能把它当变量使用,不能把属性作为ref或者out参数传递。属性和方法一样也有静态修饰,在静态属性的访问器中不能访问静态数据也不能引用this。 3.5 综合应用实例综合应用实例【例例3.22】 学生成绩管理程序。根据学生选修的课程以及课程学分和课程成绩计算GPA,最后可以按GPA的值对学生进行排序。【基本思路基本思路】 本程序的学生总人数、课程名、课程学分可以由控制台输入,为了简单起见,假定每个学生所选修的课程
68、相同。Course类定义了课程名、课程学分字段域。Student类定义学生姓名、学号、选修课程数、Course类、成绩以及GPA等字段。因为选修课程假定一样,所以将课程数、Course类对象定义为static字段,不需要每个学生都有这份数据拷贝。Course类定义了Name属性、构造函数。Student类定义了CourseNum静态属性、GPA属性、Name属性,SetCourse方法用于设置课程名,因为不需要为每个学生设置所以定义成静态方法。AddData属性用于给每个学生加入姓名、学号、成绩。ComputeGPA 方法用于计算学生成绩的GPA。stuSwap方法是对两个Student对象内
69、容交换。Test类MaxMinGPA方法求最大和最小的GPA值,SortGPA方法是按学生的GPA的值对Student类对象数组进行排序。【例例3.22】【程序程序】using System;class Coursestring courseName; / 课程名 int courseMark; / 课程学分public Course() public Course(string Name, int Mark)courseName=Name;courseMark=Mark;【例例3.22】 public string Name / Name属性,课程名可读可写get return course
70、Name;set courseName=value;public int Mark / Mark属性,课程学分可读可写get return courseMark;set courseMark=value; 【例例3.22】class Studentstring stuName; / 学生姓名string stuID; / 学生学号static int numberOfCourse; / 加static修饰表明这个域为所有学生类对象共享。static Course list ; / Course类对象数组,用于设置每门课程名、课程学分int stuScore; / 每个学生对象要填写的各课程成绩
71、double stuGPA; / GPA值public Student() / 当第一次创建Student对象时,创建list对象数组,并初始化。list=new CoursenumberOfCourse; for (int i=0; inumberOfCourse;i+)listi=new Course();stuScore=new intnumberOfCourse; / 将CourseNum定义成静态属性是因为它只对静态域进行操作【例3.22】 public static int CourseNum get return numberOfCourse;set numberOfCourse
72、=value;public double GPA / GPA属性是只读属性getreturn stuGPA;public string Name / Name属性可读可写 get return stuName;setstuName=value;【例3.22】/ 将SetCourse设为静态方法,是因为它仅访问静态数据域,/ 不需要创建Student类对象就可直接用Student类名调用。/ 它的形参是一个参数数组,这样调用时就可根据实际选修的课程数来设置。public static void SetCourse(params Course topic)for (int i=0; itopic.
73、Length ; i+)listi.Name =topici.Name;listi.Mark =topici.Mark;/ AddData方法将一个学生的数据添加到学生类对象数组中public void AddData(string name,string Id,int score)stuName=name;stuID=Id;for (int i=0;iscore.Length; i+)stuScorei=scorei;【例例3.22】public void ComputeGPA() / 根据课程的学分以及学生成绩计算GPAint i;double sMark, sumMark=0, sumG
74、P=0;for (i=0;i=95) sMark=4.5; else if (stuScorei=90) sMark=4; else if (stuScorei=85) sMark=3.5; else if (stuScorei=80) sMark=3; else if (stuScorei=75) sMark=2.5; 【例例3.22】 else if (stuScorei=70) sMark=2; else if (stuScorei=65) sMark=1.5; else if (stuScorei=60) sMark=1; else sMark=0;sumGP+=listi.Mark*
75、sMark;sumMark+=listi.Mark;stuGPA=sumGP/sumMark; / stuSwap方法提供两个Student类对象的交换操作,注意它们的形参被修饰为refpublic void stuSwap(ref Student stu1, ref Student stu2) 【例例3.22】 string name,Id;int i;int score=new int Student.CourseNum;double gpa;name=stu1.Name;Id=stu1.stuID;gpa=stu1.GPA;for (i=0;iStudent.CourseNum;i+)s
76、corei=stu1.stuScorei;stu1.stuName=stu2.stuName;stu1.stuID=stu2.stuID;stu1.stuGPA=stu2.stuGPA;for (i=0;iStudent.CourseNum;i+)stu1.stuScorei=stu2.stuScorei;【例例3.22】 stu2.stuName=name;stu2.stuID=Id;stu2.stuGPA=gpa;for (i=0;iStudent.CourseNum;i+)stu2.stuScorei=scorei;class Testpublic static void Main()
77、Test T=new Test ();int i,j,Num,Mark;string sline,Name,Id;double sMax,sMin;Console.Write(请输入学生总人数);sline=Console.ReadLine(); / 从控制台接受学生总人数【例例3.22】 Num=int.Parse(sline); / 将string类型转换成int类型Console.Write(请输入选修课程总数);sline=Console.ReadLine();Student.CourseNum=int.Parse(sline); / CourseNum是Student的静态属性 St
78、udent Stu=new Student Num; / 根据输入的学生总人数,动态地创建对象for (i=0;iNum;i+) / 对Student类的对象数组做初始化Stui=new Student ();Course tp=new CourseStudent.CourseNum; / 根据课程数创建Course类对象数组int score=new int Student.CourseNum;for (i=0;iStudent.CourseNum; i+) / 具体输入每门课名称、学分【例3.22】 Console.Write(请输入选修课程名);Name=Console.ReadLine
79、();Console.Write(请输入选修课程学分);sline=Console.ReadLine(); Mark=int.Parse(sline); tpi=new Course (Name,Mark); / 根据课程名、学分对Course数组做初始化Student.SetCourse(tp); / 用类名调用Student的静态方法SetCousefor (i=0;iNum;i+) / 输入学生姓名、学号、各门课成绩 Console.Write(请输入学生姓名);Name=Console.ReadLine(); Console.Write(请输入学号);Id=Console.ReadLi
80、ne();for (j=0;jStudent.CourseNum;j+)【例3.22】 Console.Write(请输入0课程的成绩,tpj.Name);sline=Console.ReadLine();scorej=int.Parse(sline);Stui.AddData(Name,Id,score); / 将当前输入的一个学生数据加到对象数组中Stui.ComputeGPA(); / 计算当前这个学生的GPAConsole.WriteLine(你的GPA值是:0:F2,Stui.GPA);T.MaxMinGPA(out sMax, out sMin,Stu); / 计算GPA的最大值和
81、最小值Console.WriteLine(GPA最高为0:F2, 最低为:1:F2, sMax,sMin);Console.WriteLine(按GPA从高到低输出:);T.SortGPA(ref Stu);for (i=0; iNum; i+)Console.WriteLine( 0 ,1:F2, Stui.Name, Stui.GPA);Console.Read (); 【例例3.22】/ MaxMinGPA方法用于计算Student类对象数组中GPA的最大值和最小值/ 它的形参max和min被修饰为out型,表明它的实参不需要做初始化, / 它会从方法中获得返回值public void
82、MaxMinGPA(out double max, out double min, Student stu)if (stu.Length=0)max=min=-1;return;max=min=stu0.GPA; for (int i=1; istu.Length; i+)if (maxstui.GPA) min=stui.GPA;【例例3.22】/ SortGPA方法按选择排序法对Student类对象数组排序,/ 当需要交换时,再调用Student的stuSwap方法。/ 请注意它的形参被修饰为ref,而在方法体内调用stuSwap方法时实参也要修饰refpublic void SortGPA(ref Student stu) int i,j,pos;for (i=0;istu.Length-1;i+) for (j=(pos=i)+1; jstu.Length;j+)if (stupos.GPAstuj.GPA)pos=j;if (pos!=i)stui.stuSwap(ref stui,ref stupos);【例例3.22】运行结果如图3.26所示。 图图3.26 学生成绩管理程序的运行结果学生成绩管理程序的运行结果作业: P301页: 1.1,1.2 ,1.3 ,1.4 2.1 ,2.3, 2.4 ,2.5 3.1, 3.2, 3.4, 3.5