C++批判(4)

函数重载


 C++允许在参数类型不同的前提下重载函数。重载的函数与具有多态性的函数(即虚函数)不同处在于:调用正确的被重载函数实体是在编译期间就被决定了的;而对于具有多态性的函数来说,是通过运行期间的动态绑定来调用我们想调用的那个函数实体。多态性是通过重定义(或重写)这种方式达成的。请不要被重载 (overloading)和重写(overriding)所迷惑。重载是发生在两个或者是更多的函数具有相同的名字的情况下。区分它们的办法是通过检测它们的参数个数或者类型来实现的。重载与CLOS中的多重分发(multiple dispatching)不同,对于参数的多重分发是在运行期间多态完成的。

 

 【Reade 89】中指出了重载与多态之间的不同。重载意味着在相同的上下文中使用相同的名字代替出不同的函数实体(它们之间具有完全不同的定义和参数类型)。多态则只具有一个定义体,并且所有的类型都是由一种最基本的类型派生出的子类型。C. Strachey指出,多态是一种参数化的多态,而重载则是一种特殊的多态。用以判断不同的重载函数的机制就是函数标示(function signature)。

 

 重载在下面的例子中显得很有用:


 max( int, int )

 max( real, real )

 

  这将确保相对于类型int和real的最佳的max函数实体被调用。但是,面向对象的程序设计为该函数提供了一个变量,对象本身被被当作一个隐藏的参数传递给了函数(在C++中,我们把它称为this)。由于这样,在面向对象的概念中又隐式地包含了一种对等的但却更有更多限制的形式。对于上述讨论的一个简单例子如下:


 int i, j;

 real r, s;

 i.max(j);

 r.max(s);

 

 但如果我们这样写:i.max(r),或是r.max(j),编译器将会告诉我们在这其中存在着类型不匹配的错误。当然,通过重载运算符的操作,这样的行为是可以被更好地表达如下:


 i max j 或者 r max s


 但是,min和max都是特殊的函数,它们可以接受两个或者更多的同一类型的参数,并且还可以作用在任意长度的数组上。因此,在Eiffel中,对于这种情况最常见的代码形式看起来就像这样:


 il:COMPARABLE_LIST[INTEGER]

 rl:COMPARABLE_LIST[REAL]

 

 i := il.max

 r := rl.max

 

  上面的例子显示,面向对象的编程典范(paradigm),特别是和范型化(genericity)结合在一起时,也可以达到函数重载的效果而不需要C+ +中的函数重载那样的声明形式。然而是C++使得这种概念更加一般化。C++这样作的好处在于,我们可以通过不止一个的参数来达到重载的目的,而不是仅使用一个隐藏的当前对象作为参数这样的形式。

 

 另外一个我们需要考虑的因素是,决定(resolved)哪个重载函数被调用是在编译阶段完成的事情,但对于重写来说则推后到了运行期间。这样看起来好像重载能够使我们获得更多性能上的好处。然而,在全局分析的过程中编译器可以检测函数min 和max是否处在继承的最末端,然后就可以直接的调用它们(如果是的话)。这也就是说,编译器检查到了对象i和r,然后分析对应于它们的max函数,发现在这种情况下没有任何多态性被包含在内,于是就为上面的语句产生了直接调用max的目标代码。与此相反的是,如果对象n被定义为一个NUMBER, NUMBER又提供一个抽象的max函数声明(我们所用的REAL.max和INTERGER.max都是从它继承来的),那么编译器将会为此产生动态绑定的代码。这是因为n既可能是INTEGER,也有可能是REAL。

 

 现在你是不是觉得C++的这种方法(即通过提供不同的参数来实现函数的重载)很有用?不过你还必须明白,面向对象的程序设计对此有着种种的限制,存在着许多的规则。C++是通过指定参数必须与基类相符合的方式实现它的。传入函数中的参数只能是基类,或是基类的派生类。


例如:


 A.f( B someB )

 class B …;

 class D : public B …;

 A a;

 D d;

 a.f( d );


 其中d必须与类’B’相符,编译器会检测这些。

 

  通过不同的函数签名(signature)来实现函数重载的另一种可行的方法是,给不同的函数以不同的名字,以此来使得它们的签名不同。我们应该使用名字来作为区分不同实体(entities)的基础。编译器可以交叉检测我们提供的实参是否符合于指定的函数需要的形参。这同时也导致了软件更好的自记录(self-document)。从相似的名字选择出一个给指定的实体通常都不会很容易,但它的好处确实值得我们这样去做。

 

 [Wiener95]中提供了一个例子用以展示重载虚拟函数可能出现的问题:


 class Parent

 {

  public:

   virutal int doIt( int v )

   {

    return v * v;

   }

 };

 

 class Child: public Parent

 {

  public:

   int doIt( int v, int av = 20 )

   {

    return v * av;

   }

 };

 

 int main()

 {

  int i;

  Parent *p = new Child();

  i = p->doIt(3);

  return 0;

 }

 

 当程序执行完后i会等于多少呢?有人可能会认为是60,然而结果却是9。这是因为在Child中doIt的签名与在Parent中的不一致,它并没有重写Parent中的doIt,而仅仅是重载了它,在这种情况下,缺省值没有任何作用。

再来看看这个例子,绝对让你抓狂,猜猜看输出的i和j值是多少?

#include <stdio.h>

class PARENT

{

public:

    virtual int doIt( int v, int av = 10 )

    {

         return v * v;

    }

};


class CHILD : public PARENT

{

public:

    int doIt( int v, int av = 20 )

    {

         return v * av;

    }

};


int main()

{

    PARENT *p = new CHILD();


    int i = p->doIt(3);

    printf(“i = %d\n”, i);


    CHILD* q = new CHILD();


    int j = q->doIt(3);

    printf(“j = %d\n”, j);


    return 0;

}

 

 Java也提供了方法重载,不同的方法可以拥有同样的名字及不同的签名。

 

 在Eiffel中没有引入新的技术,而是使用范型化、继承及重定义等。Eiffel提供了协变式的签名方式,这意味着在子类的函数中不需要完全符合父类中的签名,但是通过Eiffel的强类型检测技术可以使得它们彼此相匹配。