C#与C++类的继承机制的对比分析(1)
为了提高软件模块的可复用性和可扩充性,以便提高软件开发效率,我们总是希望能够通过利用前人或者以前的开发成果,同时又希望在自己的开发过程中能够有足够的灵活性,不拘泥于复用的模块。在面向对象的程序设计语言中是采用一种特殊的机制来实现的,这就是:继承。
继承是面向对象编程的支柱之一,是OOP鼓励代码复用的一种方法。继承让您通过扩展现有基类的功能来创建一个新的派生类。继承也允许建立类的层次结构。公用功能可以放在基类中,可以创建任意数量的派生类来利用此功能,而不用内部复制这些功能的代码。
在建立一个新类时,程序员可以让新类继承预定义基类(base class)的数据成员和成员函数,而不必重新编写新的数据成员和成员函数。这种新类称为派生类(derived class)。在C++中允许派生类从多个基类派生出来,这些基类之间可能毫无关系。但在C# 中只允许单一继承。
1.2 C++与C#中关于类继承代码实例对比分析
下面我将给出一个关于交通工具的一个类,以及从交通工具类派生出来的汽车类,通过用C++与C#来实现这两个类来对比分析。
C++源代码:
#include<iostream>
using namespace std;
class Vehicle
{
public:
int wheels;
float weight;
public:
void SetData() //(A)
{
cout<<"Vehicle's SetData() called !"<<endl;
}
void SetData(int w,float g) //(B)
{
cout<<"Vehicle's SetData(int,int) called !"<<endl;
}
};
class Car:public Vehicle //(C)
{
public:
int passengers;
void SetData(int w,float g,int p) //(D)
{
cout<<"Car's SetData(int,int,int) called !"<<endl;
}
};
int main()
{
Car myCar;
myCar.Vehicle::SetData(); //(E)
myCar.Vehicle::SetData(3,4); //(F)
myCar.SetData(3,4,5); //(G)
return 1;
}
C#源代码:
using System;
namespace NameLookup
{
class Vehicle
{
public int wheels;
public float weight;
public void SetData() //(A)
{
Console.WriteLine("Vehicle's SetData() called !");
}
public void SetData(int w,int g) //(B)
{
Console.WriteLine("Vehicle's SetData(int,int) called !");
}
}
class Car:Vehicle //(C)
{
public int passengers;
public void SetData(int w,float g,int p) //(D)
{
Console.WriteLine("Car's SetData(int,int,int) called !");
}
}
class Class1
{
static void Main(string[] args)
{
Car car = new Car();
car.SetData(); //(E)
car.SetData(2,3); //(F)
car.SetData(2,3,4); //(G)
}
}
}
1.2.1 类继承的方式的对比
首先,我们比较C++源代码中的第C行和C#源代码中的第C行。我们可以看到在C++中的继承是分为: 公有继承,私有继承,保护继承。而在C#中只有默认的公有继承。这样简化了继承的方式,至于限制派生类访问基类的数据以及方法成员,将由基类的数据以及方法成员的访问控制权限决定。
(1) C#中,派生类只能从一个类中继承。这是因为,在C++中,人们在大多数情况下不需要一个从多个类中派生的类。从多个基类中派生一个类,往往会带来许多问题,从而抵消了这种灵活性带来的优势。C#中的继承符合下列规则:继承是可传递的。如果C从B中派生,B又从A中派生,那么C不仅继承了B中声明的成员,同样也继承了A中的成员。Object 类作为所有类的基类。
(2) 派生类应当是对基类的扩展。派生类可以添加新的成员,但不能除去已经继承的成员的定义。构造函数和析构函数不能被继承。除此以外的其它成员,不论对它们定义了怎样的访问方式,都能被继承。基类中成员的访问方式只能决定派生类能否访问它们。
(3) 派生类如果定义了与继承而来的成员同名的新成员,就可以覆盖已继承的成员。但这并不因为这派生类删除了这些成员,只是不能再访问这些成员。类可以定义虚方法、虚属性以及虚索引指示器,它的派生类能够重载这些成员,从而使得类可以展示出多态性。
(4) C#中,派生类只能从一个类中继承,但可以通过接口实现多重继承。
1.2.2 成员函数的隐藏规则对比
我们可以看到C++和C#之间有一个非常巨大的区别:在C++中,派生类中一个给定名称的函数将隐藏基类中的所有的同名函数,不管它们的签名是否相同。在派生类中,我们只能借助作用域分解操作才能访问这些被隐藏的函数。但是在C#中却并非如此,在C#中会根据函数签名而去调用基类中的同名但不同签名的函数。而在C++中,虽然基类Vehicle中的成员函数SetData与派生类Car中的成员函数SetData具有不同的函数签名:例如C++源程序中,第A,B行代码处的void SetData()和void SetData(int w,float g)函数将被派生类的void SetData(int w,float g,int p)函数覆盖掉,因此如果我们把C++源程序中的第E,F行分别改为 myCar.SetData(); myCar.SetData(3,4)。程序是无法通过编译的。
1.2.3 访问与隐藏基类成员对比分析
u C++中访问基类成员的方法
示例:下面程序中基类 Person 和派生类 Employee 都有一个名为 Getinfo 的方法。通过使用类作用域分辨符,可以从派生类中调用基类上的 Getinfo 方法。
#include<iostream>
#include<string>
using namespace std;
class Person
{
protected :
string ssn;
string name;
public:
Person()
{
ssn = "111-222-333-444";
name = "Howells";
};
virtual void GetInfo()
{
cout<<"姓名: "<<name<<endl;
cout<<"编号: "<<ssn<<endl;
}
};
class Employee:public Person
{
public:
string id ;
public:
Employee()
{
id = "1999";
}
virtual void GetInfo()
{
Person::GetInfo(); //(A)
cout<<"成员ID: "<<id<<endl;
}
};
void main()
{
Employee howells;
howells.GetInfo();
}
程序运行结果:
姓名: Howells
编号: 111-222-333-444
成员ID: 1999
Press any key to continue
可见在C++中,派生类是通过类作用域来访问基类中的方法的。在以上程序中派生类Employee通过使用基类名加范围分解符的方法,如:第A行代码Person::GetInfo()。
u C#中访问基类成员的方法
示例:下面程序中基类 Person 和派生类 Employee 都有一个名为 Getinfo 的方法。通过使用 base 关键字,可以从派生类中调用基类上的 Getinfo 方法。
using System;
namespace fanwenjilei
{
public class Person
{
protected string ssn = "111-222-333-444" ;
protected string name = "Howells" ;
public virtual void GetInfo()
{
Console.WriteLine("姓名: {0}", name) ;
Console.WriteLine("编号: {0}", ssn) ;
}
}
class Employee: Person
{
public string id = "1999" ;
public override void GetInfo()
{
// 调用基类的GetInfo方法:
base.GetInfo();
Console.WriteLine("成员ID: {0}", id) ;
}
}
class TestClass
{
public static void Main()
{
Employee E = new Employee() ;
E.GetInfo() ;
}
}
}
程序运行结果:
姓名: Howells
编号: 111-222-333-444
成员ID: 1999
在C#中可以通过base关键字来访问基类成员,具体的访问规则如下:
(1) 调用基类上已被其他方法重写的方法。
(2) 指定创建派生类实例时应调用的基类构造函数。
(3) 基类访问只能在构造函数、实例方法或实例属性访问器中进行。
(4) 从静态方法中使用 base 关键字是错误的。
u C#中阻止类被继承的方法
如果所有的类都可以被继承,继承的滥用会带来什么后果?类的层次结构体系将变得十分庞,大类之间的关系杂乱无章,对类的理解和使用都会变得十分困难。有时候,我们并不希望自己编写的类被继承。另一些时候,有的类已经没有再被继承的必要。C#提出了一个密封类(sealed class)的概念,帮助开发人员来解决这一问题。
密封类在声明中使用sealed 修饰符,这样就可以防止该类被其它类继承。如果试图将一个密封类作为其它类的基类,C#将提示出错。理所当然,密封类不能同时又是抽象类,因为抽象总是希望被继承的。
在哪些场合下使用密封类呢?密封类可以阻止其它程序员在无意中继承该类。而且密封类可以起到运行时优化的效果。实际上,密封类中不可能有派生类。如果密封类实例中存在虚成员函数,该成员函数可以转化为非虚的,函数修饰符virtual 不再生效。让我们看下面的例子:
abstract class A
{
public abstract void F( ) ;
}
sealed class B: A
{
public override void F( ){ // F 的具体实现代码 }
}
如果我们尝试写下面的代码 class C: B{ }。C#会指出这个错误,告诉你B 是一个密封类,不能试图从B 中派生任何类。