最为基本的线性结构统称为序列$(sequence)$,根据其中数据项的逻辑次序与其物理存储地址的对应关系不同,又可进一步地将序列区分为向量$(vector)$和列表$(list)$。在向量中,所有数据项的物理存放位置与其逻辑次序完全吻合,此时的逻辑次序也称作秩$(rank)$;而在列表中,逻辑上相邻的数据项在物理上未必相邻,而是采用间接定址的方式通过封装后的位置$(position)$相互引用。
按照面向对象思想中的数据抽象原则,可对以上的数组结构做一般性推广,使得其特性更具普遍性。向量$(vector)$就是线性数组的一种抽象与泛化,它也是由具有线性次序的一组元素构成的集合$V = ${$ v_0, v_1, …, v_{n-1} $},其中的元素分别由秩相互区分。具体地,若元素$e$的前驱元素共计$r$个,则其秩就是$r$。
在此序列中,各递归实例的秩反映了它们各自被创建的时间先后,每一递归实例的秩等于早于它出现的实例总数。反过来,通过$r$亦可唯一确定$e = v_r$。这是向量特有的元素访问方式,称作“循秩访问”$(call-by-rank)$。此时不再限定同一向量中的各元素都属于同一基本类型,它们本身可以是来自于更具一般性的某一类的对象。另外,各元素也不见得同时具有某一数值属性,故而并不保证它们之间能够相互比较大小。
构造与析构
构造函数就是对类对象进行初始化赋值。构造函数由编译器自动调用,且整个过程只调用一次。与所有的对象一样,向量在使用之前也需首先被系统创建——借助构造函数$(constructor)$做初始化$(initialization)$。
public: // 构造函数 Vector(int c = DEFAULT_CAPACITY, int s = 0, T v = 0) //容量为 c、规模为 s、所有元素初始为 v { _elem = new T[_capacity = c]; for (_size = 0; _size < s; _elem[_size++] = v); } //s<=c Vector(T const* A, Rank lo, Rank hi) { copyFrom(A, lo, hi); } //数组区间复制 Vector(T const* A, Rank n) { copyFrom(A, 0, n); } //数组整体复制 Vector(Vector<T> const& V, Rank lo, Rank hi) { copyFrom(V._elem, lo, hi); } //向量区间复制 Vector(Vector<T> const& V) { copyFrom(V._elem, 0, V._size); } //向量整体复制
由上文代码可见,这里为向量重载了多个构造函数。其中默认的构造方法是,首先根据创建者指定的初始容量,向系统申请空间,以创建内部私有数组 _$elem[]$;若容量未明确指定,则使用默认值$DEFAULT$_$CAPACITY$。接下来,鉴于初生的向量尚不包含任何元素,故将指示规模的变量 _$size$初始化为$0$。整个过程顺序进行,没有任何迭代,故若忽略用于分配数组空间的时间,共需常数时间。
向量的另一典型创建方式,是以某个已有的向量或数组为蓝本,进行(局部或整体的)克隆。前文代码中虽为此功能重载了多个接口,但无论是已封装的向量或未封装的数组,无论是整体还是区间,在入口参数合法的前提下,都可归于如下所示的统一的$copyFrom()$方法:
template <typename T> //元素类型 void Vector<T>::copyFrom(T const* A, Rank lo, Rank hi) { //以数组区间 A[lo, hi)为蓝本复制向量 _elem = new T[_capacity = 2 * (hi - lo)]; _size = 0; //分配空间,规模清零 while (lo < hi) //A[lo, hi)内的元素逐一 _elem[_size++] = A[lo++]; //复制至 _elem[0, hi - lo) }
$copyFrom()$首先根据待复制区间的边界,换算出新向量的初始规模;再以双倍的容量,为内部数组 _$elem[]$申请空间。最后通过一趟迭代,完成区间$A[lo, hi)$内各元素的顺次复制。若忽略开辟新空间所需的时间,运行时间应正比于区间宽度,即$O(hi – lo) = O($_$size)$。
由于向量内部含有动态分配的空间,默认的运算符”=”不足以支持向量之间的直接赋值。通过默认赋值运算符,并不能复制向量内部的数据区。为适应此类赋值操作的需求,可像如下代码所示,重载向量的赋值运算符:
template <typename T> Vector<T>& Vector<T>::operator=(Vector<T> const& V ) { //重载赋值操作符 if (_elem) delete [] _elem; //释放原有内容 copyFrom(V._elem, 0, V.size()); //整体复制 return *this; //返回当前对象癿引用,以便链式赋值 }
析构函数:对象销毁前自动调用,执行一些清理工作。在程序执行的过程中,当遇到对象的生存期结束时系统会自动调用析构函数。然后回收为对象分配的存储空间。析构函数不是按照构造的顺序进行析构,且不能进行重载。如果类内有其他成员函数也是先析构本身。一个对象只能有一个析构函数,但是可以有多个构造函数。且构造函数和析构函数在整个程序运行过程中只执行一次。
向量对象的析构过程,如下代码中的方法$~Vector()$所示:只需释放用于存放元素的内部数组 _$elem[]$,将其占用的空间交还操作系统。_$capacity$和 _$size$之类的内部变量无需做任何处理,它们将作为向量对象自身的一部分被系统回收,此后既无需也无法被引用。若不计系统用于空间回收的时间,整个析构过程只需$O(1)$时间。
public: // 析构函数 ~Vector() { delete [] _elem; } //释放内部空间