7.5 选择重载还是默认参数
函数重载和默认参数都给函数调用提供了方便。然而,有时它也会使人产生困惑:究竟该使用哪一种技术?例如,考虑下面的程序,它用来自动管理内存块。
Mem对象包括一个byte块,以确保有足够的存储空间。默认的构造函数不分配任何的空间。第二个构造函数确保Mem对象中有sz大小的存储区,析构函数释放空间,msize()告诉我们当前Mem对象中还有多少字节,pointer()函数产生一个指向存储区起始地址的指针(Mem是一个相当底层的工具)。可以有一个重载版本的pointer()函数,用这个函数,客户程序员可以将一个指针指向一块内存。该块内存至少有minSize大,有成员函数能够做到这一点。
构造函数和pointer()成员函数都使用private ensureMinSize()成员函数来增加内存块的大小(请注意,如果内存块要调整的话,存放pointer()的结果是不安全的)。
下面是这个类的实现:
可以看到,只有函数ensureMinSize()负责内存分配,它在第二个构造函数和函数pointer()的第二个重载形式中使用。如果size足够大的话,函数ensureMinSize()什么也不需要做,为了使块变得大一些(可能会有这种情况,当使用默认构造函数时,块的大小为零),必须分配新的存储空间,使用标准的C语言库函数memset()把新分配的内存置零,关于这点已在第5章中作了介绍。接着调用标准C语言库函数memcpy(),在这种情况下,把已经存在于mem中内容拷贝到newmem中(通常用一种有效的方式),最后,删除旧的内存,然后把新的内存和大小赋给适当的成员。
设计Mem类的目的是把它作为其他类的一种工具,以简化它们的内存管理(例如,它还可以隐藏由操作系统提供的更复杂的内存管理细节)。下面是一个测试程序,它创建了一个简单的“string”类:
用这个类,所能做的是创建一个MyString,连接文本,打印输出到一个ostream中。该类仅仅包含了一个指向Mem的指针,但是请注意设置指针为零的默认构造函数和第二个构造函数的区别,第二个构造函数创建了一个Mem并把一些数据拷贝给它。使用默认构造函数的好处,就是可以非常便利地创建空值MyString对象的大数组,因为每一个对象只是一个指针,默认构造函数的惟一开销是赋零值。当连接数据时,MyString的开销才会开始增长。在此情况下,只有Mem对象不存在的情况下才会被创建。但是,要是使用默认的构造函数,并且从未连接任何数据,调用析构函数仍然是安全的,因为为零调用的delete已经定义,这样它不会试图释放存储空间,或者另外导致一些问题。
如果观察这两个构造函数,乍一看,好像这是默认构造函数最好的候选,然而,如果删除默认构造函数,像下面用一个默认的参数来写另外一个构造函数:
将会发现,它能正常工作,但是,我们将会失去宝贵的效率,因为Mem对象总是会被创建。为了获得效率,必须修改构造函数:
这也意味着默认值变成了一个标志:使用非默认值将导致需执行的一块代码被单独分离。这样构造一个小的构造函数,虽然看起来很合理,但是一般会导致错误。如果必须查看默认值而不是把它当做一个普通值的话,这就会意味着实际上是在单个函数体中使用两个不同的有效的函数版本:一个版本用于正常情况,另一个版本用于默认情况。我们也许会把它当成两个完全不同的函数体,由编译器来选择究竟使用哪一个。这种做法会稍微提高程序的效率(但是通常情况下不易察觉),因为额外的参数不会被传递,特定条件下的代码也不会被执行。更重要的是,我们使用两个完全不相干的函数维护两个函数的代码,而不是使用默认参数把它们组合成一个函数。这样,维护起来就更容易,尤其是当函数特别大时。
另外一方面,考虑一下Mem类,如果审视两个构造函数和两个pointer()函数时,可以发现:在两种情况下使用默认参数根本不会导致成员函数定义的改变。因此,类的定义可以如下面所示:
注意:调用ensureMinSize(0)总是非常有效。
尽管这两种情况都是基于效率问题作出决定的,但是应该注意不要陷入到只考虑效率的境地(这是诱人的)。设计类时,最重要的问题是类的接口(客户程序员可以使用的public成员)。如果产生的类容易使用和重用,那说明成功了。要是有必要,总是可以为了效率而作适当的调整。但是,如果程序员过分强调效率的话,设计的类的效果将是可怕的。应该主要关心的是接口清晰,使使用和阅读代码的人易于理解。注意MemTest.cpp文件中MyString的语法没有变化,不管一个默认的构造函数是否使用,以及效率是高还是低。