模板的简单理解

模板

说到模板,就先要提到函数重载,函数重载就是为了重新编写函数以便实现函数的其余功能,但是使用函数重载就必须针对所需想同行为的不同类型重新实现他。

以一个加法函数为例:

int Add(int left, int right)
{
	return left + right;
}

float Add(float left, float right)
{
	return left + right;
}

只要有新的类型出现,那么就必须去重新去重载,添加对应的函数;而且代码的类型都差不多,这样就增加了代码的复杂程度,复用率太低,而且最关键的是,函数重载不能解决函数返回值或返回类型不一样的情况,所以我们需要去采用别的方法。

不用函数重载,那么大家可能会想到去用继承的方式,将需要的主方法写在基类里面,然后在需要使用的时候继承基类,那么就可以使用了,但是这也有一个缺点就是你想要维护代码的时候,你必须要去查找基类,那么难度就会增加很多,所以这种方法也不是最好的。

当然,如果你去使用预处理的话这就更不适合了,你根本没有办法在编译期间查找出错误,如果错了你根本不知道错在哪里,所以,在这里我介绍一下模板。

模板抽象了具体的型别,实现了共性逻辑,为一类问题提供了统一的泛型接口,模板机制使得编程者在定义类和函数时能以类型作为参数,并且模板只依赖于实际使用时的传入参数,不关心能被用作参数的那些不同类型之间的任何联系。模板有函数模板和类模板,下面分别作介绍。

函数模板

函数模板代表了一个函数家族,该函数与类型无关,在使用时被参数化,根据实参的类型产生具体的函数特定类型版本。

模板函数的关键字为template,格式为

template<typename T1, typename T2,......,class Tn>

返回值类型    (函数名)参数列表

{...}

其中template是定义模板关键字,而typename则是定义模板形参名字的关键字,和class一样可以定义任意的参数名,注意,定义参数名的关键字只有这两个,不可以用别的进行替代

要注意到模板只是一个样本,他不是类或者函数,编译器会通过模板产生特定的类或者是函数的特定类型版本,产生模板特定类型的过程被称为模板的实例化。

以代码为例:

template <typename T>
T Add(T left, T right)
{
	return left + right;
}

int main()
{
	cout << Add(10, 20) << endl;
	cout << Add(2.1, 3.6) << endl;
	return 0;
}
在这里面,第一个Add(10,20)构成的是类似于int Add(int left,int right){return right+left;}这样的函数,而第二个则是double Add(double left,double right){return right+left;}这样的函数,这就是函数模板的实例化。但是,这还有其余的实例化方式:

template <typename T>
T Add(T left, T right)
{
	return left + right;
}

int main()
{
	cout << Add(10, 20) << endl;
	cout << Add(2.1, 3.6) << endl;
	cout << Add<int>(2.3, 1.3) << endl;
	cout << Add(10, (int)2.3) << endl;
	return 0;
}
在这其中,第三种调用的方式调用模板形成的也是int Add(int left,int right){return right+left;}这样的函数,而第四种则是将数据进行强转,传的得时同一种数据类型。

值得注意的是在进行实例化之前,模板是被编译了两次的:在实例化之前先检查模板代码本身是否有错;在实例化期间再检查模板的代码,看所有调用是否有效。

模板参数

函数模板有两种参数,分别是类型形参和非类型形参。


在使用模板参数的时候,有一些地方是需要注意的:

①注意名字屏蔽规则,不要使用已经被定义过的名字,另外参数名字只能在模板形参之后到模板定义的末尾之间;

②形参中的名字不能重复使用,一个名字在形参列表中只能出现一次;

③在形参名字前面必须加上class或者是typename关键字进行修饰;

④模板的定义只能放在开头,不能放在模板内部;

⑤模板的形参列表不能为空(但是要除去特化的情况);

当然,作为模板函数,它毕竟还是一个函数,当然可以进行函数重载,只是,你需要注意的是:函数所有的重载的声明都应该被放在该函数被调用的位置之前

在这里,可能有人会想到,既然模板函数可以重载,那么同名的非模板函数和模板函数会构成重载么?那么我就需要问一下构成重载的条件是什么?

①在同一个作用域;②函数名相同;③参数列表不同。只要满足这三个条件,那么就可以构成重载,那么非模板函数和模板函数自然就可以构成重载了。不仅可以构成重载,这个函数模板还可以被实例化为这个非模板函数。既然已经构成重载了,那么在调用的时候会先调用哪一个函数呢?

还是用代码作为例子吧:


在输出函数这打一个断点,当函数进入下一步的时候,函数会调用哪一个函数?


可以看到会先去调用这个非模板函数,这就说明在条件相同的情况下,调用时会先去调用非模板函数而不是去调用模板生成一个实例。但是如果模板可以产生一个更好匹配的函数,那么会优先调用模板。

这是不是就是说模板很好用,没有什么缺点呢?

当然不是,在有些时候并不能写出很满足可能被实例化的类型的合适的模板,甚至某些情况下还会发生错误。下面就举个例子:

template <typename T>
int compare(T p1, T p2)
{
	if (p1 < p2)
		return -1;
	if (p1 > p2)
		return 1;
	return 0;
}

int main()
{
	char *pStr1 = "abcd";
	char *pStr2 = "1234";
	cout << compare(pStr1, pStr2) << endl;
	return 0;
}
正常情况下,这里我们需要让它返回的应该是1,但是结果却是


这是为什么呢?

其实道理很简单,在调用函数的时候,直接将两个指针变量的地址传递给模板参数,在比较时,比较的只是指针的大小,并没有比较指针的内容。

所以,我在这里就引入了模板特化的概念。

模板特化

我们可以这样定义模板

template<>
int compare<const char*>(const char* const p1, const char* const p2)
{
	return strcmp(p1, p2);
}
这里就是模板的特化,在某种意义上与函数重载有点类似。具体的就是如下:

template <typename T>
int compare(T p1, T p2)
{
	if (p1 < p2)
		return -1;
	if (p1 > p2)
		return 1;
	return 0;
}

template<>
int compare<const char*>(const char* const p1, const char* const p2)
{
	return strcmp(p1, p2);
}

int main()
{
	const char* pStr1 = "abcd";
	const char* pStr2 = "1234";
	cout << compare(pStr1, pStr2) << endl;
	return 0;
}
这样的结果就会如我们所想的为1么,我们来看一下


这样就得到了我们想要的结果了。

模板特化使用的方式很简单,在template后面接上<>,再接上模板名和<>,尖括号中指定这个特化定义的模板形参,然后就是()和括号中的形参列表,最后就是函数体。

至于使用模板特化,你就不得不注意到它的使用条件:

①模板特化必须与特定的模板相匹配;如果你没写模板或是名字不相同,又或是参数列表不一样,那么就会报错;

②模板特化中在模板名后面的<>中一定不能漏掉这个模板形参,如果漏了就只相当于定义了一个普通参数而已。

③这一点很重要,如果你模板和特化都已经写好且没有错误,那么在调用的时候,实参类型必须与特化后的模板的形参类型完全匹配,否则编译器将从模板中重新实例化一个实例。代码作证:

template <typename T>
int compare(T p1, T p2)
{
	if (p1 < p2)
		return -1;
	if (p1 > p2)
		return 1;
	return 0;
}

template<>
int compare<const char*>(const char* const p1, const char* const p2)
{
	return strcmp(p1, p2);
}

int main()
{
	const char* pStr1 = "abcd";
	const char* pStr2 = "1234";
	char* pStr3 = "abcd";
	char* pStr4 = "1234";
	cout << compare(pStr1, pStr2) << endl;
	cout << compare(pStr3, pStr4) << endl;
	return 0;
}
在这里面,结果显示的是多少呢,大家可以想看看。


就是一个1,一个-1。


打上一个断点,当代码运行到这里的时候,他下一步会进入哪一个?


现在代码进入了模板特化的部分,在实参与形参匹配的情况下,特化实现了;但是当实参形参不匹配的时候,代码会进入哪里呢?


下一步会到哪里呢?


代码现在进入了模板的部分,说明在这里编译器通过模板生成了一个char*类型的函数。

这就充分的证实了我刚才说的实参形参需要完全匹配的情况。

模板类

上面介绍了模板函数,那么这里就得提到模板类了,模板类也还是模板,还是需要以关键字template开头,后接模板形参表。

基本格式就是

template<class 形参名1,形参名2,形参名3,......,形参名n

class 类名{...};

就以一个普通顺序表为例,通常我们是这样定义一个顺序表:

#define DataType int

class SeqList
{
private:
	DataType* _data;
	int _size;
	int _capacity;
};
但是,通过模板,我们就可以这样写:

template<typename T>
class SeqList
{
private:
	T* _data;
	int _size;
	int _capacity;
};
在这里,我们使用动态顺序表作为例子进行分析
template<typename T>
class SeqList
{
public:
	SeqList();
	~SeqList();
private:
	int _size;
	int _capacity;
	T* _data;
};

template <typename T>
SeqList <T>::SeqList()
: _size(0)
, _capacity(10)
, _data(new T[_capacity])
{}

template <typename T>
SeqList <T>::~SeqList()
{
	delete[] _data;
}

void test1()
{
	SeqList<int> sl1;
	SeqList<double> sl2;
}

int main()
{
	test1();
	return 0;
}
void test1()
{
	SeqList<int> sl1;
	SeqList<double> sl2;
}
test1这一部分去调用模板,int和double会分别由编译器进行模板推演,然后生成

class SeqList
{
private:
	int _size;
	int _capacity;
	int* _data;
};

class SeqList
{
private:
	int _size;
	int _capacity;
	double* _data;
};

编译器会重新编写SeqList类,最后创建名为SeqList<int>和SeqList<double>的类。

另外,在这里我需要提醒一下,模板类的类型不是SeqList,而是SeqList<T>,这一点很重要,务必记住,否则那些函数将不再是模板的类的成员函数。

模板参数

先来看一下这部分的代码:

template <typename T>
class SeqList
{
private:
	int _size;
	int _capacity;
	T* _data;
};

template <class T, class C = SeqList<T>>
class Stack
{
public:
	void Push(const T& x);
	void Pop();
	const T& Top();
	bool Empty();
private:
	C _con;
};

void Test()
{
	Stack<int> s1;
	Stack<int, SeqList<int>> s2;
}

int main()
{
	Test();
	return 0;
}
里面的模板形参带上了缺省值,在调用的时候既可以用<int>,也可以用<SeqList<int>>去调用。这就表示这样使用是可以的。

模板的模板参数

这个和一般的区别在于哪里呢?看一下代码:

template <typename T>
class SeqList
{
private:
	int _size;
	int _capacity;
	T* _data;
};

template <class T, template<class> class C = SeqList> 
class Stack
{
public:
	void Push(const T& x);
	void Pop();
	const T& Top();
	bool Empty();
private:
	C<T> _con;
};

void Test()
{
	Stack<int> s1;
	Stack<int, SeqList> s2;
}

int main()
{
	Test();
	return 0;
}
关键就在于里面标红的地方,template<class>就表示C是一个模板类类型的模板形参,它所定义的成员也是一个模板类类型。

非类型的模板参数

之前就说过模板的参数被分为类型与非类型,那么这同样会有非类型的类模板参数,还是以代码为例:

template <typename T, size_t MAX_SIZE = 10> 
class SeqList
{
public:
	SeqList();
private:
	T _array[MAX_SIZE];
	int _size;
};

template <typename T, size_t MAX_SIZE>
SeqList <T, MAX_SIZE>::SeqList()
: _size(0)
{}

void Test()
{
	SeqList<int> s1;
	SeqList<int, 20> s2;
}

int main()
{
	Test();
	return 0;
}
带上缺省的模板参数,这样就是一种典型的非类型形参的模板参数。注意:浮点数和类对象是不允许作为非类型模板参数的。

类模板的特化

既然模板函数有特化,那么模板类也会有特化,特化的基本定义是没有什么变化的,但是,和模板函数唯一不同的区别就在于,类模板的特化分为全特化和局部特化。

全特化

全特化和模板函数的特化的区别不大,,还是一样的定义方式和使用方法。但是,需要提醒的是, 特化后定义成员函数不再需要模板形参。

局部特化


以代码为例:

template <typename T1, typename T2>
class Data
{
public:
	Data();
private:
	T1 _d1;
	T2 _d2;
};

template <typename T1, typename T2>
Data<T1, T2>::Data()
{
	cout << "Data<T1, T2>" << endl;
}

template <typename T1>
class Data <T1, int>
{
public:
	Data();
private:
	T1 _d1;
	int _d2;
};

template <typename T1>
Data<T1, int>::Data()
{
	cout << "Data<T1, int>" << endl;
}
这里就是在局部特化第二个参数;当然,局部特化当然不只是说特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
	Data();
private:
	T1 _d1;
	T2 _d2;
	T1* _d3;
	T2* _d4;
};

template <typename T1, typename T2>
Data<T1 *, T2*>::Data()
{
	cout << "Data<T1*, T2*>" << endl;
}
这里就是局部特化两个参数为指针,当然,局部特化引用也是可以的。

需要注意的是:模板的全特化和偏特化都是在已定义的模板基础之上,不能单独存在


注:所有代码都是放在VS2013编译器下运行的。





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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值