会对函数重载和设定参数缺省值产生混淆的原因在于,它们都允许一个函数以多种方式被调用:
1 2 3 4 5 6 7 8 |
void f(); // f被重载 void f(int x); f(); // 调用f() f(10); // 调用f(int) void g(int x = 0); // g 有一个 // 缺省参数值 g(); // 调用g(0) g(10); // 调用g(10) |
那么,什么时候该用哪种方法呢?
答案取决于另外两个问题。第一,确实有那么一个值可以作为缺省吗?第二,要用到多少种算法?一般来说,如果可以选择一个合适的缺省值并且只是用到一种算法,就使用缺省参数(参见决不要重新定义继承而来的缺省参数值)。否则,就使用函数重载。
下面是一个最多可以计算五个int的最大值的函数。这个函数使用了——深呼一口气,看清楚啦——std::numeric_limits<int>::min()
,作为缺省参数值。等会儿再进一步介绍这个值,这里先给出函数的代码:
1 2 3 4 5 6 7 8 9 10 11 |
int max(int a, int b = std::numeric_limits::min(), int c = std::numeric_limits::min(), int d = std::numeric_limits::min(), int e = std::numeric_limits::min()) { int temp = a > b ? a : b; temp = temp > c ? temp : c; temp = temp > d ? temp : d; return temp > e ? temp : e; } |
现在可以放松了。std::numeric_limits<int>::min()是c++标准库用一种特有的新方法所表示的一个在c里已经定义了的东西,即c在<limits.h>中定义的int_min宏所表示的那个东西——处理你的c++原代码的编译器所产生的int的最小可能值。是的,它的句法背离了c所具有的简洁,但在那些冒号以及其它奇怪的句法背后,是有道理可循的。
假设想写一个函数模板,其参数为固定数字类型,模板产生的函数可以打印用“实例化类型”表示的最小值。这个模板可以这么写:
1 2 3 4 5 |
template void printminimumvalue() { cout << 表示为t类型的最小值; } |
如果只是借助<limits.h>和<float.h>来写这个函数会觉得很困难,因为不知道t是什么,所以不知道该打印int_min还是dbl_min,或其它什么类型的值。
为避开这些困难,标准c++库(熟悉标准库)在头文件<limits>
中定义了一个类模板numeric_limits,这个类模板本身也定义了一些静态成员函数。每个函数返回的是“实例化这个模板的类型”的信息。也就是说,numeric_limits<int>中的函数返回的信息是关于类型int的,numeric_limits<double>
中的函数返回的信息是关于类型double的。numeric_limits中有一个函数叫min,min返回可表示为“实例化类型”的最小值,所以numeric_limits<int>::min()返回的是代表整数类型的最小值。
有了numeric_limits(和标准库中其它东西一样,numeric_limits存在于名字空间std中;numeric_limits本身在头文件<limits>中),写printminimumvalue就可以象下面这样容易:
1 2 3 4 5 |
template void printminimumvalue() { cout << std::numeric_limits::min(); } |
采用基于numeric_limits的方法来表示“类型相关常量”看起来开销很大,其实不然。因为原代码的冗长的语句不会反映到生成的目标代码中。实际上,对numeric_limits的调用根本就不产生任何指令。想知道怎么回事,看看下面,这是numeric_limits<int>::min的一个很简单的实现:
1 2 3 4 5 |
#include namespace std { inline int numeric_limits<int>::min() throw () { return int_min; } } |
因为此函数声明为inline,对它的调用会被函数体代替(明智地使用内联)。它只是个int_min,也就是说,它本身仅仅是个简单的“实现时定义的常量”的#define。所以即使本条款开头的那个max函数看起来好象对每个缺省参数进行了函数调用,其实只不过是用了另一种聪明的方法来表示一个类型相关常量而已(本例中常量值为int_min)。象这样一些高效巧妙的应用在c++标准库里俯拾皆是,这可以参考熟悉标准库。
回到max函数上来:最关键的一点是,不管函数的调用者提供几个参数,max计算时采用的是相同(效率很低)的算法。在函数内部任何地方都不用在意哪些参数是“真”的,哪些是缺省值;而且,所选用的缺省值不可能影响到所采用的算法计算的正确性。这就是使用缺省参数值的方案可行的原因。
对很多函数来说,会找不到合适的缺省值。例如,假设想写一个函数来计算最多可达5个int的平均值。这里就不能用缺省参数,因为函数的结果取决于传入的参数的个数:如果传入3个值,就要将总数除以3;如果传入5个值,就要将总数除以5。另外,假如用户没有提供某个参数时,没有一个“神奇的数字”可以作为缺省值,因为所有可能的int都可以是有效参数。这种情况下就别无选择:必须重载函数:
1 2 3 4 5 |
double avg(int a); double avg(int a, int b); double avg(int a, int b, int c); double avg(int a, int b, int c, int d); double avg(int a, int b, int c, int d, int e); |
另一种必须使用重载函数的情况是:想完成一项特殊的任务,但算法取决于给定的输入值。这种情况对于构造函数很常见:“缺省”构造函数是凭空(没有输入)构造一个对象,而拷贝构造函数是根据一个已存在的对象构造一个对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// 一个表示自然数的类 class natural { public: natural(int initvalue); natural(const natural& rhs); private: unsigned int value; void init(int initvalue); void error(const string& msg); }; inline void natural::init(int initvalue) { value = initvalue; } natural::natural(int initvalue) { if (initvalue > 0) init(initvalue); else error("illegal initial value"); } inline natural::natural(const natural& x) { init(x.value); } |
输入为int的构造函数必须执行错误检查,而拷贝构造函数不需要,所以需要两个不同的函数来实现,这就是重载。还请注意,两个函数都必须对新对象赋一个初值。这会导致在两个构造函数里出现重复代码,所以要写一个“包含有两个构造函数公共代码”的私有成员函数init来解决这个问题。这个方法——在重载函数中调用一个“为重载函数完成某些功能”的公共的底层函数——很值得牢记,因为它经常有用(见尽量使用初始化而不要在构造函数里赋值)。