中的多重继承
exactly;">C++允许一个类从多个基类继承实现代码,这称为多重继承(MI)。现在考虑下面一个问题:用面向对象的方法来描述教育系统中的学生(Student),教师(Teacher),助教角色(TeacherAssistant)。可以分析得出Student类和Teacher类是完全不同的,区别也是非常明显的。但是TeacherAssistant这个类却是前面两个角色的组合。所以我们可以采用C++中的多重继承的概念来实现TeacherAssistant类。
图1-1
虽然多重继承表面上看来给程序设计带来很大的方便之处,但是当一个派生类从两个不同的路径继承了某个基类的相同成员时,就产生了重复继承的问题。通过下面的实例来分析多重继承带来的问题。下图显示了一个类层次体系,Manager类和SalesPerson类都派生与基类Employee。SalesManager类同时派生于Manager类和SalesPerson类。可见SalesManager类通过了两条路径具有一个共同的祖先。
图1-2
当一个派生类可以通过多个路径继承成员并且这些不同的路径具有一个共同的祖先时,就会产生下面这些问题。
(1) 公共基类子对象的重复创建问题。例如:SalesManager类的构造函数被调用的时候,在这个新创建的对象中有两个不同版本的Employee子对象,导致派生类对象内部公共基类子对象的重复创建。
(2) 成员函数的名字冲突问题。例如:SaleManager 类从两个不同的路径继承了两个成员函数,它们具有相同的签名,但是它们的实现不相同。如果SalesManager类并没有对这两个成员函数进行覆盖,而去直接通过对象名来访问这个函数就会产生错误。
(3) 数据成员的名字冲突问题。SalesManager类继承了两个具有相同名字的不同数据成员department,分别来自两个基类Manager和Salesperson。这两个数据成员尽管拥有相同的名字,但是它们在SalesManager类中具有不同的含义,这是因为一个SalesManager对象可以有两个与之相关联的”department”属性。一个销售经理可以属于一家公司的某个特定部门,同时又负责一个特定的销售组织。因此我们要避免由于两个数据成员同名而造成的混淆。
中的接口
接口(interface)用来定义一种程序的协定。实现接口的类或者结构要与接口的定义严格一致。有了这个协定,就可以抛开编程语言的限制(理论上)。接口可以从多个基接口继承,而类或结构可以实现多个接口。接口可以包含方法、属性、事件和索引器。接口本身不提供它所定义的成员的实现。接口只指定实现该接口的类或接口必须提供的成员。
接口好比一种模版,这种模版定义了对象必须实现的方法,其目的就是让这些方法可以作为接口实例被引用。接口不能被实例化。类可以实现多个接口并且通过这些实现的接口被索引。接口变量只能索引实现该接口的类的实例。
既然接口可以实现多重继承,同样它也会出现一些类似于C++类的多重继承中的问题。例如:如果底层成员的命名与继承而来的高层成员一致,那么底层成员将覆盖同名的高层成员。但由于接口支持多重继承,在多继承中,如果两个父接口含有同名的成员,这就产生了二义性(这正是C#中取消了类的多重继承机制原因之一)。
ormal; mso-bidi-font-weight: bold;">中公共基类子对象的重复创建问题
如“图4-3”所示,:SalesManager类的构造函数被调用的时候,在这个新创建的对象中有两个不同版本的Employee子对象,导致派生类对象内部公共基类子对象的重复创建。如下图所示:
图1-3
1) 在C++中类的多重继承中,通过把一个公共基类声明为虚基类,我们就可以消除公共基类子对象的重复创建问题。此时Manager和SalesPerson都共享同一个Employee对象。如下图所示:
图1-4
2) 在C#接口的多重继承中,默认的方式就是Manager和SalesPerson都共享同一个Employee接口。所以在使用C#接口的多重继承的时候不需要程序员关注这个问题。
SaleManager 类从两个不同的路径继承了两个成员函数,它们具有相同的签名,但是它们的实现不相同。如果SalesManager类并没有对这两个成员函数进行覆盖,当通过对象名来直接调用这些函数时,这个程序就会出错。
1) 在C++中,当一个类从两个不同的路径继承了两个成员函数,它们具有相同函数名,但是它们的实现不相同时,必须在该派生类中对它们进行覆盖,否则直接访问该成员函数就会出错。当然也可以采用通过作用域分辨符来标识要访问哪个成员。
#include<iostream>
using namespace std;
class X
{
public:
int i;
void showMe()
{
cout<<"I am X !"<<endl;
}
};
class Y
{
public:
int i;
void showMe()
{
cout<<"I am Y !"<<endl;
}
};
class Z:public X,public Y
{
public:
void showMe()
{
cout<<"I am Z !"<<endl;
}
};
void main(void)
{
Z bill;
bill.X::showMe(); //(A)
bill.Y::showMe(); //(B)
bill.showMe(); //(C)
system("pause");
}
程序运行结果:
I am X !
I am Y !
I am Z !
请按任意键继续. . .
在源程序第A,B行,我们使用了作用域分辨符来访问bill的父类X,Y的成员函数showeMe()。这样也就解决了成员函数名冲突的问题。
2) 在C#中,当一个类从两个不同的路径继承了两个接口的同名成员函数,只需要在类的实现中对两个不同的同名函数实现时,进行显示的指派父接口类型。(注意:显式接口只能通过接口来调用)如下所示:
using System;
public interface IShape
{
double Area();
int Sides { get; }
void Display();
}
public interface IShapeDisplay
{
void Display();
}
public class Square : IShape, IShapeDisplay
{
private int sides;
public int SideLength;
public void SidesByLength()
{
Console.WriteLine("sides*SideLength:{0}",sides*SideLength);
}
public int Sides
{
get { return sides; }
}
public double Area()
{
return ((double) (SideLength * SideLength));
}
public double Circumference()
{
return ((double) (Sides * SideLength));
}
public Square()
{
sides = 4;
}
void IShape.Display() //(A)
{
Console.WriteLine("/nDisplaying Square Shape/'s information:");
Console.WriteLine("Side length: {0}", this.SideLength);
Console.WriteLine("Sides: {0}", this.Sides);
Console.WriteLine("Area: {0}", this.Area());
}
void IShapeDisplay.Display() //(B)
{
Console.WriteLine("/nThis method could draw the shape...");
}
}
public class myApp
{
public static void Main()
{
Square mySquare = new Square();
mySquare.SideLength = 7;
IShape ish = (IShape) mySquare;
IShapeDisplay ishd = (IShapeDisplay) mySquare;
ish.Display();
mySquare.SidesByLength();
ishd.Display();
}
}
程序运行结果:
Displaying Square Shape's information:
Side length: 7
Sides: 4
Area: 49
sides*SideLength: 28
This method could draw the shape...
接口的多重继承的问题也会带来成员访问上的问题。如:图4-5所示,Ileft接口把IBase接口的 F成员覆盖了,但IRight接口并没有对其进行覆盖,这样派生接口IDerived就有两份同名方法成员F,一份是从ILeft继承的,另外一份是从IRight的父接口IBase间接继承的。那么当我们对IDerived接口调用方法F时,会执行哪一个呢?这一个问题在C++和C#中的答案是一样的,即是调用ILeft中被覆盖的方法F。
图1-5
C++源代码:
#include<iostream>
using namespace std;
class IBase
{ public:
void F(int i)
{
cout<<"IBase::F(int i) called !"<<endl;
}
};
class ILeft:virtual public IBase
{
public:
void F(int i)
{
cout<<"ILeft::F(int i) called !"<<endl;
}
};
class IRight:virtual public IBase
{
public:
void G()
{
cout<<"IRgiht::G()"<<endl;
}
};
class IDerived:public ILeft,public IRight
{
};
void main(void)
{
IDerived d;
d.F(1);
system("pause");
}
程序运行结果:
ILeft::F(int i) called !
请按任意键继续. . .
C#源代码:
using System;
namespace jiekou1
{
public interface IBase
{
void F(int i);
}
public interface ILeft:IBase
{
new void F(int i);
}
public interface IRight:IBase
{
void G();
}
interface IDerived:ILeft,IRight
{
}
class C:IDerived
{
void IBase.F(int i)
{
Console.WriteLine("IBase's F(int i) !");
}
public void F(int i)
{
Console.WriteLine("ILeft's F(int i) !");
}
public void G()
{
Console.WriteLine("IRight's F() !");
}
}
class Class1
{
static public void Test(IDerived d)
{
d.F(1);
((IBase)d).F(1);
((ILeft)d).F(1);
((IRight)d).F(1);
}
static void Main(string[] args)
{
C bill = new C();
Test(bill);
}
}
}
程序执行结果是:
ILeft's F(int i) !
IBase's F(int i) !
ILeft's F(int i) !
IBase's F(int i) !
上例中,方法IBase.F在派生的接口ILeft中被ILeft的成员方法F覆盖了。所以对d.F(1)的调用实际上调用了。虽然从IBase——IRight——IDerived这条继承路径上来看,IBase.F方法是没有被覆盖的。但是事实上是:一旦成员被覆盖以后,所有对其的访问都被覆盖以后的成员“拦截”了 。根据程序运行结果可以知道C++和C#在处理该问题时是采用了相同的方法。
C++中的多重继承会产生数据成员名字冲突问题,如果派生类的多个基类拥有同名的数据成员,同时,派生类又新增这样的同名成员,在这种情况下,派生类成员将覆盖所有基类同名数据成员。该问题的解决方法与成员函数冲突问题的解决方法是一样的,均是通过作用域分辨符来访问冲突的数据成员。然而C# 中的接口并不存在该问题,因为C# 接口不允许包含数据成员。