C++ template 模板

1. 关于模板

1.1 基本概念

模板其实是C++泛型编程思想的一种体现。简单来说,泛型编程,意思就是针对广泛类型的编程方式。具体类型可以有不同的实现方式,但是针对广泛类型编程,就能在需要调用时才指定参数类型或者调用类型。

通过把通用逻辑设计为模板,C++得以摆脱了类型的限制,提供了继承机制以外的另一种抽象机制,极大提升了代码的可重用性。

当编译器遇到一个模板定义时,它并不生成代码。只有当我们实例化出模板的一个特定版本时,编译器才会生成代码。

1.2 模板与普通类定义上区别

模板的声明和定义只能在全局,命名空间或类范围内进行。不能在局部,函数内进行,比如不能在main函数中声明和定义一个模板。

通常,当我们调用一个函数时,编译器只需要掌握函数的声明。类似的,当我们使用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现。因此我们将类定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放在源文件中。

然而,模板并不同。为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常即包括声明也包括定义

2. 两种模板

2.1 函数模板

2.1.1 普通函数模板

【格式】

//定义函数模板
//这里class也可以用typename来表示
template<class T1, class T2>返回类型 函数名(参数列表){函数体}
//实例化
函数名(参数表)

【例子】比较函数

//定义
template<typename T> 
T getMin(T t1, T t2){
	return t1 < t2 ? t1 : t2;
}

//实例化
getMin(3, 2); //方法1
getMin<int>(3, 2);//方法2

模板函数的使用与普通非模板函数使用相同,因为模板函数的参数可以从其传入参数中解析出来。当然,如果函数参数没有包含T类变量,则只能用方法2(不过这种情况的模板函数并没有什么意义)。

2.1.2 成员函数模板

【例子】

//定义
class A{
public:
	template<typename T>
	bool templateTest(T t1, T t2);
};

template<typename T>
bool A::templateTest(T t1, T t2){
	return t1 < t2;
}

//实例化
A a;
a.templateTest<int>(2, 1);

2.2 类模板

我们既可以在类模板内部,也可以在类模板外部为其定义成员函数,且定义在类模板内的成员函数被隐式声明为内联函数。

类模板的成员函数本身是一个普通函数,但是类模板的每个实例都有其自己版本的成员函数,因此类模板的成员函数具有和模板相同的模板参数。因而,定义在类模板之外的成员函数就必须以关键字template开始,后接类模板参数列表。

【格式】

//定义模板类
template<class T1, class T2> class 类名{};
//类外定义成员函数
template<class T1, class T2> 函数返回类型 类名<T1, T2>::函数名(函数参数列表){函数体}
//实例化
类名<参数列表> 对象名; 
对象名.类成员变量/成员函数;

【例子】定义栈的模板类

template<class T>
class Stack{
public:
	Stack(int size = 100);
	~Stack();
	bool isEmpty();
	bool isFull();
	void push_back(const T &t);
	T pop_back();
private:
	T* stack;
	int capacity;
	int top;

};

template<class T>
Stack<T>::Stack(int size){
	capacity = size;
	stack = new T[capacity];
	top = -1;
}

template<class T>
Stack<T>::~Stack(){
	top = -1;
	delete[] stack;
}

template<class T>
bool Stack<T>::isEmpty(){
	if(top >= 0) return false;
	else return true;
}

template<class T>
bool Stack<T>::isFull(){
	if(top < capacity - 1)
		return false;
	else return true;
}

template<class T>
void Stack<T>::push_back(const T &t){
	if(isFull()){
		cout << "is full" << endl;
		return;
	}
	stack[++top] = t;
}

template<class T>
T Stack<T>::pop_back(){
	if(isEmpty()){
		cout << "is empty" << endl;
		return 0;
	}
	top = top - 1;
	return stack[top + 1];
}

//实例化
Stack<int> int_stack;
int_stack.push_back(1);

3. 模板特化/专门化

有时通用的函数模板不能解决个别类型的问题,因此,我们必须对其进行定制,这就是模板特化。模板特化必须把所有的模版参数全部指定。

模板特化分为两种:全特化和偏特化。全特化是所有的模板参数都被进行特化,偏特化则是局部参数的特化。

3.1 全特化

3.1.1 模板函数的特化

前面,我们定义了getMin函数模板。对于int、float、char等类型,它都可以很好的工作,但是对于char *却没有办法,因此我们可以针对其进行特化。

template<> 
const char * getMin<char *>(const char *t1, const char *t2){
	return (strcmp(t1,t2) < 0) ? t1 : t2;
}
3.1.2 模板类的特化

前面,我们定义了Stack类模板。我们可以用int、char、bool去实例化它。但是,因为bool类型只需要一个二进制位就可以对其进行存储,使用一个字或者一个字节都是浪费存储空间的,因此,我们可以对它进行特化。

template <>
class stack<bool> {
//...
};

3.2 偏特化

3.2.1 函数模板的偏特化

严格的来说,函数模板并不支持偏特化,但由于可以对函数进行重载,所以可以达到类似于类模板偏特化的效果。比如,我们有如下函数:

template <class T> void f(T);  (a)

根据重载规则,对(a)进行重载。

template < class T> void f(T*);  (b)

如果将(a)称为基模板,那么(b)称为对基模板(a)的重载,而非对(a)的偏特化。

3.2.2 类模板的偏特化

c++标准库中,包含了类vector的定义。我们可以将第一个参数被绑定到bool类型,而另一个参数仍未绑定需要由用户指定。

template <class T, class Allocator>
class vector { 
    // … // 
};

template <class Allocator>
class vector<bool, Allocator> { 
    //…//
};

4. 容器适配器

点击链接,感觉这篇博文把容器适配器讲得挺清楚的。

5. 模板与虚函数

5.1 模板类中使用虚函数

#include <iostream>

using namespace std;

template<class T>
class A{
public:
	virtual void f1();
	virtual void f2() = 0;
};

template<class T> void A<T>::f1(){
	cout << "A::f1()" << endl;
}

template<class T>
class B : public A<T>{
public:
	void f1();
	void f2();
};

template<class T> void B<T>::f1(){
	cout << "B::f1()" << endl;
}

template<class T> void B<T>::f2(){
	cout << "B::f2()" << endl;
}

int main(){
	B<int>* p2 = new B<int>; p2->f1();
	A<int>* p3 = new B<int>; p3->f1();	
}

输出结果:

B::f1()
B::f1()

非模板类怎么用虚函数,模板类就怎么用。模板是不影响类的多态的。但是需要注意的是:A<int>和A<char>是两个不完全相同的类,因此A<int>不是B<char>的基类。

5.2 模板成员函数不可以是虚函数

模板定义本身不参与编译,而是编译器根据模板的用户使用模板时提供的类型参数生成代码,再进行编译,这一过程被称为模板实例化。用户提供不同的类型参数,就会实例化出不同的代码。

因为每个包含虚函数的类具有一个virtual table(vtable),包含该类的所有虚函数的地址,因此vtable的大小是确定的。成员函数模板只有被使用时才会被实例化,将其声明为虚函数会使vtable的大小不确定。所以,成员函数模板不能为虚函数。(参考

6. 模板类型转换

假设我们定义了Shape和Circle类,代码如下:

//shape.h
class Shape {

};

class Circle : public Shape {
};

我们希望可以这么用:

#include <stdio.h>
#include "stack.h"
#include "shape.h"
int main() {
    Stack<Circle> pcircleStack;
    Stack<Shape> pshapeStack;
    pcircleStack.push(new Circle);
    pshapeStack = pcircleStack;
    return 0;
}

然而,这样是无法编译的,因为Stack<Shape*>并不是Stack<Circle*>的父类,然而我们却希望代码可以这么工作,那我们就要定义转换运算符了,Stack代码如下:

template <class T> 
class Stack {
public:
    Stack(int size = 100);
    ~Stack();
    bool isEmpty();
    bool isFull();
    void push_back(const T &t);
    T pop_back();
    template<class T2>  operator Stack<T2>();
private:
    T* stack;
    int capacity;
    int top;
};

//这里只写了转换运算符函数
template <class T> template <class T2>  Stack<T>::operator Stack<T2>() {
    Stack<T2> StackT2;
    for (int i = 0; i < top; i++) {
        StackT2.push_back((T2)m_pT[i]);
    }
    return StackT2;
}

这样,Stack 或者Stack<Circle*>就可以自动转换为Stack<Shape>或者Stack<Shape*>。不过,如果转换的类型是Stack<int>到Stack<Shape>,编译器会报错。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值