重载函数在运行时刻的行为与非重载函数完全一样,主要的负担是在编译时刻用来决定
该调用哪个实例所需要的时间,如果C++不提供函数重载支持,那么我们就必须为程序中
每个函数都要提供一个独一无二的名字,第9 章将详细讨论函数重载的内容
我们为IntArray 类指定了下面三个构造函数
class IntArray {
public:
explicit IntArray( int sz = DefaultArraySize );
IntArray( int *array, int array_size );
IntArray( const IntArray &rhs );
// ...
private:
static const int DefaultArraySize = 12;
// ...
};
构造函数
IntArray( int sz = DefaultArraySize );
被称为缺省构造函数default constructor ,因为它不需要用户提供任何参数,我们
现在不打算解释这个,缺省构造函数声明中出现的关键字explicit 之所以显示,它仅仅是为了完
整性,如果程序员提供参数则该值将被传递给构造函数,例如
IntArray array1( 1024 );
将参数1024 传递给构造函数,另一方面,如果用户不指定长度,那么构造函数将使用
DefaultArraySize 的值,例如
IntArray array2;
将导致用DefaultArraySize 的值来调用,构造函数被声明为static 的数据成员是一类特
殊的共享数据成员,无论这个类的对象被定义了多少个,静态数据成员在程序中也只有一份,
这是在类的所有对象之间共享数据的一种方式,13.5 节有全面的讨论。
下面是缺省构造函数的一个简化实现版本,简化到没有考虑出错的可能性,可能出现
什么错误呢?本例中有两个首先提供给程序的动态内存不是无限的,因此new 表达式
有可能失败,2.6 节中将介绍如何处理这种情况。第二,传递给参数sz 的值可能是无效的。
例如负数或0,或者一个很大的值,以至于无法存储在int 类型的变量中
IntArray::
IntArray( int sz )
{
// 设置数据成员
size = sz;
ia = new int[ _size ];
// 初始化内存
for ( int ix=0; ix < _size;
++ix )
ia[ ix ] = 0;
}
这是我们第一次在类体的外面定义类的成员函数,惟一的语法区别是要指出成员函数属
于哪个类,这可以通过类域操作符class scope operator 来实现
IntArray::
双冒号:: 操作符被称为域操作符scope operator,当与一个类名相连的时候,像上
面例子中那样它就成为一个类域操作符,我们可以非正式地把域看作是一个可视窗口,全
局域的对象在它被定义的整个文件里一直到文件末尾都是可见的,在一个函数内被定义
的对象是局域的local scope,它只在定义其他的函数体内可见,每个类维持一个域,在这
个域之外,它的成员是不可见的。类域操作符告诉编译器后面的标识符可在该类的范围内被找到,本例中
IntArray::
IntArray( int sz )
告诉编译器IntArray()函数被定义为IntArray 类的成员,尽管程序域不是我们现在应该关注的事情,但是最终我们还是要理解域的概念,在第8 章中我们将详细讲解程序域,而类域将在13.9 节中特别讨论。
IntArray 类的第二个构造函数用内置的整数数组初始化一个新的IntArray 类对象,它需
要两个参数,一个是实际的数组,另一个参数指明数组的大小,例如
int ia[10] = {0,1,2,3,4,5,6,7,8,9};
IntArray iA3(ia,10);
这个构造函数的实现几乎与第一个构造函数相同,这里我们又一次没有保护自己的代码
IntArray::
IntArray( int *array, int sz )
{
// 设置数据成员
size = sz;
ia = new int[ _size ];
// 拷贝数据成员
for ( int ix=0; ix < _size; ++ix )
iz[ix ] = array[ ix ];
}
最后一个IntArray 构造函数用另外一个IntArray 对象来初始化,当前的IntArray 对象对
于下面两种形式无论是哪一种它都将被自动调用
IntArray array;
// 等价的初始化方式
IntArray ia1 = array;
IntArray ia2( array );
这种构造函数被称为类的拷贝构造函数copy constructor,在后面的章节中我们将会
看到更多的示例,下面的实现再次忽略了可能出现的运行时刻程序异常
IntArray::
IntArray( const IntArray &rhs ){
// 拷贝构造函数
_size = rhs._size;
ia = new int[ _size ];
for (int ix = 0; ix < _size; ix++ )
iz[ ix ] = rhs.ia[ ix ];
}
本例引入了一种新的复合类型引用reference, 即IntArray &rhs,引用是一种没有
指针语法的指针,因此我们写成rhs._size 而不是rhs->_size 与指针一样,引用提供
对对象的间接访问,3.6 节中我们将对指针和引用作更多的介绍
注意这三个构造函数都是以相似的方式来实现的,一般来说,当两个或多个函数重复
相同的代码时,就会将这部分代码抽取出来形成独立的函数,以便共享以后如果需要
改变这些实现,则只需改变一次,而且这种实现的共享本质更容易为大家所理解。
怎么样把构造函数中的代码抽取出来形成独立的共享函数呢?下面是一种可能的实现
class IntArray {
public:
// ...
private:
void init( int sz, int *array );
// ...
};
void
IntArray::
init( intsz, int *array )
{
_size = sz;
ia = new int[ _size ];
for ( int ix=0; ix < _size; ++ix )
if ( ! array )
ia[ ix ] = 0;
else ia[ ix ] = array[ ix ];
}
三个构造函数可重写为
IntArray::IntArray( int sz ){ init( sz, 0 ); }
IntArray::IntArray( int *array, int sz )
{ init( sz, array ); }
IntArray::IntArray( const IntArray &rhs )
{ init( rhs.size, rhs.ia ); }
类机制还支持特殊的析构成员函数destructor member function,每个类对象在被程序
最后一次使用之后,它的析构函数就会被自动调用,我们通过在类的名字前面加一个波浪线
~ 来标识析构函数。一般地,析构函数会释放在类对象使用和构造过程中所获得的资源,
例如,在IntArray 的析构函数中,它会删除构造时分配的内存,我们将在第14 章详细讨论
构造函数和析构函数,下面是我们的实现
class IntArray {
public:
// 构造函数
explicit IntArray( int size = DefaultArraySize );
IntArray( int *array, int array_size );
IntArray( const IntArray &rhs );
// 析构函数
~IntArray() { delete [] ia; }
// ...
private:// ...
};
除非用户能够很容易地通过索引访问单个元素,否则数组类就没有更实际的用处,例如
我们的类需要支持下面的一般用法
IntArray array;
int last_pos = array.size()-1;
int temp = array[ 0 ];
array[ 0 ] = array[ last_pos ];
array[ last_pos ] = temp;
我们通过提供专用于一个类的下标操作符实例来支持索引IntArray 类对象,下面
是支持这种用法的一个实现
#include <cassert>
int&
IntArray::
operator[]( int index )
{
assert( index >= 0 && index < size );
return ia[ index ];
}
一般而言,C++语言支持操作符重载operator overloading,这样,就可以为特定的类类
型定义新的操作符实例,典型地类提供一个或多个赋值操作符、等于操作符可能还有
一个或多个关系操作符以及iostream 输入和输出操作符,3.15 节有关于操作符重载的更
进一步的说明,在第15 章我们将详细讨论操作符重载。
类定义以及相关的常数值或typedef 名通常都存储在头文件中,并且头文件以类名来命
名。因此,假如我们创建一对头文件IntArray.h 和Matrix.h,则所有打算使用IntArray 类或Matrix 类的程序就都必须包含相关的头文件。
类似地,不在类定义内部定义的类成员函数都存储在与类名同名的程序文本文件中,例如我们将创建一对程序文本文件IntArray.C 和Matrix.C ,用来存储相关类的成员函数。
记住程序文本文件的后缀因编译系统而不同,你应该检查自己的系统所使用的命名习惯。
这些函数不用随每个使用相关类的程序而重新编译,这些成员函数经过预编译之后被保存在
类库中,iostream 库就是这样一个例子。