对象
=(算法
+数据结构
)
OOP程序=(对象+对象+……)
程序就是许多对象在计算机中相继表现自己,而对象则是一个个程序实体。
1. 封装性
封装: 是指将数据成员、属性、事件和方法(统称为成员)集合在一个整体里的过程。
隐藏: 对内部细节隐藏保护的能力,类内的某些成员可以以对外隐藏的特性被保护起来。从而保证了类具有较好的独立性,防止外部程序破坏类的内部数据,同时便于程序的维护和修改。
2. 继承性
继承: 利用现有类派生出新类的过程。
特点: 新类拥有原有类的特性,又增加了自身新的特性,设计程序时,只需对新增的内容或是对原内容的修改设计代码。
作用: 继承性可简化类和对象的创建工作量,增强代码的可重用性。
例如:有了人类,要定义学生类,只要增加一个学号属性。
3. 多态性
多态性:不同的类进行同一操作可以有不同的行为。同样的消息被不同类型的对象接收时导致完全不同的行为。多态性允许每个对象以适合自身的方式去响应共同的消息,不必为相同功能的操作作用于不同的对象而去特意识别。
例如,定义一个狗类和一个猫类,狗和猫都会叫(可定义方法bark),说明两者在这方面可以进行相同的操作,然而,狗和猫叫的行为是截然不同的,因为狗叫是“汪汪”,而猫叫是“喵喵” 。
类(class)是面向对象技术中最重要的一种数据结构,是指具有相同属性和操作的一组对象的抽象集合,它支持信息隐藏和封装,进而支持对抽象数据类型(Abstract Data Type,ADT)的实现。
类包含:数据成员(常量和域)、函数成员(方法、属性、事件、索引器、运算符、实例构造函数、析构函数和静态构造函数)和嵌套类型。
类的属性集 类的修饰符 关键字class 类名 继承方式 基类名
{
}
如:定义汽车类
public class Car
{
public string color;
public string brand;
}
C#中常用的类修饰符:
序号 | 修饰符 | 说明 |
---|---|---|
1 | public | 表示不限制对该类的访问 |
2 | protected | 表示只能对其所在类和所在类的子类进行访问 |
3 | internal | 只有其所在类才能访问,不允许外部程序集使用该类 |
4 | private | 只有.NET中的应用程序或库才能访问 |
5 | new | 仅允许在嵌套类声明时使用,表明类中隐藏了由基类中继承而来的、与基类中同名的成员 |
6 | abstract | 抽象类,不允许建立类的实例 |
7 | sealed | 密封类,不允许被继承 |
局部变量:在指定范围内有效。
字段:即类中的变量或常量,包括静态字段、实例字段、常量和只读字段。
方法成员:包括静态方法和实例方法。
属性:按属性指定的get方法和Set方法对字段进行读写。属性本质上是方法。
事件:代表事件本身,同时联系事件和事件处理函数。
索引指示器:允许像使用数组那样访问类中的数据成员。
操作符重载:采用重载操作符的方法定义类中特有的操作。
构造函数和析构函数。
包含可执行代码的成员被认为是类中的函数成员,这些函数成员有方法、属性、索引指示器、操作符重载、构造函数和析构函数。
Private:私有数据成员只能被类内部的函数使用和修改,私有函数成员只能被类内部的函数调用。派生类虽然继承了基类私有成员,但不能直接访问它们,只能通过基类的公有成员访问。
Protected:保护数据成员只能被类内部和派生类的函数使用和修改,保护函数成员只能被类内部和派生类的函数调用。
Public:类的公用函数成员可以被类的外部程序所调用,类的公用数据成员可以被类的外部程序直接使用。公有函数实际是一个类和外部通讯的接口,外部函数通过调用公有函数,按照预先设定好的方法修改类的私有成员和保护成员。
Internal:内部成员只能在同一程序集中的文件中才是可以访问的,一般是同一个应用(Application)或库(Library)。
对象是类的实例,是OOP应用程序的一个组成部件。
汽车类是抽象的概念,它有颜色和品牌,是所有汽车的集合,是“类”;而一辆红色奔驰车则是汽车类的一个具体实例,是“对象”。
变量类型是一个类,变量也是一个对象。
对象的定义、实例化及访问
【例7-1】实现访问Car类的对象和对象数据状态。
public class Car
{
public int number; //号码属性
public string color; //颜色属性
private string _brand; //品牌域
public Car()
{
}
public string brand //品牌属性
{
get //读操作
{
return this._brand;
}
set //写操作
{
this._brand = value;
}
}
}
构造函数和析构函数是类中比较特殊的两种成员函数,主要用来对对象进行初始化和回收对象资源。
构造函数的名字和类名相同。析构函数和构造函数的名字相同,但析构函数要在名字前加一个波浪号(~)。
构造函数的名称与类名相同;
构造函数不声明返回类型;
构造函数通常是公有的(使用public访问限制修饰符声明),如果声明为保护的(protected)或私有的(private),则该构造
函数不能用于类的实例化;
构造函数的代码中通常只进行对象初始化工作,而不应执行其他操作;
构造函数在创建对象时被自动调用,不能像其他方法那样显式地调用构造函数。
【例7-2】使用构造函数对类Person的3个字段进行初始化。
public class Person
{
public string m_name;
protected int m_age;
protected bool m_gender;
//构造函数
public Person()
{
m_name = "Unknown";
m_age = 0;
m_gender = false;
}
}
一个类定义必须且至少有一个构造函数,如果定义类时,没有声明构造函数,系统会提供一个默认的构造函数,不带任何参数的构造函数称为默认构造函数。如果声明了构造函数,系统将不再提供默认构造函数。
为了创建实例的方便,一个类可以有多个具有不同参数列表的构造函数,即构造函数可以重载。
public class Xyz {
// 成员变量
int x;
public Xyz() {
//参数表为空的构造函数
x = 0; // 默认创建对象
}
public Xyz(int i) {
//带一个参数的构造函数
x = i; // 使用参数创建对象
}
}
析构函数不接受任何参数,也不返回任何值;
析构函数不能使用任何访问限制修饰符;
析构函数的代码中通常只进行销毁对象的工作,而不应执行其他的操作;
析构函数不能被继承,也不能被显式地调用。
public class Person
{
private string m_name;
private int m_age;
private bool m_gender;
//析构函数
~Person()
{
}
}
方法是一种用于实现可以由对象或类执行的计算或操作的成员。类的方法主要是和类相关联的动作,它是类的外部界面,对于那些私有的字段来说,外部界面实现对它们的操作一般只能通过方法来实现。
语法格式:
访问修饰符 返回类型 方法名(参数列表){ }
方法的访问修饰符通常是public,以保证在类定义外部能够调用该方法。
方法的返回类型用于指定由该方法计算和返回的值的类型,可以是任何值类型或引用类型数据,如,int、string及前面定义的Person类。如果方法不返回一个值,则它的返回类型为void。
方法名是一个合法的C#标识符。
参数列表在一对圆括号中,指定调用该方法时需要使用的参数个数、各个参数的类型,参数之间以逗号分隔。
实现特定功能的语句块放在一对大括号中,叫方法体,“{”表示方法体的开始,“}”表示方法体的结束。
如果方法有返回值,则方法体中必须包含一个return语句,以指定返回值,其类型必须和方法的返回类型相同。如果方法无返回值,在方法体中可以不包含return语句。
例如:无返回值的方法method:
public void method()
{
Console.Write("方法声明");
}
在方法声明中使用的参数叫形式参数(形参)
在调用方法中使用的参数叫实际参数(实参)
在调用方法时,参数传递就是将实参传递给形参的过程。
例如,某类定义中声明方法时的形参如下:
public int IntMax(int a,int b){}
则声明对象classmax后调用方法时的实参为:
classmax.IntMax(x,y)
调用方法时,传递给方法的参数类型应该与方法定义的参数类型相同,或者是能够隐式转换为方法定义的参数类型。
如果方法进行处理和更改数值等操作,有时需要传递参数值给方法并从方法获得返回值。下面是参数值的4种常用情况。
值参数
声明时不带修饰符的参数是值参数,一个值参数相当于一个局部变量,初始值来自该方法调用时提供的相应的实参。参数按值的方式传递是指当把实参传递给形参时,是把实参的值复制(拷贝)给形参,实参和形参使用的是两个不同内存中的值,所以这种参数传递方式的特点是形参的值发生改变时,不会影响实参的值,从而保证了实参数据的安全。 之所以叫做值类型,是因为传递的是对象的副本而不是对象本身。传递的是值,而不是同一个对象。
【例7-3】值参数传递。
CzMath c = new CzMath();
c.Swap(a, b);
……
class CzMath
{
//交换两个数的值
public void Swap(double x, double y)
{
double temp = x;
x = y;
y = temp;
}
}
程序中,方法Swap试图通过一个临时变量来交换a和b的值,但程序运行后的结果仍然是:
a = 100;
b = 150;
这是因为调用Swap方法时,首先将主方法Main中定义的变量a和b(实参)进行一次复制,而后传递给方法的形参,在方法的执行代码中所改变的只是复制的值,而不是实际参数的原始值,因此达不到交换数值的效果。
上面传递参数的方法叫做值传递值传递:
实参的值复制(拷贝)给形参,实参和形参使用的是两个不同内存中的值,所以这种参数传递方式的特点是形参的值发生改变时,不会影响实参的值,从而保证了实参数据的安全。
如果希望改变实参的值,则需要另一种传递参数的方法引用传递。
引用参数
引用传递是指实参传递给形参时,不是将实参的值复制给形参,而是将实参的引用传递给形参,实参与形参使用的是一个内存中的值。这种参数传递方式的特点是形参的值发生改变时,同时也改变实参的值。
引用参数使用ref修饰符,告诉编译器,实参与形参的传递方式是引用。
【例7-4】引用参数传递。
……
CzMath c = new CzMath();
c.Swap(ref a, b);
……
class CzMath
{
//交换两个数的值
public void Swap(ref double x, double y)
{
double temp = x;
x = y;
y = temp;
}
}
类对象参数总是按引用传递的,所以类对象参数传递不需要使用ref关键字。
程序中,传递给方法的参数就不是实参的拷贝,而是指向实参的引用。所以当对形参进行修改时,相应的实参也发生了改变。程序运行的结果表明了数值交换的成功:
a = 150;
b = 100;
输出参数
在传递的参数前加out关键字,即可将该传递参数设置为一个输出参数。
与引用参数类似,输出参数也不开辟新的内存区域。它与引用型参数的差别在于,调用方法前无需对变量进行初始化。输出型参数用于传递方法返回的数据。
out修饰符后应跟随与形参的类型相同的类型声明。在方法返回后,传递的变量被认为经过了初始化。
【例7-5】输出参数传递。
……
double ave; //ave未初始化
CzMath m = new CzMath();
m.Average(a, b, out ave);
……
class CzMath
{
public void Average(int x, int y, out double z)
{
z = Convert.ToDouble(x + y) / 2;
}
}
参数数组
参数数组必须用params修饰词明确指定。
方法的参数类型也可以是数组 。
public double Average(int[] array)
{
double ave = 0;
int sum = 0;
for(int i = 0; i < array.Length; i++)
sum += array[i];
ave = (double)sum / array.Length;
return ave;
}
public double Average(params int[] array)
{
……}
调用Average方法的示例代码:
int[] a = {
1, 3, 3, 5, 7, 9 };
int x = Average(a);
int y = Average(1, 3, 5, 7, 9);
int z = Average(10, 20, 30);
如果在形参前面加上关键字params,该形参就成为了参数数组。
public double Average(params int[] array)
{ double ave = 0;
int sum = 0;
for(int i = 0; i < array.Length; i++)
sum += array[i];
ave = (double)sum / array.Length;
return ave;
}
传递给数组型参数的实参既可以是一个数组,也可以是任意多个数组元素类型的变量。
例如,调用Average的代码:
int[] a = {
1, 3, 3, 5, 7, 9 };
int x = Average(a);
int y = Average(1, 3, 5, 7, 9);
int z = Average(10, 20, 30);
都是合法的 。
C#中对于参数数组有着严格的规定:
【例7-6 】参数数组的使用-计算数组所有元素的平均值 。
方法分为静态方法(方法声明中含有static修饰符 )和非静态方法(方法声明中没有static修饰符 )。
1.静态方法
(1)静态方法不对特定实例进行操作,在静态方法中引用this会导致编译错误。
(2)C#中通过关键字static来定义静态成员。和实例成员不同,使用静态成员时,圆点连接符的前面不再是某个具体的对象变量,而是类的名称。如Console.WriteLine和Console.ReadLine等方法就属于静态方法,使用时没有创建哪个具体的Console类的实例,而是直接使用类本身。
例7-4类CzMath的Swap方法只用于交换两个数值,而与具体的对象无关,因此可以将其定义为静态方法,这样在调用方法时就无需创建CzMath类的实例,
【例7-7 】静态方法。
……
// CzMath c = new CzMath();
//c.Swap(ref a, b);
CzMath.Swap(ref a, ref b);
……
class CzMath
{ //交换两个数的值
public static void Swap(ref double x, double y)
{ double temp = x; x = y; y = temp; }
}
方法分为静态方法(方法声明中含有static修饰符 )和非静态方法(方法声明中没有static修饰符 )。
2.非静态方法
非静态方法是对类的某个给定的实例进行操作,而且可以用this来访问该方法。
例如程序例7-4中的
CzMath c = new CzMath();
c.Swap(ref a, ref b);
就是先实例化CzMath的对象c,然后用对象c去调用非静态方法Swap()。
方法重载是指调用同一方法名,但各方法中参数的数据类型、个数或顺序不同。类中有两个以上的同名方法,但使用的参数类型、个数或顺序不同,调用时,编译器就可以判断在哪种情况下调用哪种方法。
public int MethodTest(int i, int j)
{
……}
public int MethodTest(int i)
{
……}
public string MethodTest(string s)
{
……}
为了保存类的实例的各种数据信息,C#提供了两种方法—字段(或称域、成员变量)和属性。
属性不是字段,本质上是定义修改字段的方法, 两者密切相关。
字段也叫成员变量,表示存储位置,用来保存类的各种数据信息。
字段是 C#中不可缺少的一部分,代表一个与对象或类相关的变量或常量。
一个字段声明可以把一个或多个给定类型的字段引入。
字段的声明非常简单。例如:
private int a;
字段又可分为静态字段、实例字段、常量和只读字段。
例如:
private static int myvar;
private int myvar;
private const int myvar=5;
private readonly int myvar=6;
【例7-8】四种字段的使用。
属性不是字段,但必然和类中的某个或某些字段相联系。
属性定义了得到和修改相联系的字段的方法。
C#中的属性更充分地体现了对象的封装性:不直接操作类的数据内容,而是通过访问器进行访问,借助于get和set方法对属性的值进行读写。
当读取属性时,执行get访问器的代码块;当向属性分配一个新值时,执行set访问器的代码块。
属性在类模块内是通过以下方式声明的:指定字段的访问级别,后面是属性的类型,接下来是属性的名称,然后是声明get访问器和/或set访问器的代码模块。
例如,在Student类中可以Name属性来封装对私有字段name的访问:
public class Student
{
private string name; //字段
public string Name //属性
{
get {
return name; }
set {
name = value; }
}
}
属性
作为类的特殊函数成员,get和set访问函数需要包含在属性声明的内部,而函数声明只需要写出get和set关键字即可。其中get访问函数没有参数,默认返回类型就是属性的类型,表示属性返回值;set访问函数的默认返回类型为void,且隐含了一个与属性类型相同的参数value,表示要传递给属性的值。
通过属性来访问隐藏的字段,例如:
Student s1 = new Student(“李明”);
Console.WriteLine(s1.Name); //调用get访问函数访问name字段
Console.WriteLine(“请输入新姓名:”);
s1.Name = Console.ReadLine(); //调用set访问函数修改name字段
属性可以只包含一个访问函数,如只有get访问函数,那么属性的值不能被修改;如只有set访问函数,则表明属性的值只能写不能读。
例如,希望Student对象在创建之后就不允许修改其姓名,那么Name属性的定义可以修改为:
public string Name //只读属性
{
get {
return name; }
}
属性的典型用法是一个共有属性对应封装一个私有或保护字段,但这并非强制要求。属性本质上是方法,在其代码中可以进行各种控制和计算。
例如,在学生类中有个表示出生年份的私有字段birthYear,那么表示年龄的属性Age的返回值应该是当前年份减去出生年份:
privage int birthYear;
public int Age
{
get{
return DateTime.Now.Year – birthYear;}
}
注意:在C# 3.0中,提供了名为“自动属性”特征,它允许只写出属性及其访问函数的名称,编译就会自动生成所要封装的字段以及访问函数的执行代码。
例如,
public class Student
{
public string Name {
get;set;}
}
其效果和下面传统的定义方式是一样的:
public class Student
{
private string name; //字段
public string Name //属性
{
get {
return name; }
set {
name = value; }
}
}
封装、继承和多态性是面向对象程序设计的3个基本要素
通过继承,派生类能够在增加新功能的同时,吸收现有类的数据和行为,从而提高软件的可重用性。
而多态性使得程序能够以统一的方式来处理基类和派生类的对象行为,甚至是未来的派生类,从而提高系统的可扩展性和可维护性。
1.基类和派生类
所谓继承,是指在已有类的基础上构造新的类,新类继承了原有类的数据成员、属性、方法和事件。原有的类称为基类,新类称为派生类。
【例7-9】基类和派生类的例子。
为避免层次结构过于复杂,C#中的类不支持多继承,即不允许一个派生类继承多个基类,只有在类和接口之间可以实现多继承。
.NET 类库本身在构造过程中就充分利用了继承技术。System.Object类是其他所有类的基类,不仅如此,C#是完全面向对象的编程语言,其中所有的数据类型都是从类中衍生而来,因此 System.Object类也是其他所有数据类型的基类。
访问基类成员
2.访问基类成员
访问基类成员涉及隐藏基类成员和base关键字的使用。
(1)隐藏基类成员
大多数情况下,派生类不会一成不变地继承基类中的所有成员,如可能希望在某些字段中存储不同的信息、在某些方法中执行不同的操作等。这时就需要通过new关键字来隐藏基类中的成员。
例如例7-9中,可能会希望StudentLeader类的NewPrintInfo()方法输出不同的内容(除了基类的三个字段之外,还需要输出Duty),这时就可以将NewPrintInfo()方法定义为:
public new void PrintInfo()
{
Console.WriteLine(No);
Console.WriteLine(Name);
Console.WriteLine(Score);
Console.WriteLine(Duty);
}
提示:隐藏基类成员时所使用的new
关键字属于一种修饰符,它和创建
对象时使用的new操作符是完全不同的。
这时称派生类中重新定义的方法覆盖了基类中的同名方法。其中,new关键字放在访问限制修饰符的前后均可,但一定要在成员的类型说明之前。
(2) base关键字的使用
base关键字用来访问当前对象的基类对象,进而调用基类对象的成员。
例如,可以StudentLeader类的PrintInfo方法改写成:
public new void PrintInfo()
{
base.PrintInfo();
Console.WriteLine(Duty);
}
这样,派生类StudentLeader的PrintInfo()方法就调用了其隐藏的基类方法。
base的另外一个用途是在派生类中调用基类构造函数,例如,上例中StudentLeader类的构造函数可以改写为:
public StudentLeader(string a, string b, int c,string d):base(a, b, c)
{
Duty = d;
}
其中,base(a, b, c)表示调用基类构造函数Student(a,b,c)。
“多态性” 的含义:
同一事物在不同的条件下可以表现出不同的形态。
在对象之间进行通信时非常有用,如一个对象发送消息到其他对象,它并不一定要知道接收消息的对象属于哪一类。接收到消息之后,不同类型的对象可以做出不同的解释,执行不同的操作,从而产生不同的结果。
1.虚拟方法和重写方法
2.抽象类和抽象方法
3.密封类和密封方法
虚拟方法和重写方法
派生类很少一成不变地去继承基类中的所有成员。
一种情况是派生类中的方法成员可以隐藏基类中同名的方法成员,这时通过关键字new对成员加 以修饰;
另一种更为普遍和灵活的情况是将基类的方法成员定义为虚拟方法,而在派生类中对虚拟方法进行重写。
后者的优势在于它可以实现运行时的多态性,即程序可以在运行过程中确定应该调用哪一个方法成员。
基类的虚拟方法通过关键字virtual进行定义,而派生类的重写方法则通过关键字override进行定义。
【例7-10】修改例7-9,要求使用虚拟方法和重写方法。
说明:
1.如果在派生类中使用override关键字定义了重写方法,那么也就允许该类自己的派生类继续重写这个方法,因此重写方法默认也是一种虚 拟方法,但不能同时使用virtual和override修饰一个方法。
2.虚拟方法不能是私有的。而且在基类和派生类中,对同一个虚拟方法和重写方法的访问限制应当相同,即要么都使用public修饰符,要么都使用 protected修饰符。
重载和重写
重载和重写的区别:
2.抽象类和抽象方法
基类中的虚拟方法允许在派生类中进行重写,并在调用时动态地决定是执行基类的方法代码,还是执行哪一个派生类的方法代码。
C#中还可以为基类定义抽象方法,它强制性地要求所有派生类必须重写该方法。
抽象方法使用关键字abstract定义,并且不提供方法的执行体。
例如:
//抽象方法
public abstract void Area();
包含抽象方法的类必须是抽象类,它也需要使用关键字abstract加以定义。
例如:
public abstract class Shape
{
//类的成员定义...
}
抽象类表达的是抽象的概念,它本身不与具体的对象相联系,其作用是为派生类提供一个公共的界面。例如“图形”就可以是一个抽象类,因为每一个具体的图形对象必然是其派生类的实例,如“四边形”对象、“圆形”对象等;但“四边形”不是一个抽象类,因为“四边形”对象既可以是其派生的“平行四边形”、“正方 形”等特殊四边形对象,也可以是一般的四边形对象。
对于抽象类,不允许创建类的实例, 例如在定义了抽象类Shape之后,下面的代码是错误的:
Shpae s = new Shape();
抽象类之间也可以进行继承。抽象类要求其所有派生类都继承它的抽象方法;而如果派生类不是抽象类,它就必须重写这些抽象方法并提供实现代码。
和虚拟方法类 似,派生类中对抽象方法的重写也通过override关键字进行。抽象方法不能是私有的,而且抽象方法及其重写方法的访问限制应当相同。最后,抽象方法不 能同时使用virtual关键字进行修饰。
【例7-11】抽象类和抽象方法。
public abstract class Shape
{
//定义Shape抽象类
public const double PI = 3.14;
public abstract double area(); //抽象方法,不需要定义处理
}
3.密封类和密封方法
抽象类本身无法创建实例,而强制要求通过派生类实现功能。与之相反的是,在C#中还可以定义一种密封类,它不允许从中派生出其他的类。密封类通常位于类的继承层次的最低层,或者是在其他一些不希望类被继承的情况下使用。
密封类使用关键字sealed定义,例如:
public sealed class Circle : Shape
{
//类的成员定义...
}
4.说明
(1)有趣的是, 尽管密封类和抽象类是截然相反的两个概念,但它们并不冲突:一个类可以同时被定义为密封类和抽象类,这意味着该类既不能被继承、也不能被实例化。这只出现在一种情况之下,那就是该类的所有成员均为静态成员,Console类就是这样的一个类。
(2)类似的,如果方法在定义中使用了关键字sealed,它就成为密封方法。与密封类的不同之处在于密封类是指不允许有派生类的类,而密封方法则是指不允许被重写的方法。密封方法所在的类不一定是密封类(这一点与抽象方法不同),而如果该类存在派生类,那么在派生类中就必须原封不动地继承这个密封方法。此外, 密封方法本身也要求是一个重写方法(即sealed和override必须在方法定义中同时出现)
例如,可在Rectangle类中将重写 area()为密封的:
public sealed override double area()
{
return (width * height);
}
这样在Rectangle类的所有派生类中,就不允许重写该方法的实现。如果要在派生类中定义同名的方法,就必须使用关键字new来隐藏基类的方法。
public class square : rectangle
{
public new double area()
{
return (width * width);
}
}
1.类是面向对象程序设计的基本单元,是C#中最重要的一种数据结构。这种数据结构可以包含字段成员、方法成员以及其他的嵌套类型。
2.构造函数、析构函数、属性、索引指示器、事件和操作符都可以视为方法成员。类的构造函数用于对象的初始化,而析构函数用于对象的销毁。
3.利用属性提供的访问方法,可以隐藏数据处理的细节,更好地实现对象的封装性。
4.继承是面向对象的程序设计方法中实现可重用性的关键技术。
5.同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性。多态性通过派生类重载基类中的虚拟方法来实现。
6.C#中还提供了抽象和密封的概念,给继承和多态性的实现带来了更大的灵活性。抽象类和接口都把方法成员交给派生类去实现。而密封类不允许被继承,密封方法不允许被重载。