一,基础定义
类模板是用来生成类的蓝图,是一种创建类的方式,同一套类模板可以生成很多种不同的类。
编译器基于类模板生成的每个类被称为类模板的实例。
第一次使用模板类型声明变量时,会创建类模板的一个实例, 以后定义同类型的变量时,会使用已经创建的第一个实例。
类模板有许多应用,最常见的应用是定义容器类。
类模板和类一样,可以有友元,其友元可以是类,函数或者其他模板。
如果一个派生类继承自该类模板,那么这个派生类也必须是模板。
类模板的代码样式:
template <parameter list>
class ClassName
{
//class definition ...
}
类型模板参数 & 非类型模板参数图示:
代码样例:用类模板实现的Array<T>
template <typename T>
class Array
{
private:
T* elements;
size_t size;
public:
explicit Array<T>(size_t arraySize); //构造函数
Array<T>(const Array<T>& array); //拷贝构造函数
~Array<T>(); //析构函数
T& operator[](size_t index); //下标运算符
Array<T>& operator=(const Array<T>& rhs); //赋值运算符
size_t getSize() const {return size;}
};
在类模板的内部,可以直接使用类模板名称,不需要显式地带模板参数,因此,在类模板的内部,Array和Array<T>等价。
以上代码可以简化为:
template <typename T>
class Array
{
private:
T* elements;
size_t size;
public:
explicit Array(size_t arraySize);
Array(const Array& array);
~Array();
T& operator[](size_t index);
Array& operator=(const Array& rhs);
size_t getSize() const {return size; }
};
类模板参数指定默认值的方式和函数参数一样。
默认值在类模板的声明中指定即可,不需要在成员函数模板中指定默认值。
定义类模板的时候也可以这样写:
template <typename T=string>
class Array
{
//code
}
使用默认值来实例化类模板,可以这样写:
Array<> myArray;
二,类模板的成员函数
在类模板的模板体中定义的成员函数,与普通的类一样,成员函数可以看作是所有模板实例的内联函数。
但是在模板体的外部定义的成员函数,语法与普通的类不同,需要将成员函数定义为函数模板。
由于成员函数的函数模板与它们的类模板绑定在一起,所以函数模板使用的参数列表必须与类模板的参数列表完全相同。
1.构造函数模板:
template <typename T>
Array<T>::Array(size_t arraySize):
elements{new T[arraySize]}, size{arraySize}
{
}
2.拷贝构造函数模板:
假定赋值运算符可以用于T类型的变量。
template <typename T>
Array<T>::Array(const Array& array): Array{array.size}
{
for (size_t i {}; i < size; ++i)
{
elements[i] = array.elements[i];
}
}
3.析构函数模板:
释放给数组分配的堆内存。
template <typename T>
Array<T>::~Array()
{
delete[] elements;
}
4.下标运算符模板:
template <typename T>
T& Array<T>::operator[](size_t index)
{
if (index >= size)
{
throw std::out_of_range {"Index too large: " + std::to_string(index)};
}
return elements[index];
}
5.赋值运算符模板:
template <typename T>
Array<T>& Array<T>::operator=(const Array& rhs)
{
if (&rhs != this)
{
delete[] elements;
size = rhs.size;
elements = new T[size]; //may throw std::bad_alloc
for(size_t i {}; i < size; ++i)
{
elements[i] = rhs.elements[i];
}
}
return *this;
}
由类模板创建模板实例时,并不会把所有的成员函数的函数模板都拿去生成模板实例,只有被代码用到的成员函数才会被生成模板实例,例如,由类模板生成某个类时,这个类只进行了创建对象的操作,只有构造函数和析构函数的函数模板会生成模板实例,其他暂时没用到的函数模板,比如拷贝构造函数模板,则不会生成模板实例。简单讲就是,当实例化一个类模板时,它的成员函数对应的函数模板只有在使用时才会被实例化。
声明指向对象的指针并不会创建类模板的实例:
Array<std::string>* obj_ptr;
//声明了一个指针,不会创建类模板的实例
Array<std::string*> str_obj {10};
//定义了一个对象,会创建类模板的实例,同时还会生成构造函数的函数模板实例
三,非类型模板参数
非类型参数是指模板定义中,带有指定类型的参数。
非类型参数的主要用途是指定容器的大小和上下限。
代码样例如下:
template <typename T, size_t size> //size: 非类型参数
class ClassName
{
//code
};
注意:类型参数T必须放在非类型参数size的前面。
非类型模板参数还可以在定义的时候给一个初始值,例如:
template <typename T, size_t size = 10>
class ClassName
{
//code
};
非类型参数支持的数据类型:
整数类型(例如size_t、long)
枚举类型
对象的指针or引用类型
函数的指针or引用类型
非类型参数不支持浮点类型或类类型。
从C++17开始,也可以指定auto,auto& 和 auto* 等作为非类型参数,编译器会自动推导出类型。
代码样例:
a.带有非类型参数的Array类模板:
template <typename T, int startIndex>
class Array
{
private:
T* elements;
size_t size;
public:
explicit Array(size_t arraySize);
Array(const Array& array);
~Array();
T& operator[](int index);
Array& operator=(const Array& rhs);
size_t getSize() const { return size; }
}
b.带有非类型参数的成员函数模板
1.构造函数模板:
template <typename T, int startIndex>
Array<T, startIndex>::Array(size_t arraySize):
elements{new T[arraySize]}, size{arraySize}
{
}
2.拷贝构造函数模板:
template <typename T, int startIndex>
Array<T, startIndex>::Array(const Array& array): Array{array.size}
{
for (size_t i {}; i < size; ++i)
{
elements[i] = array.elements[i];
}
}
3.析构函数模板:
template <typename T, int startIndex>
Array<T, startIndex>::~Array()
{
delete[] elements;
}
4.下标运算符模板:
template <typename T, int startIndex>
T& Array<T, startIndex>::operator[](int index)
{
return const_cast<T&>(std::as_const(*this)[index]);
}
5.赋值运算符模板:
template <typename T, int startIndex>
Array<T, startIndex>& Array<T, startIndex>::operator=(const Array& rhs)
{
if (&rhs != this)
{
delete[] elements;
size = rhs.size;
elements = new T[size]; //may throw std::bad_alloc
for(size_t i {}; i < size; ++i)
{
elements[i] = rhs.elements[i];
}
}
return *this;
}
非类型模板参数在使用时需要注意,给非类型参数传不同的实参,将生成不同的模板实例。
代码样例:
以下代码将创建两个不同的模板实例
Array<double, 0> obj1{10};
Array<double, 1> obj2{10};
四,类模板的特例
和函数模板一样,类模板也有特例,被称为类模板的具体化。
当有些模板参数只适用于特定的数据类型,比如可以使用string类型实例化模板,但使用char*类型实例化模板时会报错,此时需要定义类模板的特例。
代码样例:
template <>
class Array<const char*>
{
};
类模板特例的定义必须在原始类模板的定义之后。
五,参考阅读
《C++17入门经典》
《C++ Primer》