870 likes | 994 Views
第三章 C# 面向对象初级编程. 面向对象得程序设计越来越受到编程人员的喜爱。类和对象是面向对象程序设计中的重要概念。封装性、继承性和多态性是面向对象的特点,本章旨在全面说明 C# 编写面向对象程序设计的方法。. 3.1 面向对象的基本概念.
E N D
第三章 C#面向对象初级编程 面向对象得程序设计越来越受到编程人员的喜爱。类和对象是面向对象程序设计中的重要概念。封装性、继承性和多态性是面向对象的特点,本章旨在全面说明C#编写面向对象程序设计的方法。
3.1 面向对象的基本概念 早期的程序设计方法多为面向过程的程序设计思想(POP,Procedure-Oriented Programming),在这种设计方法下,编程人员的主要任务是把一个处理分解成若干个过程,然后编写这些过程。每个过程都基于某些特定的算法。对于C语言来说就是编写一个个函数,每个函数的数据和程序代码是分离的,当修改某段程序时,所有与之有关的部分都需要作相应的调整。随着问题规模的增大,程序变得容易出错,而且越来越难以管理。这种面向过程的程序设计语言有C、Pascal、Basic等。 面向对象的程序设计(OOP,Object-Oriented Programming)则是一种基于结构分析的、以数据为中心的程序设计方法。它的主要思想是将数据及处理这些数据的操作都封装(Encapsulation)到一个称为类(Class)的数据结构中。使用这个类时,只需要定义一个类的变量即可,这个变量叫做对象(Object)。通过调用对象的数据成员完成对类的使用。这类编程思想较好地适应了现实世界中的问题,因而得以广泛应用。
3.1.1 对象和类 在我们的日常生活中,对象是指可辨识的一种实体。例如汽车、房子、书、文档和支票等。为了进一步说明,可以把对象看作是用于在程序中表示的一个实体。因此对象可以包括把有生命的“对象”——人、员工、客户,以及更抽象的“对象”——公司、数据库和国家等。 面向对象程序设计(OOP)通过使用真实世界的对象实例概念,改变了应用程序处理问题的模式。非面向对象的传统程序语言,必须针对特定问题开发应用程序,一旦所处理的问题脱离了原先考虑设计的范围,就得扩充应用程序得功能以面对新产生的问题;而在这种情形下,整个应用程序不是大幅改写就是必须重新开发。OOP程序设计理论的出现,解决了这样的缺陷,其中关键就在于对象的使用。 我们可以设计不同的对象,处理应用程序所要解决的各种问题,其中最大的好处在于应用程序可以轻易地针对新的问题进行扩充处理,只要修改原有的对象或是加入新定义的对象,完全不用重新改写原有的应用程序。
OOP得对象概念,使得开发应用程序得复杂度与难度,不会随着程序规模的扩大,变得难以处理与掌握。OOP得对象概念,使得开发应用程序得复杂度与难度,不会随着程序规模的扩大,变得难以处理与掌握。 类是一组具有相同数据结构和相同操作的对象的集合。类是一系列具有相同性质的对象的抽象,是对对象共同特征的描述。比如每一辆汽车是一个对象的话,所有的汽车可以作为一个模板,我们就定义汽车这个类。 在一个类中,每个对象都是类的实例,可以使用类中提供的方法。从类定义中产生对象,必须有建立实例的操作,C++和C#中的new操作符可用于建立一个类的实例,C#为我们提供的方法则更加安全。
3.1.2 继承 继承是使用己存在的定义作为基础建立新定义的技术。新类的定义可以是即存类所声明的数据和新类所增加的声明组合。新类复用即存的定义,而不要求修改即存类。即存类可以作为基类来引用,而新类可以作为派生类来引用。这种复用技术大大降低了软件的开发费用。 例如,汽车作为一个类己经存在,作为具有自身特征的卡车就可以从汽车类中继承。它同汽车一样,具有颜色、重量这些特征,可以行驶和鸣笛。它还具有一般汽车不一定具有的特征,比如可以载货等。
3.1.3 封装 一般来说,程序员都力求软件工程系统的高集成性。一个具有高集成性的软件系统包含着各种执行独立任务的成分,而每一个独立任务都是整个系统的重要组成部分。相反,如果一个软件系统的集成性差,那么系统所包含的各种成分由于没有很好的被定义而往往会容易发生冲突。 封装可以将对象相关的信息集中存放在一个独立的单元中,因此,用一个标识符就可以访问对象,还可以把整个对象作为一个变量参数传送给函数。
3.1.4 多态 多态性是指用一个名字定义不同的函数,这函数执行不同但又类似的操作,从而实现“一个接口,多种方法”。 多态性的实现与静态联编、动态联编有关。静态联编支持的多态性称为编译时的多态性,也称静态多态性,它是通过函数重载和运算符重载实现的。动态联编支持的多态性称为运行时的多态性,也称动态多态性,它是通过继承和虚函数实现的。
3.2 C#中的类与对象 3.2.1 在C#中定义类 类的声明格式如下: [类修饰符] class 类名 [:基类名] { 类的成员; } 类的修饰符可以是以下几种之一或者是它们的组合(在类的声明中同一修饰符不允许出现多次):
◆new——新建类,仅允许在嵌套类声明时使用,表明类中隐藏了由基类中继承而来的、与基类中同名的成员。◆new——新建类,仅允许在嵌套类声明时使用,表明类中隐藏了由基类中继承而来的、与基类中同名的成员。 ◆public——公有类,表示不限制对该类的访问。 ◆protected一—保护类,表示只能从所在类和所在类派生的子类进行访问。 ◆internal——内部类,只有其所在类才能访问。 ◆private——私有类,只有对包.Net中的应用程序或库才能访问。 ◆abstract——抽象类,不允许建立类的实例。 ◆sealed——密封类,不允许被继承。 以上类修饰符可以两个或多个。
使用new关键字可以建立类的一个实例,比如下面的代码:使用new关键字可以建立类的一个实例,比如下面的代码: class A { } class B { void Fun { A a; a=new A(); } } 在类B的方法Fun中创建了一个类A的实例。 我们使用如下代码表示类B从类A中继承: class A { } class B: A{ } 有关C#中的继承机制我们放在后面的章节中进行详细讨论,在这里要事先声明的一点是:C#中的类只支持单继承。
3.2.2 访问修饰符 类的成员有以下类型: ◆成员常量,代表与类相关的常量值。 ◆域,即类中的变量。 ◆成员方法,完成类中各种计算或功能的操作。 ◆属性,用于定义类中的值,并对它们提供读、写操作。 ◆事件,用于说明发生了什么事情。 ◆索引指示器,允许编程人员在访问数组时,通过索引指示器访问类的多个实例。 ◆操作符,定义类中特有的操作。 ◆构造函数,在类被实例化时首先执行的函数,主要是完成对象初始化操作。 ◆析构函数,在对象被销毁之前最后执行的函数,主要是完成对象结束时的收尾操作。
包含有可执行代码的成员被认为是类中的函数成员,这些函数成员有方法、属性、索引指示器、操作符、构造函数和析构函数。包含有可执行代码的成员被认为是类中的函数成员,这些函数成员有方法、属性、索引指示器、操作符、构造函数和析构函数。 在编写程序时,我们可以对类的成员使用不同的访问修饰符,从而定义它们的访问级别。 (1)公有成员 C#中的公有成员提供了类的外部界面,允许类的使用者从内部或外部直接进行访问。公有成员的修饰符为public,这是限制最少的一种访问方式。它的优先是使用灵活,缺点是外界可能会破坏对象成员值得合理性。 (2) 私有成员 C#中的私有成员仅限于类中的成员可以访问,从类的外部访问私有成员是不合法的。如果在声明中没有出现成员的访问修饰符,按照默认方式成员为私有的。私有成员的修饰符为private。 (3)保护成员 为了方便派生类的访问,又希望成员对于外界是隐藏的,这时可以使用protected修饰符,声明成员为保护成员。 (4)内部成员 使用internal修饰符的类的成员是一种特殊的成员。这种成员对于同一包中的应用程序或库是透明的,而在包.Net之外是禁止访问的。
使用下面的例子说明一下类的成员的访问修饰符的用法。使用下面的例子说明一下类的成员的访问修饰符的用法。 程序清单: class ClassA { public int a; private int b; protected int c; public void SetA() { a=1;//正确,允许访问类自身公有成员 b=2;//正确,允许访问类自身私有成员 c=3;//正确,允许访问类自身保护成员 } } class ClassB:A { public void SetB() {
ClassA BaseA=new ClassA(); BaseA.a=11;//正确,允许访问基类公有成员 BaseA.b=22;//错误,不允许访问基类私有成员 BaseA.c=33;//正确,允许访问基类保护成员 } } class ClassC { public void AetB() { ClassA BaseA=new ClassA(); BaseA.a=111;//正确,允许访问类的其他公有成员 BaseA.b=222;//错误,不允许访问类的其他私有成员 BaseA.c=333;//错误,不允许访问类的其他保护成员 } }
(5) this保留字 保留字this仅限于在构造函数、类的方法和类的实例中使用,它有以下含义: ◆在类的构造函数中出现的this作为一个值类型,它表示对正在构造的对象本身的引用。 ◆在类的方法中出现的this作为一个值类型,它表示对调用该方法的对象的引用。 ◆在结构的构造函数中出现的this作为一个变量类型,它表示对正在构造的结构的引用。 ◆在结构的方法中出现的this作为一个变量类型,它表示对调用该方法的结构的引用。 除此以外,在其已地方使用this保留字都是不合法的。
案例:this保留字的使用目标:学习保留字this的使用方法案例:this保留字的使用目标:学习保留字this的使用方法 步骤: 1、启动VS.NET,新建一个控制台应用程序,名称填写为“ThisTest”,位置设置为“c:\CSharpSamples\chp3。 2、在代码设计窗口中编辑Class1.cs。在其中的代码如下: using System; namespace thistest { class Class1 { public int x; public void aaa() {
x=5; Console.WriteLine("The value of x is: {0}",x); Console.WriteLine("The value of this. is: {0}", this.x); } static void Main(string[] args) { Class1 bb=new Class1(); bb.aaa(); } } }
图 3-1 程序运行结果 5、按Ctrl + F5编译并运行该程序,效果如图3-1所示。图 3-1 程序运行结果 实际上,在C#内部,this被定义为一个常量。因此,使用this++,this--这样的语句都是不合法的。但是,this可以作为返回值来使用。
3.2.3 实例化对象与构造函数 若将类中的某个成员声明为static该成员称为静态成员。类中的成员要么是静态,要么是非静态的。一般说来,静态成员是属于类所有的,非静态成员则属于类的实例化对象。
以下示例代码演示了如何声明静态和非静态成员。以下示例代码演示了如何声明静态和非静态成员。 程序清单: class Test { int x; static int y; void F() { x=1;// 正确,等价于this.x=1 y=1;// 正确,等价于Test.y=1 } static void G() { x=1;//错误,不能访问this.x y=1;//正确,等价于Test.y=1 }
static void Main(string[] args) { Test.t=new Test(); t.x=1;//正确 t.y=1;//错误,不能在类的实例中访问静态成员 Test.x=1;//错误,不能按类访问非静态成员 Test.y=1;//正确 } }
类的非静态成员属于类的实例所有,每创建一个类的实例,都在内存中为非静态成员开辟了一块区域。而类的静态成员属于类所有,为这个类的所有实例所共享。无论这个类创建了多少个副本,一个静态成员在内存中只占有一块区域。类的非静态成员属于类的实例所有,每创建一个类的实例,都在内存中为非静态成员开辟了一块区域。而类的静态成员属于类所有,为这个类的所有实例所共享。无论这个类创建了多少个副本,一个静态成员在内存中只占有一块区域。 构造函数用于执行类的实例的初始化。每个类都有构造函数,即使我们没有声明它,编译器也会自动地为我们提供一个默认的构造函数。在访问一个类的时候,系统将最先执行构造函数中的语句。实际上,任何构造函数的执行都隐式地调用了系统提供默认的构造函数base()。
如果我们在类中声明了如下的构造函数, C(…){…} 它等价于: C(…):base(){…} 使用构造函数请注意以下几个问题: ◆一个类的构造函数通常与类名相同。 ◆构造函数不声明返回类型。 一般地,构造函数总是public类型的。如果是private类型的,表明类不能被实例化,这通常用于只含有静态成员的类。
下面的例子示范了构造函数的使用: class A { int x=0,y=0, count; public A() { count=0; } public A(int vx,int vy) { x=vx; y=vy; } }
构造函数的名字不能随便起,必须让编译器认得出才可以被自动执行。它的命名方法既简单又合理:让构造函数与类同名。除了名字外,构造函数的另一个特别之处是没有返回值类型,这与返回值类型为void的函数不同。如果它有返回值类型,那么编译器将不知所措。在你可以访问一个类的方法、属性或任何其它东西之前, 第一条执行的语句是包含有相应类的构造函数。甚至你自己不写一个构造函数,也会有一个缺省构造函数提供给你。
class TestClass { public TestClass(): base() {} // 由CLR提供 } 下面列举了几种类型的构造函数。 1)缺省构造函数 class TestClass { public TestClass(): base() {} } 上面已介绍,它由系统(CLR)提供。
2)实例构造函数 实例构造函数是实现对类中实例进行初始化的方法成员。如: using System; class Point { public double x, y; public Point() { this.x = 0; this.y = 0; } public Point(double x, double y) { this.x = x; this.y = y; } …… }
class Test { static void Main() { Point a = new Point(); Point b = new Point(3, 4); // 用构造函数初始化对象 …… } }
声明了一个类Point,它提供了两个构造函数。它们是重载的。一个是没有参数的Point构造函数和一个是有两个double参数的Point构造函数。如果类中没有提供这些构造函数,那么会CLR会自动提供一个缺省构造函数的。但一旦类中提供了自定义的构造函数,如Point()和Point(double x, double y),则缺省构造函数将不会被提供,这一点要注意。
3)静态构造函数 静态构造函数是实现对一个类进行初始化的方法成员。它一般用于对静态数据的初始化。静态构造函数不能有参数,不能有修饰符而且不能被调用,当类被加载时,类的静态构造函数自动被调用。如: using System.Data; class Employee { private static DataSet ds; static Employee() { ds = new DataSet(...); } …... }
声明了一个有静态构造函数的类Employee。注意静态构造函数只能对静态数据成员进行初始化,而不能对非静态数据成员进行初始化。但是,非静态构造函数既可以对静态数据成员赋值,也可以对非静态数据成员进行初始化。声明了一个有静态构造函数的类Employee。注意静态构造函数只能对静态数据成员进行初始化,而不能对非静态数据成员进行初始化。但是,非静态构造函数既可以对静态数据成员赋值,也可以对非静态数据成员进行初始化。 如果类仅包含静态成员,你可以创建一个private的构造函数:private TestClass() {…},但是private意味着从类的外面不可能访问该构造函数。所以,它不能被调用,且没有对象可以被该类定义实例化。
以上是几种类型构造函数的简单运用,下面将重点介绍一下在类的层次结构中(即继承结构中)基类和派生类的构造函数的使用方式。派生类对象的初始化由基类和派生类共同完成:基类的成员由基类的构造函数初始化,派生类的成员由派生类的构造函数初始化。以上是几种类型构造函数的简单运用,下面将重点介绍一下在类的层次结构中(即继承结构中)基类和派生类的构造函数的使用方式。派生类对象的初始化由基类和派生类共同完成:基类的成员由基类的构造函数初始化,派生类的成员由派生类的构造函数初始化。 当创建派生类的对象时,系统将会调用基类的构造函数和派生类的构造函数,构造函数的执行次序是:先执行基类的构造函数,再执行派生类的构造函数。如果派生类又有对象成员,则,先执行基类的构造函数,再执行成员对象类的构造函数,最后执行派生类的构造函数。
至于执行基类的什么构造函数,缺省情况下是执行基类的无参构造函数,如果要执行基类的有参构造函数,则必须在派生类构造函数的成员初始化表中指出。如:至于执行基类的什么构造函数,缺省情况下是执行基类的无参构造函数,如果要执行基类的有参构造函数,则必须在派生类构造函数的成员初始化表中指出。如: class A { private int x; public A( ) { x = 0; } public A( int i ) { x = i; } } class B : A { private int y; public B( ) { y = 0; } public B( int i ) { y = i; } public B( int i, int j ):A(i) { y = j; } }
B b1 = new B(); //执行基类A的构造函数A(),再执行派生类的构造函数B() B b2 = new B(1); //执行基类A的构造函数A(),再执行派生类的构造函数B(int) B b3 = new B(0,1); //执行执行基类A的构造函数A(int),再执行派生类的构造函数B(int,int) 在这里构造函数的执行次序是一定要分析清楚的。另外,如果基类A中没有提供无参构造函数public A( ) { x = 0; },则在派生类的所有构造函数成员初始化表中必须指出基类A的有参构造函数A(i),如下所示:
class A { private int x; public A( int i ) { x = i; } } class B : A { private int y; public B():A(i) { y = 0; } public B(int i):A(i) { y = i; } public B(int i, int j):A(i) { y = j; } }
3.2.4 方法重载 方法是类中用于执行计算或其它行为的成员。我们看一下方法的声明格式: 方法修饰符 返回类型 方法名(方法参数列表) { 方法实现部分; }
1、修饰符 方法的修饰符可以是:new、public、protected、internal、private、static、virtual、sealed、override、abstract和extern几种。 如果修饰符为static则表明这个方法只能访问类中的静态成员,没有修饰符static的方法可以访问类中任意成员。 如果修饰符为virtual,则称这个方法为虚方法,反之称为非虚方法。对于非虚方法,无论是被用此类定义的对象调用,还是被这个类的派生类定义的对象调用,方法的执行方式不变。对于虚方法,它的执行方式可以被派生类改变,这种改变是通过重载实现的。 如果修饰符为extern,则表示这个方法是外部方法。
2、返回值 方法的返回值的类型可以是合法的C#的数据类型。C#在方法的执行部分通过return语句得到返回值。
案例:求最大值、最小值。目标:掌握方法的格式使用案例:求最大值、最小值。目标:掌握方法的格式使用 步骤: 1、启动VS.NET,新建一个控制台应用程序,名称填写为“MaxandMinTest”,位置设置为“c:\CSharpSamples\chp3”。 2、在代码设计窗口中编辑Class1.cs。在其中的代码如下: using System; namespace MaxandMinTest { class Test { public static int max(int x ,int y) { if(x>y) return x; else return y; }
public static void WriteMin(int x,int y) { int temp=x; if(x>y) temp=y; Console.WriteLine("the min of {0} and {1} is:{2}。",x,y,temp); return; } public static void Main() { Console.WriteLine("the max of 6 and 8 is: {0}。",max(6,8)); WriteMin(6,8); } } }
图 3-2 程序运行结果 3、按Ctrl + F5编译并运行该程序,效果如图3-2所示。图 3- 2 程序运行结果
如果在return后不跟随任何值,方法返回值是void型的。如果在return后不跟随任何值,方法返回值是void型的。 类的成员方法的重载也是类似的。类中两个以上的方法(包括隐藏的继承而来的方法),取的名字相同,只要使用的参数类型或者参数个数不同,编译器便知道在何种情况下应该调用哪个方法,这就叫做方法的重载。 方法重载得格式就是在一个类中两次或多次定义同名的方法,这些同名的方法也包括从基类中继承而来的方法。这些方法的名称相同,但每个方法的参数类型或个数不同,这样便于系统进行区分。
其实,我们非常熟悉的Console类之所以能够实现对字符串进行格式化的功能,就是因为已定义了多个重载的成员方法:其实,我们非常熟悉的Console类之所以能够实现对字符串进行格式化的功能,就是因为已定义了多个重载的成员方法: public static void WriteLine(); public static void WriteLine(int); public static void WriteLine(float); public static void WriteLine(long); public static void WriteLine(uint); public static void WriteLine(char); public static void WriteLine(bool); public static void WriteLine(double); public static void WriteLine(char[]); public static void WriteLine(string); public static void WriteLine(Object); public static void WriteLine(ulong); public static void WriteLine(string, Object[]); public static void WriteLine(string, Object); public static void WriteLine(char[], int, int); public static void WriteLine(string, Object, Object); public static void WriteLine(string, Object, Object, Object);
案例:学生类中包含有学生姓名、性别、年龄、体重等信息。我们比较学生之间的年龄和体重。目标:说明重载的使用基本方法案例:学生类中包含有学生姓名、性别、年龄、体重等信息。我们比较学生之间的年龄和体重。目标:说明重载的使用基本方法 步骤: 1、启动VS.NET,新建一个控制台应用程序,名称填写为“StudentCompareTest”,位置设置为“c:\CSharpSamples\chp3”。 2、在代码设计窗口中编辑Class1.cs。在其中的代码如下: using System; namespace StudentCompareTest { class Student//定义学生类 { public string s_name; public int s_age; public float s_weight; public Student(string n,int a,float w) {
s_name=n; s_age=a; s_weight=w; } public int max_age(int x, int y) { if(x>y) return x; else return y; } public float max_weight(float x, float y) { if(x>y) return x; else return y; } }
class Test { public static void Main() { Student s1=new Student("Mike",21,70); Student s2=new Student("John",21,70); if(s1.max_age(s1.s_age,s2.s_age)==s1.s_age) Console.WriteLine("{0}'s age is bigger than {1}'s", s1.s_name,s2.s_name); else Console.WriteLine("{0}'s age is smaller than{1}'s", s1.s_name,s2.s_name); if(s1.max_weight(s1.s_weight,s2.s_weight)==s1.s_weight) Console.WriteLine("{0}'s weight is bigger than{1}'s", s1.s_name,s2.s_name); else Console.WriteLine("{0}'s weight is smaller than{1}'s",s1.s_name,s2.s_name); } } }
图 3-4 程序运行结果 5、按Ctrl + F5编译并运行该程序,效果如图3-4所示。
3.2.5 销毁对象与析构函数 在类的实例超出范围时,我们希望确保它所占的存储能被收回。C#中提供了析构函数,用于专门释放被占用的系统资源。 析构函数的名字与类名相同,只是在前面加了一个符号“~”。析构函数不接受任何参数,也不返回任何值。如果你试图声明其已任何一个以符号“~”开头而不与类名相同的方法,和试图让析构函数返回一个值一样,编译器都会产生一个错误。 析构函数不能是继承而来的,也不能显式地调用。当某个类的实例被认为不再有效,符合析构的条件,析构函数就可能在某个时刻被执行。C++的程序员常常需要在析构函数中写上一系列delete语句来释放存储,而在C#中,我们不必再为此担心了。垃圾收集器会帮助我们完成这些易被遗忘的工作。 虽然C#(更确切的说是CLR)提供了一种新的内存管理机制---自动内存管理机制(Automatic memory management),资源的释放是可以通过“垃圾回收器” 自动完成的,一般不需要用户干预,但在有些特殊情况下还是需要用到析构函数的,如在C#中非托管资源的释放。
资源的释放一般是通过“垃圾回收器”自动完成的,但具体来说,仍有些需要注意的地方:资源的释放一般是通过“垃圾回收器”自动完成的,但具体来说,仍有些需要注意的地方: 1、值类型和引用类型的引用其实是不需要什么“垃圾回收器”来释放内存的,因为当它们出了作用域后会自动释放所占内存,因为它们都保存在栈(Stack)中; 2、只有引用类型的引用所指向的对象实例才保存在堆(Heap)中,而堆因为是一个自由存储空间,所以它并没有像“栈”那样有生存期(“栈”的元素弹出后就代表生存期结束,也就代表释放了内存),并且要注意的是,“垃圾回收器”只对这块区域起作用。
然而,有些情况下,当需要释放非托管资源时,就必须通过写代码的方式来解决。通常是使用析构函数释放非托管资源,将用户自己编写的释放非托管资源的代码段放在析构函数中即可。需要注意的是,如果一个类中没有使用到非托管资源,那么一定不要定义析构函数,这是因为对象执行了析构函数,那么“垃圾回收器”在释放托管资源之前要先调用析构函数,然后第二次才真正释放托管资源,这样一来,两次删除动作的花销比一次大多的。下面使用一段代码来示析构函数是如何使用的:然而,有些情况下,当需要释放非托管资源时,就必须通过写代码的方式来解决。通常是使用析构函数释放非托管资源,将用户自己编写的释放非托管资源的代码段放在析构函数中即可。需要注意的是,如果一个类中没有使用到非托管资源,那么一定不要定义析构函数,这是因为对象执行了析构函数,那么“垃圾回收器”在释放托管资源之前要先调用析构函数,然后第二次才真正释放托管资源,这样一来,两次删除动作的花销比一次大多的。下面使用一段代码来示析构函数是如何使用的: public class ResourceHolder { …… ~ResourceHolder() { // 这里是清理非托管资源的用户代码段 } }