C#与C++多态性对比分析1

 

1.1 什么是多态性

 在面向对象的系统中,多态性是一个非常重要的概念,它允许客户对一个对象进行操作,由对象来完成一系列的动作,具体实现哪个动作,如何实现由系统负责解释。

多态性的定义是:同一操作作用于不同的类的实例,不同的类将进行不同的解释,最后产生不同的执行结果。C#C++均支持两种类型的多态性:编译时多态性和运行时多态性。前者是在编译的过程中确定了同名操作的具体操作对象,而后者则是程序运行过程中动态地确定操作所针对的具体对象。这种确定具体对象的过程就是联编。联编是指计算机程序自身彼此关联的过程,也就是把一个标识符名和一个存储地址联系在一起的过程:用面向对象的术语讲,就是把一条消息和一个对象的方法相结合的过程。按照联编进行的阶段的不同,可以分为两种不同的联编方法:静态联编和动态联编,这两种联编过程分别对应着多态的两种实现方式。

u     静态联编
   
联编工作在编译连接阶段完成的情况称为静态联编。因为联编过程是程序开始执行之前进行的,因此有时也称为早期联编或前联编。在编译,连接过程中,系统就可以根据类型匹配等特征确定程序中操作调研能够与执行该操作的代码的关系,即确定了某一个同名标识到底是调用哪一段程序代码。有些多态类型,其同名操作的具体对象能够在编译、连接阶段确定、通过静态联编解决,比如重载。

u     动态联编
   
和静态联编相对应,联编工作在程序运行阶段完成的情况称为动态联编,也称为晚期联编或者后联编。在编译、连接过程中无法解决的联编问题,要等到程序开始运行之后再来确定,包含多态中操作对象的确定就是通过动态联编来完成的。

交通工具是多态性的一个很好的例子。一个交通工具接口可以只包括所有交通工具都具有的属性和方法,还可能包括颜色、车门数、变速器和点火器等。这些属性可以用于所有类型的交通工具,包括轿车、卡车和客车。

面向对象的多态性还可以分为三类:重载多态、强制多态、包含多态。普通函数以及类的成员函数的重载都属于重载多态;运算符重载,比如加法运算分别使用于浮点数、整数之间就是重载的实例。强制多态是指将一个变元的类型加以变化,以符合一个函数或者操作的要求,前面所讲的加法运算符在进行浮点数与整数相加时,首先进行类型强制转换,把整型数变为浮点数再相加的情况,就是强制多态的实例。包含多态是研究类族中定义于不同类中的同名成员函数的多态行为,主要是通过虚函数来实现。下面我将从这四个方面来对比C++C#的多态性。

1.2 C++C#中重载多态性分析

1.2.1 C++函数重载解析

假如我们有一组数学函数,它们的原型如下所示。所有函数都完成相同的任务,但是它们所接受的参数类型不同:

int foo( int x, int y);

double foo(double x, int y);

int foo(int x,double y);

现在我们假设主程序中调用下面这个函数:

double u;  double v;

double w = foo(u,v);

我们很难判定它究竟会选择哪个函数调用。因为其实出现了两个适合的函数原型,即foo(double x, int y)foo(int x, double y)。所以以上情况发生时,编译器就会报错。

C++C#编译器都有自己的一些算法用于重新解析。为了进行重新解析,函数以它的签名来表示,签名就是函数名加上它的参数列表(参数的数量以及每个参数的类型)。值得注意的是,函数的返回类型以及函数所抛出的异常的类型不包括在函数的签名以内,因此它们不会在重载解析中发挥任何作用。以下是各种不同层次的特异性,根据它们,在C++中的一个函数调用的实参可以与函数定义中的对应形参进行匹配。

特异性层次1 当我们把一个实参与其对应的形参进行匹配时,如果两者的类型完全相同或者只需要进行微小的转换,那么我们就能得到具有佳特异性的匹配。这种微小的类型转换的例子包括数组名转换为指针、函数名转换为函数指针以及类型T转换为const T 等。

特异性层次2 如果实参类型和形参类型之间的转换需要进行类型升级,我们所得到的匹配比以前那种匹配的特异性要差一点。所谓的类型升级是指:(1)整形转换为整形;(2)非整形转换为非整形;(3)并且转换不会产生信息损失。

特异性层次3 匹配特异性较前一种匹配更低一级的是涉及标准转换的匹配。基本类型的标准转换的例子包括intdouble,doubleint型等(C#,double int 是不允许的)。对于指针类型,标准转换包括T*void *int* unsigned int*等。对于类类型而言,从派生类转换为基类都是标准转换。最后一种类型的转换使我们可以把一个指向派生类对象的指针转换为一个指向基类的指针。

特异性层次4   这是一种更低级别的特异性层次,需要一个用户定义的类型转换才能进行的匹配。

特异性层次 5  通过在函数定义中使用的省略号所得到的匹配是特异性最差的匹配。

我们来看下面这个例子:

#include<iostream>

using namespace std;

void foo(int i,long j)

{

      cout<<"The first foo was called ! ";

}

void foo(long i,int j)

{

      cout<<"The second foo was called ! ";

}

void main()

{

      long x=10,y=10;

      foo(x,y);

}

我们可以看到由于foo(x,y)函数调用的实参类型与第一个和第二个foo函数定义的形参类型匹配。应该选择哪一个呢?我们又进行进一步比较发现第一个和第二个foo函数都符合特异性层次3,所以编译器无法确定应该调用哪个函数,将会报以下错误:

error C2666: 'foo' : 2 overloads have similar conversions

分析可以知道重载解析将选择能够提供最佳匹配的特异性的函数。如果超过一个的函数能够达到最高层次的匹配特异性,那么就会出现歧异,导致一个编译错误。

综上所述,只有函数的签名允许参与重载解析算法。由于返回类型和函数所抛出的异常的类型并不包括在函数的签名中,因此它们对重载解析不会有任何影响。重载解析也与函数的声明顺序无关。此外,在不同的作用域中声明的函数不属于重载的范畴。

1.2.2 C#方法重载解析

C++一样,C#的重载解析也是根据匹配特异性概念进行的,如果存在超过一个的方法,它们的形参与一个方法调用的实参相匹配,编译器将从中选择一个匹配特异性最高的方法。如果超过一个的方法具有同等层次的匹配特异性,那么编译器就会认为产生了歧义,它将发出一条编译时错误信息。C#的重载解析是以下这几个步骤。

(1)    如果给定了适用的候选函数成员集,则在其中选出最佳函数成员。

(2)    如果该集合只包含一个函数成员,则该函数成员为最佳函数成员。

(3)    否则,最佳函数成员的选择依据是:各成员对给定的参数列表的匹配程度。比所有其他函数成员匹配得更好的那个函数成员就是最佳函数成员。且使用以下规则进行比较:
   
给定一个带有参数类型集 {A1, A2, ..., AN} 的参数列表 A 和带有参数类型 {P1, P2, ..., PN} {Q1, Q2, ..., QN} 的两个可应用的函数成员 MP MQ,则在以下情况中,MP 定义为比 MQ 更好的函数成员: 对于每个参数,从 AX PX 的隐式转换都不比从 AX QX 的隐式转换差,并且对于至少一个参数,从 AX PX 的转换比从 AX QX 的转换更好。

(4)    如果不是正好有一个函数成员比所有其他函数成员都好,则函数成员调用不明确并发生编译时错误。

虽然C#的函数重载解析所采用的方法和C++方法大体上是一样的,但算法的细节还是有些不同,其具体情况如下:

(1)    C++所允许的许多类型转换在C#中是不允许的。事实上,在函数调用中,C#只允许从派生类转换为超类或者超接口类(扩大转换),但不允许收缩转换(从double 转换为int32,int16),如果一定要使用这种转换,那么必须显示的使用强制转换符来表明。

(2)    C#中不允许在C++中可以使用的用户定义的类型转换。

(3)    C#中不存在省略号,所以这一特性层在C#中没有。

     我们可以看下面这个例子:

 #include<iostream>

using namespace std;

void foo(int i,int j)

{

      cout<<"The foo was called ! "

<<endl;

}

void main()

{

      long x=10,y=10;

      foo(x,y);

}

程序运行输出是:

The foo was called !

可以看到在C++中是允许把long转换为int 类型进行函数调用的。但是在C#中是不允许把double转换为int类型的。即C#中不允许进行收缩转换,因为有可能会使内存产生溢出。如果非要转换的话,就必须使用类型强制转换符。

1.2.3 C++运算符重载

运算符重载是对已经有的运算符赋予多重含义,使同一个运算符作用于不同的类型的数据导致不同类型的行为。运算符重载实质就是函数重载。在实现过程中,首先把指定的运算表达式转换为对运算符函数的调用,运算对象转化为运算符函数的实参,然后根据实参的类型来确定需要调用的函数,这个过程是在编译过程中完成的。

C++的运算符重载的规则如下:

(1)    C++中运算符除了类属关系运算符“.”、成员指针运算符“*”、作用域分辨符号“::”、sizeof运算符和三目运算符“?”不能被重载,其他的都可以重载,但只能重载C++中已有的运算符。

(2)    重载之后运算符的优先级和结合性都不会改变。

(3)    运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造。一般来讲,重载的功能应当与原有的功能相类似,不能改变原运算符的操作对象个数,同时至少要有一个操作对象是自定义类型。

1.2.4 C#运算符重载

C#中的操作符重载的作用与C++是一致的,但是C#中的操作符重载也有其自己的特点。如下所示:

(1)    C#中,操作符重载总是在类中进行声明,并且通过调用类的成员方法来实现。操作符重载声明的格式为:type operator operator-name(formal-param-list)

(2)    C#中不能被重载的操作符:=&&||?:、newtypeofsizeofis其他的都可以重载,但只能重载C#中已有的运算符。

(3)    C#中的操作符重载与C++唯一一点不同在于C#中的运算符重载函数必须是静态的。

1.3 C++C#中强制多态性分析

数据类型的转换就是,将存储在变量中的数值由一种类型转换成另外一种类型。在C++中,数据类型转换可以是隐式的,也可以是显式的:

      float I = 100.1;

      long j = I;      //隐式转换

      int k = (int)I;   //显式转换

      int m=int(I);   //显式转换,新C++风格

显式的数据类型转换,就是在代码中显式的指出目标数据类型。C++中,有两种显式的数据类型转换格式——旧的C风格,其中数据类型名放在括号内。新风格,其中变量名放在括号内。C++允许在所有基本数据类型之间进行转换。如果目标数据类型的范围比源数据类型的范围小,就会有丢失数据的危险。这时,编译器会根据警告级别的设定来提出警告。在上例中,隐式的数据类型转换可能会导致数据丢失,编译器会提出警告。当进行显式的数据类型转换时,编译器会认为你清楚您所做的事情,所以一般不提出警告。

C#的类型安全性要比C++要高,所以它的数据间的转换不灵活。C#的显式转换和隐式转换都是正式的转换。定义为隐式的转换可以用显式或隐式两种转换来执行。而对定义为显式转换的数据转换使用隐式转换会出现错误。这点与C++不同,C++只会出现警告。

C#中,关于数值类型与其他类型之间的转换规则,是很有逻辑性的。隐式转换不会导致数据丢失——如intlongfloatdouble。显式转换可能会导致数据丢失,引发溢出错误、符号错误或者部分数据损失。比如,floatintintuint或者shortunlong。另外,由于Char与整型是相区别的,所以在与Char的转换中,只能使用显式转换。

下面的代码在C#中是有效的:

float f1 = 420.0F;

long l1 = (long)f1;

short s1 = (short)l1;

int i1 = s1;

uint i2 = (uint)i1;

C#中,显式转换数据类型总是用旧的C风格语法,不能用C++新的风格:

uint i2 = uint(i1);

 C#中,可以在检查环境中执行数据类型转换和其他的数学运算。这意味着.NET运行环境会检测是否发生了溢出。若发生了溢出,则会抛出一个异常(尤其是一个OverFlowException)。C++中不存在这一特性。

Checked

{

   int I1= -3;

   uint I2 =(uint)I1;

}

因为在checked 环境中,第二行代码将会抛出一个异常。如果不指定checked,就不会抛出异常,I2变量中将包含无用信息。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值