声明类模板类似于声明函数模板:在声明之前,您必须声明一个或多个模板参数,书中的使用了一个表示栈的示例,例如
template<class T> class Stack;
也可以使用关键字typename声明模板参数,但就我个人建议,对于声明一个类模板,从语义上来说,我建议使用第一种声明,而对于函数模板,即使用typename关键字。这是我的个人使用约定。
template<typename T> class Stack;
OK,直接入正题,通常在头文件中定义类模板,我们以前有说过,在正式的应用场合,我们应该要将类声明(类接口)和类定义(类实现)**的代码分离。类接口文件对应的是C++的头文件,通常是.hh或.hpp为后续的文件名。类实现的文件通常是.cpp文件。
显然类模板的声明语句自然就出现在类接口文件中。因此下面是一个完整的类模板示例,同时它也是类接口.
#include <iostream>
#include <vector>
#include <cassert>
template<typename T>
class Stack{
private:
std::vector<T> elems;
public:
//Stack(Stack const&); //拷贝构造函数
void push(T const& elem);
T pop();
T const& top() const;
bool empty() const{return elems.empty();}
//Stack& operator=(Stack const&); //拷贝赋值运算符
};
备注:这是一个没有实际用途的stack代码,因为没有用封装原生的C代码,而对C++已经封装好的对象无意义的二次封装而已。这里仅仅是为了简化封装和演示类模板而已。
2.1.2类模板成员函数的实现
在类实现的文件中,比如这里的是stack.cpp,我们要导入刚才的类接口文件stack.hh,即如下代码,每实现一个类模板的成员函数,都需要在成员函数前加上template<typename T>或template<class T>,并且需要在成员函数名称和返回类型之间,显式指定成员函数所在的类作用域,例如Stack<T>::
#include "../headers/stack.hh"
template<typename T>
void Stack<T>::push(T const& elem){
elems.push_back(elem);
}
template<typename T>
T Stack<T>::pop(){
assert(!elems.empty());
T elem=elems.back();
elems.pop_back();
return elem;
}
template<typename T>
T const& Stack<T>::top() const{
assert(!elems.empty());
return elems.back();
}
void foo(Stack<int> const& s){
using IntStack=Stack<int>;
Stack<int> istack[10];
IntStack istack2[10];
}
上面的示例代码是一个比较完整且简单的stack实现细节。当我们需要在调用层代码中使用我们刚才定义的代码,和之前函数模板所说的那样,我们要显式告知编译器,Stack模板类中的模板参数T的具体数据类型,下面是完整的调用示例。
//调用代码
int main(){
Stack<int> intStack;
Stack<std::string> stringStack;
intStack.push(7);
std::cout<<intStack.top()<<std::endl;
stringStack.push("Hello,it-dog!!");
std::cout<<stringStack.top()<<std::endl;
stringStack.pop();
foo(intStack);
}
小插曲:可能有人会质疑,有必要将类接口和类实现分开写吗?我发觉大部分网上大部分人的示例代码都是接口和每个成员函数的实现细节都是混写的啊!,你要知道那些示例代码,我自己网上写随笔的时候,也是混写的,但你要清楚那些都是示例代码,要和“生产环境中的代码”严格区分。其实抱着这种想法的,你可能没有经历过数千号甚至数万行代码混写的经历,耦合度过高的代码在重构时会给你带来噩耗般的存在,你到时付出的不仅是不必要的时间。
要点: C++编译器仅针对被调用的模板(成员)函数的相关代码进行实例化(即转换为带明确的参数类型和返回类型的代码)。对于类模板,成员函数只有在使用时才会实例化。当然,这节省了时间和空间,并且只允许部分使用类模板,书本的第2.3节中讨论这一点。 如果类模板具有静态成员,则对于使用该类模板的每种类型,这些成员也会被实例化一次。
2.3类模板的部分实例化
这个英文术语是"Partial template specialization",要理解这个问题,仍然引用书中的实例,首先要强调的是书中是"混写风格",而这里采取类接口和类实现分离的编写风格。
类接口
template <class T>
class Stack
{
....
public:
void printOn(std::ostream& strm);
}
类成员函数实现
template<typename T>
void printOn(std::ostream& strm){
for (T const &elem : elems){
strm << elem << ' ';
}
}
调用
首先上面这个成员函数,仅当你调用它时,编译器会报错。但我们调用其他方法的话,编译是能够正常通过的。
这里的关注焦点是我们在调用层函数中,并未执行存在问题的成员函数的话,也就是说编译器并不会去尝试实例化该成员函数模板,也就是说编译器并未为该模板函数进行任何编译时的语法检测。
类模板部分实例化:就是只有调用者函数中调用到类模板中的成员函数,编译器才会去实例化那些已被调用的类成员函数的模板,那些未被调用的会被编译器视为纯文本"对待"。
2.3.1潜在问题
这里类模板部分实例化就会引发一个潜在的问题,我们如何知道类模板实例化需要哪些操作?类模板部分实例化,它的运行机制本身就默许那些未实例化的类成员函数模板,可能存在运算符和模板参数的兼容性错误。但作者将这个问题抛到第三卷的19章去讨论。所以嘛,这个问题我们先搁置一下。
我们这里暂时回避这个问题,对于上面要打印栈中的内容,最好是实现operator<<函数,但operator<<函数通常被实现为非成员函数
网友评论