下面用前面的一个例子来说明,为数组定义一个类模板,该数组要对索引值进行边界检查,确保索引值是合法的。尽管标准库提供了数组模板的完整实现方式,但建立自己的数组模板有助于理解模板的工作原理。我们已经很清楚数组的工作原理了,因此下面集中讨论模板的特性。这也更容易使用第20章介绍的标准库中的 Array 模板。
数组模板只有一个类型参数,所以该模板的定义如下:
template <typename T> class Array{ //definition of the template.. }
Array 模板只有一个类型参数 T 。说它是类型参数,是因为它的前面有关键字 typename。 在实例化模板时,为这个参数指定的参数,如 int 、 double*、 string 等,确定了存储在相关类对象中的元素类型。模板体中的定义与类定义非常类似,其数据成员和成员函数可以声明为 Public、 protected 或 private,类模板一般有构造函数和析构函数。可以使用 T 或类型指针 T*声明变量或指定成员函数的参数或返回类型,而且,还可以把模板名称(在本例中是 Array )用作类型名称,也可以在构造函数和析构函数的声明中使用模板名称。
要利用类接口,至少需要构造函数、副本构造函数(因为要为数组动态地分配内存)、副本赋值运算符(因为如果没有提供该运算符,编译器就会提供一个),重载的下标运算符和析构函数。因此,模板的最初定义如下所示:
template <typename T> class Array{ private: T* elements; // Array of type T size_t size; // Number of array elements public: explicit Array<T>(size_t arraySize); // Constructor Array<T>(const Array<T>& array); // Copy Constructor ~Array<T>(); // Destructor T& operator[](size_t index); // Subscript operator const T& operator[](size_t index) const; // Subscript operator-const arrays Array<T>& operator=(const Array<T>& rhs); // Assignment operator };
模板体看起来类似于普通的类定义,只是在许多地方都使用了T。例如,有一个数据成员 elements ,其类型是 T 的指针(等价于 T 的数组)。在实例化类模板,生成一个类定义时, T 就会被用于实例化模板的类型代替.如果为 double 类型创建模板的一个实例, elements 的类型就是 doouble 数组。
给成员 size 使用 size_t 类型,它存储一数组中的元素个数。这是在标准头文件<cstddef>,中定义的标准整数类型,对应于sizeof()运算符返回的类型值。它是指定数组维数的首选类型。注意第一个构造函数声明为 explicit 。因为这个函数带有一个整数参数,所以编写构造函数调用有两种方式:
Array<int> data(5); //explicit constructor notation Array<int> numbers=5 //assignment-like notation
把构造函数声明为 explicit ,可以防止出现第二种语法形式(很不直观),还可以防止给需要 Array<int>类型参数的函数传送整数。构造函数中如果没有 explicit 声明,编译器就会插入一个构造函数调用,把整数参数转换为 Array<int>类型。
下标运算符重载为const。第 9 章介绍了带有 const 参数和非const参数的重载函数。下标运算符的非 const 版本应用于非 const 数组对象,可以返回数组元素的一个非 const 引用。因此这个版本可以放在等号的左边, const 版本用于调用const 对象.返回元素的const 引用。显然它不能放在等号的左边。
在副本赋值运算符的声明中,使用了类型 Array<T>&。这个类型是 Array<T>的引用。在类从模板中综合处埋时,例如 T 是 double 类型. Array<T>&就是该类的类名引用,也就是Array<double>。一般来说,模板某个实例的类名足由模板名后跟尖括号中的类型参数组成的。
模板名后跟尖括号中的参数名列表称为模板ID。
在模板定义中,不需要使用完整的模板ID。在类模板体中, Array 本身就表示 Array<T>, 而 Array&表示 Array<T>&,所以,可以把类模板定义简化为:
template <typename T> class Array{ private: T* elements; // Array of type T size_t size; // Number of array elements public: explicit Array(size_t arraySize); // Constructor Array(const Array& array); // Copy Constructor ~Array(); // Destructor T& operator[](size_t index); // Subscript operator const T& operator[](size_t index) const; // Subscript operator-const arrays Array& operator=(const Array& rhs); // Assignment operator size_t getSize() { return size; } // Accessor for size };
提示: 如果需要在模板体的外部标识模板,必须使用模板ID,在本章后面定义类模板的成员函数时,就要用到模板 ID。
赋值运算符允许把一个数组对象赋予另一个数组对象,而 C++中的一般数组不能这样做。如果因某种原因要保留这个功能,仍旧需要把operator=()函数声明为模板的一个成员。否则,在需要对模板实例进行这个操作时,就会创建一个默认的公共赋值运算符.为了不使用赋值运算符,可以把它声明为类的私有成员,这样就不能访问它了。当然,在这种情况下,该成员函数不需要实现代码,因为除非要使用成员函数,否则 C++不需要实现它,而这个成员函数是从来都不会使用的。
定义类模板的成员函数
可以在类模板体中包含它的成员函数的定义。在这种情况下,成员函数险含为模板所有实例的内联函数,这与普通的类一样。但是,有时需要在模板体的外部定义成员函数,特别是在成员函数包含许多代码时,就更是如此。在模板体的外部定义成员函数时,语法有些不同,初看时会觉得很令人沮丧。下面就介绍该语法。
理解该语法的线索是模板类的成员函数的定义本身就是函数模板。定义成员函数的函数模板中的参数列表必须与类模板的参数列表完全相同。这听起来有点混乱,下面举例说明。我们为 Array 模板的成员函数编写定义.首先编写构造函数。
在类模板定义的外部定义构造函数时,构造函数的名称必须用类模板名称来限定,所采用的方式与普通类成员函数相同。但是,这不是函数定义,而是函数定义的模板,所以也必须表示出来。下面是构造函数的定义:
template <typename T> // This is a template with parameter T Array<T>::Array(size_t arraySize) : size (arraySize){elements = new T[size];}
其中,第一行把这个函数标识为模板,还把模板参数指定为T。把模板函数声明放在两行上,只是为了演示得更清晰,如果整个声明可以放在一行上,就不必放在两行上。
在限定构造函数的名称 Array<T>时,模板参数是必须的,因为它把函数定义和类模板联系起来。注意这里没有使用关键字 typename,该关键字仅用于模板参数列表.在构造函数名的后面不需要列出参数。在为类模板的实例实例化构造函数时,例如为类型 double 实例化
构造函数,类型名会替换掉构造函数限定符中的T,于是类 Array<double>的限定构造函数名应是Array<double>::Array()。
在构造函数中,必须在自由存储区中为elements数组分配内存,该数组包含 size 个类型T的元素,如果T是类类型,就必须在类 T 中包含一个默认的公共构造函数。如果 T 不是类类型,就不会编译这个构造函数的实例。如果不能分配内存,运算符 new 就会抛出bad_alloc
异常。 Array 构造函数应总是放在 try 块中。
析构函数必须释放 elements 数组的内存,其定义如下所示:
template <typename T> Array<T>::~Array() { delete[] elements; }
要释放数组专用的内存,必须使用 delete 运算符的正确形式。
副本构造函数必须为要创建的对象创建一个数组.其大小与参数相同,接着把后者的数据成员复制到前者中。其定义代码如下:
template <typename T> Array<T>::Array(const Array& theArray) { size=theArray.size; elements = new T[size]; for(int i=0;i<size;i++) elements[i] = theArray.elements[i]; }
这段代码假定赋值运算符处理类型 T 。这对为动态分配内存的类定义赋值运算符是非常重要的.如果类 T 没有定义副本构造函数,就使用 T 的默认副本构造函数,但这样动态分配内存的类会出现不希望的负面效应,如第 13 章所述。在使用之前不杳看模板的代码,就可能认识不到副本构造函数对赋值运算符的依赖关系。
operator[]()函数相当简单,但应确保不能使用不合法的索引值。对于超出范围的索引值,可以抛出一个异常:
template <typename T> T& Array<T>::perator[](size_t index){ if(index<0 || index >=size) throw std::out_of_range(index<0?"Negative index":"Index too large"); return elements[index]; }
这里可以定义自己的异常类,但借用在标准库的<stdexcept>头文件中定义的out_of_range类会更容易。例如,如果用超出范圈的索引值引用string对象,就会抛出该异常.这样用法就一致了.如果索引的值不在0到size-l 之间,就抛出 out_of_range类型的异常。构造函数的参数是一个描述错误的 string 对象,对应于 string 对象的非空字符串(类型为 const char*)由异常对象的 what() 成员返回。传送给out_of_range构造函数的参数是一个简单的消息, 但可以在string对象中包含索引值和数组大小等信息,以易于跟踪问题的源头。
下标运算符函数的const版本和非const版本大致相同:
template <typename T> const T& Array<T>::perator[](size_t index)const{ if(index<0 || index >=size) throw std::out_of_range(index<0?"Negative index":"Index too large"); return elements[index]; }
最后一个需要定义的函数模板是赋值运算符的函数模板,该函数模板需要释放在目标对象中分配的内存,再执行副本构造函数的操作--这当然要在检查对象是不同的之后进行。下面是定义:
template <typename T> Array<T>& Array<T>::operator=(const Array& rhs){ if(&rhs == this) //If lhs == rhs return *this; //just return lhs if(elements) //If lhs array exists delete[]elements; //release the free store memoy size = rhs.size; elements = new T[rhs.size]; for(int i = 0; i< size;i++) eleents[i] =rhs.elements[i]; }
检查左操作数与右操作数是否相同是必不可少的,否则就是释放普通 elements 成员的内存,然后在它己不存在的情况下复制它.如果操作数不问,就释放左操作数占用的内存,之后创建右操作数的副本。
这里编写的所有定义都是函数模板的定义,它们都绑定到类模板上。但它们不是随数定义,而是在需要生成某个类成员的数的代码时由编译器使用的函数模板。因此需要在使用该模板的源文件中可用。为此,一般应将类模板的所有成员函数的定义都放在包含类模板的头文件中。
即使把模板的成员函数定义为独立的函数模板,它们仍可以是内联函数。为了让编译器把它们看作内联实现代码,只需在定义开头的template<>之后加上关键字 inline,如下所示:
template <typename T> inline const T& Array<T>::operator[](size_t index)const { if(index<0 || index >= size) throw out_of_range(index<0?"Negative index":"Index to large"); return elements[index]; }