[C++ Template]基础--类模板

3 类模板

与函数相似, 类也可以被一种或多种类型参数化。

 

3.1 类模板Stack的实现

template <typename T>
class Stack {
private:
	std::vector<T> elems; // 存储元素的容器
public:
	void push(T const&); // 压入元素
	void pop(); // 弹出元素
	T top() const; // 返回栈顶元素
	bool empty() const { // 返回栈是否为空
		return elems.empty();
	}
};
template <typename T>
void Stack<T>::push(T const& elem)
{
	elems.push_back(elem); // 把elem的拷贝附加到末尾
}
template<typename T>
void Stack<T>::pop()
{
	if (elems.empty()) {
		throw std::out_of_range("Stack<>::pop(): empty stack");
	} 
	elems.pop_back(); //删除最后一个元素
}
template <typename T>
T Stack<T>::top() const
{
	if (elems.empty()) {
		throw std::out_of_range("Stack<>::top(): empty stack");
	} 
	return elems.back(); // 返回最后一个元素的拷贝
}

3.1.1 类模板声明

类模板的声明和函数模板的声明很相似: 在声明之前, 我们先(用一条语句) 声明作为类型参数的标识符; 我们继续使用T作为该标识符:

template <typename T>
class Stack {
	//...
};

在此, 我们可以再次使用关键字class来代替typename:

template <class T>
class Stack {
	//...
};

这个类的类型是Stack<T>, 其中T是模板参数。 因此, 当在声明中需要使用该类的类型时, 你必须使用Stack<T>。例如, 如果你要声明自己实现的拷贝构造函数和赋值运算符, 那么应该这样编写:

template <typename T>
class Stack {
	//...
	Stack(Stack<T> const&); //拷贝构造函数
	Stack<T>& operator= (Stack<T> const&); //赋值运算符
	//...	
};

然而, 当使用类名而不是类的类型时, 就应该只用Stack; 譬如,当你指定类的名称、 类的构造函数、 析构函数时, 就应该使用Stack。
 

3.1.2 成员函数的实现

为了定义类模板的成员函数, 你必须指定该成员函数是一个函数模板, 而且你还需要使用这个类模板的完整类型限定符。 因此, 类型Stack<T>的成员函数push()的实现如下:

template <typename T>
void Stack<T>::push(T const& elem)
{
	elems.push_back(elem); //把传入实参elem的拷贝附加到末端
}

3.2 类模板Stack的使用

为了使用类模板对象, 你必须显式地指定模板实参。 下面的例子展示了如何使用类模板Stack<>:

扫描二维码关注公众号,回复: 4138936 查看本文章
int main()
{
	try {
		Stack<int> intStack; // 元素类型为int的栈
		Stack<std::string> stringStack; // 元素类型为字符串的栈
		// 使用int栈
		intStack.push(7);
		std::cout << intStack.top() << std::endl;
		// 使用string栈
		stringStack.push("hello");
		std::cout << stringStack.top() << std::endl;
		stringStack.pop();
		stringStack.pop();
	} 
	catch(std::exception const& ex) {
		std::cerr << "Exception: " << ex.what() << std::endl;
		return EXIT_FAILURE; // 程序退出, 且带有ERROR标记
	}
}

通过声明类型Stack<int>, 在类模板内部就可以用int实例化T。 因此, intStack是一个创建自 Stack<int>的对象, 它的元素储存于 vector,且类型为 int。 对于所有被调用的成员函数, 都会实例化出基于int类型的函数代码。

注意, 只有那些被调用的成员函数, 才会产生这些函数的实例化代码。 对于类模板, 成员函数只有在被使用的时候才会被实例化。 显然,这样可以节省空间和时间; 另一个好处是: 对于那些“未能提供所有成员函数中所有操作的”类型, 你也可以使用该类型来实例化类模板, 只要对那些“未能提供某些操作的”成员函数, 模板内部不使用就可以。例如,某些类模板中的成员函数会使用operator<来排序元素; 如果不调用这些“使用operator<的”成员函数, 那么对于没有定义operator<的类型,也可以被用来实例化该类模板。另一方面, 如果类模板中含有静态成员, 那么用来实例化的每种类型, 都会实例化这些静态成员。

借助于类型定义, 你可以更方便地使用类模板:

typedef Stack<int> IntStack; 
void foo(IntStack const& s) //s是一个int栈
{
	IntStack istack[10]; //istack是一个含有10个int栈的数组
	//...
}

C++的类型定义只是定义了一个“类型别名”, 并没有定义一个新类型。 因此, 在进行类型定义:

typedef Stack<int> IntStack

之后, IntSatck和Stack<int>实际上是相同的类型, 并可以用于相互赋值。
 

3.3 类模板的特化

你可以用模板实参来特化类模板。 和函数模板的重载类似, 通过特化类模板, 你可以优化基于某种特定类型的实现, 或者克服某种特定类型在实例化类模板时所出现的不足。 另外, 如果要特化一个类模板, 你还要特化该类模板的所有成员函数。 虽然也可以只特化某个成员函数, 但这个做法并没有特化整个类, 也就没有特化整个类模板。

为了特化一个类模板, 你必须在起始处声明一个 template<>, 接下来声明用来特化类模板的类型。 这个类型被用作模板实参, 且必须在类名的后面直接指定

template<>
class Stack<std::string> 
{
    ...
} 
...

进行类模板的特化时, 每个成员函数都必须重新定义为普通函数,原来模板函数中的每个T也相应地被进行特化的类型取代

void Stack<std::string>::push(std::string const& elem)
{
	elems.push_back(elem); //附加传入实参elem的拷贝
}

 

3.4 局部特化

类模板可以被局部特化。 你可以在特定的环境下指定类模板的特定实现, 并且要求某些模板参数仍然必须由用户来定义。 例如类模板:

template <typename T1, typename T2>
class MyClass 
{
	//...
};

就可以有下面几种局部特化:

//局部特化: 两个模板参数具有相同的类型
template <typename T>
class MyClass < T, T > 
{
	...
};
//局部特化: 第2个模板参数的类型是int
template<typename T>
class MyClass < T, int > 
{
	...
};
//局部特化: 两个模板参数都是指针类型。
template<typename T1, typename T2>
class MyClass < T1*, T2* > 
{
	...
};

下面的例子展示各种声明会使用哪个模板:

Myclass<int, float> mif; //使用MyClass<T1,T2>
MyClass<float, float> mff; //使用MyClass<T,T>
MyClass<float, int> mfi; //使用MyClass<T,int>
MyClass<int*, float*> mp; //使用MyClass<T1*,T2*>

如果有多个局部特化同等程度地匹配某个声明, 那么就称该声明具有二义性:

MyClass<int,int> m; //错误:同等程度地匹配MyClass<T,T>
                    // 和MyClass<T,int>
MyClass<int*,int*> m; //错误:同等程度地匹配MyClass<T,T>
                // 和MyClass<T1*,T2*>

为了解决第2种二义性, 你可以另外提供一个指向相同类型指针的特化:

template<typename T>
class MyClass < T*, T* > 
{
	...
};

3.5 缺省模板参数

对于类模板, 你还可以为模板参数定义缺省值; 这些值就被称为缺省模板实参; 而且, 它们还可以引用之前的模板参数。 例如, 在类Stack<>中, 你可以把用于管理元素的容器定义为第2个模板参数, 并且使用std::vector<>作为它的缺省值:
 

template <typename T, typename CONT = std::vector<T> >
class Stack 
{
private:
	CONT elems; // 包含元素的容器
public:
	void push(T const&); // 压入元素
	void pop(); // 弹出元素
	T top() const; // 返回栈顶元素
	bool empty() const
	{ // 返回栈是否为空
		return elems.empty();
	}
};

template <typename T, typename CONT>
void Stack<T, CONT>::push(T const& elem)
{
	elems.push_back(elem); // 把传入实参elem附加到末端
} 
template <typename T, typename CONT>
void Stack<T, CONT>::pop()
{
	if (elems.empty()) {
		throw std::out_of_range("Stack<>::pop(): empty stack");
	} 
	elems.pop_back(); // 删除末端元素
} 
template <typename T, typename CONT>
T Stack<T, CONT>::top() const
{
	if (elems.empty()) {
		throw std::out_of_range("Stack<>::top(): empty stack");
	} 
	return elems.back(); // 返回末端元素的拷贝
}

可以看到: 我们的类模板含有两个模板参数, 因此每个成员函数的定义都必须具有这两个参数。如果你只传递第一个类型实参给这个类模板, 那么将会利用vector来管理stack的元素;另外, 当在程序中声明Stack对象的时候, 你还可以指定容器的类型;

// int栈:
Stack<int> intStack;
// double栈, 它使用std::deque来管理元素
Stack<double,std::deque<double> > dblStack;

3.6 小结

•类模板是具有如下性质的类: 在类的实现中, 可以有一个或多个类型还没有被指定。
•为了使用类模板, 你可以传入某个具体类型作为模板实参; 然后编译器将会基于该类型来实例化类模板。
•对于类模板而言, 只有那些被调用的成员函数才会被实例化。
•你可以用某种特定类型特化类模板。
•你可以用某种特定类型局部特化类模板。
•你可以为类模板的参数定义缺省值, 这些值还可以引用之前的模板参数。

猜你喜欢

转载自blog.csdn.net/u012481976/article/details/84109127