第7章 函数重载与默认参数
能使名字方便使用,是任何程序设计语言的一个重要特征。
当我们创建一个对象(一个变量)时,要为存储区取一个名字。函数就是一个操作的名字。通过编制各种名字来描述身边的系统,我们可以产生易于被人们理解和修改的程序。这在很大程度上就像是写文章—其目的是与读者进行交流。
这里就产生了这样一个问题:如何把人类自然语言中有细微差别的概念映射到程序设计语言中。通常,自然语言中同一个词可以代表多种不同的含义,具体含义要依赖上下文来确定。这就是所谓的一词多义—该词被重载(overload)了。这点非常有用,特别是对于细微的差别。我们可以说“洗衬衫,洗汽车”。如果非得说成“衬衫-洗衬衫,汽车-洗汽车”,那将是很愚蠢的,就好像听话的人对指定的动作毫无辨别能力一样。人类语言都有内在的冗余,所以即使漏掉几个词,我们仍然可以知道其中的含义。我们不需要惟一标识符—我们可以从上下文中理解它的含义。
然而,大多数程序设计语言要求我们为每个函数设定一个惟一标识符。如果我们想打印三种不同类型的数据:int、char和float,通常不得不创建三个不同的函数名,如print_int()、print_char()和print_float(),这些既增加了我们的编程工作量,也给读者理解程序增加了困难。
在C++中,还有另外一个因素会使函数名重载:构造函数。因为构造函数的名字预先由类的名字确定,所以看上去只能有惟一一个构造函数名。但如果我们想用多种方法来创建一个对象时该怎么办呢?例如假设创建一个类,这个类可以用标准的方法初始化自身,也可以通过从文件中读取信息来初始化,我们需要两个构造函数,一个不带参数(默认构造函数),另一个以一个字符串作为参数,这个字符串是初始化对象的文件的名字。两个都是构造函数,所以它们必须有相同的名字:类名。因此,函数重载对于允许函数同名是必不可少的。在这种情况下,构造函数是与不同的参数类型一起使用的。
尽管函数重载对构造函数来说是必须的,但是它仍是一个通用的方便手段,并且可以与任意函数(不仅包括类成员函数)一起使用。另外,函数重载意味着,我们有两个库,它们都有同名的函数,只要它们的参数列表不同就不会发生冲突。我们将在本章中详细讨论所有这些问题。
本章的主题就是方便地使用函数名。函数重载允许多个函数同名,但还有第2种方法使函数调用更方便。如果我们想以不同的方法调用同一个函数,该怎么办呢?当函数有一个长长的参数列表时,而大多数参数每次调用都一样时,书写这样的函数调用会使人厌烦,程序可读性也差。C++中有一个很通用的特征叫做默认参数(default argument)。默认参数就是在用户调用一个函数时没有指定参数值而由编译器插入参数值的参数。因此,f(“hello”)、f(“hi”,1)和f(“howdy”,2,‘c’)可以用来调用同一个函数。它们也可能用来调用三个已重载的函数,但当参数列表相同时,我们通常希望调用同一个函数来完成相同的操作。
函数重载和默认参数实际上并不复杂。当我们学习完本章时,我们就会明白什么时候要用到它们,以及编译、连接时它们是怎样实现的。
7.1 名字修饰
在第4章中介绍了名字修饰(name decoration)的概念。在下面的代码中:
class X内的函数f()不会与全局的f()发生冲突,编译器用不同的内部名f()(全局函数)和X:f()(成员函数)来区分两个函数。在第4章中,我们建议在函数名前加类名的方法来命名函数,所以编译器使用的内部名字可能就是_f和_X_f。函数名不仅与类名关系密切,而且还跟其他因素有关。
为什么要这样呢?假设重载了两个函数名:
无论这两个函数是某个类的成员函数,还是全局函数都无关紧要。如果编译器只使用函数名的域,编译器并不能产生惟一的内部标识符,这两种情况下都得用_print结尾。重载函数的思想是让我们用同名的函数,但这些函数的参数列表应该不一样。所以,为了让重载函数正确工作,编译器要用不同的参数类型来修饰不同的函数名。上面的两个在全局范围定义的函数,可能会产生类似于_print_char和_print_float的内部名。因为,要注意编译器如何为这样的名字修饰没有统一的标准,所以不同的编译器可能会产生不同的内部名(让编译器产生汇编语言代码后就可以看到这个内部名是个什么样子了)。当然,如果想为特定的编译器和连接器购买编译过的库的话,这就会引起错误。但是即使名字修饰有统一的标准,因为编译器用不同的方式产生代码,也还会出现其他问题。
有关函数重载就讲到这里,可以对不同的函数用同样的名字,只要求函数的参数不同。编译器会修饰这些名字、范围和参数来产生内部名以供它和连接器使用。
7.1.1 用返回值重载
读了上面的介绍,我们自然会问:“为什么只能通过范围和参数来重载,为什么不能通过返回值呢?”乍一听,似乎完全可行,而且还用内部函数名修饰了返回值,然后就可以用返回值重载了:
当编译器能从上下文中惟一确定函数的意思时,如int x=f();这当然没有问题。然而,在C中,总是可以调用一个函数但忽略它的返回值,即调用了函数的副作用(side effect),在这种情况下,编译器如何知道调用哪个函数呢?更糟的是,读者怎么知道哪个函数会被调用呢?仅仅靠返回值来重载函数实在过于微妙了,所以在C++中禁止这样做。