模板的使用大全
概述
在C++中,有两种特别重要的编程思想。一种是我们熟知的面向对象编程,另一种是泛型编程。所谓泛型编程,就是以一种不依赖任何特定数据类型的方式编写代码。在C++ STL标准库中,有许多泛型编程的例子,像vector、list、map等,都用到了泛型编程。模板是泛型编程的基础,它使用参数化的类型来创建函数和类,分别对应函数模板和类模板。通过模板,可以实现数据类型的多态化,可以编写支持多种数据类型的函数和类,大大提高了代码的复用性。
函数模板
1、函数模板的定义如下:
template<typename T1, typename T2, ...>
返回类型 函数名(参数列表)
{
函数体
}
其中,template是关键字,用于声明模板。尖括号<>中的参数是模板的形参,可以有一个,也可以有多个。typename是关键字,用于表示后面的符号是一种数据类型。也可以用class代替typename,但为了与类申明时的class区别,推荐使用typename。T1和T2为通用的数据类型,名称可以更改,一般推荐全大写。在下面的示例代码中,我们给出了一个函数模板的声明。
template<typename TYPE>
TYPE Add(TYPE a, TYPE b)
{return a + b;
}
2、函数模板中形参除了可以是typename表示的类型形参,还可以是非类型形参。非类型形参,是指该参数不是一个通用类型,而是一个固定类型的常量。对应的,传入实参时,必须是编译时就能确定的常量或常量表达式。可参考下面的示例代码。
template<typename TYPE, int MAX_SIZE>
void PrintArray(TYPE pData[MAX_SIZE])
{for (int i = 0; i < MAX_SIZE; i++){std::cout << pData[i] << std::endl;}
}int main()
{int pData1[] = { 1, 2, 3 };PrintArray<int, sizeof(pData1)/sizeof(pData1[0])>(pData1);return 0;
}
非类型形参虽然是一个固定类型的常量,但并不是任何固定类型的常量都能作为非类型形参。一般情况下,只有char、short、long、unsigned int、bool等可转换为int类型的常量可以作为非类型形参,float、double、类、字符串等类型的常量不可以作为非类型形参。在下面的示例代码中,使用了字符串类型作为非类型形参,此时编译会报错,提示:error C2762: 'Print': invalid expression as a template argument for 'CUSTOM_TEXT'。
template<typename TYPE, const char *CUSTOM_TEXT>
void Print(TYPE data)
{std::cout << data << "," << CUSTOM_TEXT << std::endl;
}int main()
{Print<int, "hello">(2); // 编译出错return 0;
}
3、函数模板不是函数,只是一个用来实例化具体函数的模板。因为模板中的类型是通用的,编译器并不知道需要占用的栈大小等信息,因此无法生成具体函数。也就是说,声明了函数模板,而不去使用它,编译器不会为该函数模板生成任何代码。只有当使用函数模板,用具体的数据类型替代类型形参时,才会生成具体函数。
4、使用函数模板时,有两种方式:一种是自动推导,此时直接传入实参即可;另一种是显式指定数据类型,在调用函数的括号前面添加:<数据类型>。
template<typename TYPE>
TYPE Add(TYPE a, TYPE b)
{return a + b;
}int main()
{Add(66, 88); // 自动推导Add<int>(66, 88); // 显式指定数据类型return 0;
}
自动推导时,不会发生隐式类型转换;显式指定数据类型时,允许发生隐式类型转换。可参看下面的示例代码。
int main()
{int a = 66;char b = 88;Add(a, b); // 编译错误Add<int>(a, b); // 编译正常return 0;
}
在上面的代码中,调用Add(a, b)会发生编译错误,因为此时属于自动推导,不会将char隐式类型转换为int,从而导致与声明模板时的类型不一致。调用Add<int>(a, b)时,属于显式指定数据类型,会自动将char隐式类型转换为int,这样与声明模板时的类型一致,编译便没有问题。
5、在全局作用范围下,如果有变量、对象或类型与函数模板中的类型形参同名,则该全局变量、对象或类型会被覆盖,不其作用。
int TYPE = 99;template<typename TYPE>
TYPE Add(TYPE a, TYPE b)
{return a + b;
}int main()
{std::cout << Add(66, 88) << std::endl; // 正常输出154return 0;
}
但如果在函数模板内部,有变量、对象或类型与类型形参同名,则会发生编译错误。
template<typename TYPE>
TYPE Add(TYPE a, TYPE b)
{// 提示编译错误:'TYPE': template parameter name cannot be redeclaredint TYPE = 99;return a + b;
}int main()
{std::cout << Add(66, 88) << std::endl;return 0;
}
6、多个函数模板之间、函数模板与普通函数之间,均可以发生重载。调用时,具体使用哪个重载函数,有两条规则:一是都匹配的话,优先使用普通函数,如果此时需要强制使用函数模板,则可以显式指定数据类型,或者指定空的数据类型;二是函数模板可以更好匹配的话,优先使用函数模板。
int Echo(int a)
{printf("echo from normal\\n");return a;
}template<typename TYPE>
TYPE Echo(TYPE a)
{printf("echo from template 1\\n");return a;
}template<typename TYPE>
TYPE Echo(TYPE a, TYPE b)
{printf("echo from template 2\\n");return a;
}int main()
{Echo(66);Echo<int>(66);Echo<>(66);Echo(66, 88);char a = 99;Echo(a);return 0;
}
上面示例代码的输出如下:
echo from normal
echo from template 1
echo from template 1
echo from template 2
echo from template 1
Echo(66)时,普通函数和函数模板1均匹配,优先使用普通函数。
Echo<int>(66)和Echo<>(66)时,强制使用了函数模板1。
Echo(66, 88)时,只能匹配到函数模板2。
Echo(a)时,匹配到普通函数需要进行隐式类型转换,而匹配到函数模板1不需要隐式类型转换,故优先使用函数模板1。
7、有时候在使用函数模板时,会发现传入的数据类型并不支持指定的操作和运算。在下面的示例代码中,我们希望对base1和base2进行相加操作,但我们并没有重载CBase的相加运算符。此时,编译器会报错,提示"error C2676: binary '+': 'TYPE' does not define this operator or a conversion to a type acceptable to the predefined operator"。
class CBase
{
public:CBase(int nData) : m_nData(nData){NULL;}int GetData() const{return m_nData;}private:int m_nData;
};template<typename TYPE>
TYPE Add(TYPE a, TYPE b)
{return a + b;
}int main()
{CBase base1(66);CBase base2(88);Add(base1, base2);return 0;
}
解决该问题的其中一种方法是:对Cbase类型提供一个特殊的函数模板,也叫做函数模板的特化。当我们声明一个特化的函数模板时,其参数类型、返回值类型必须与之前声明的函数模板中对应的类型保持一致。可参见下面的示例代码。
class CBase
{
public:CBase(int nData) : m_nData(nData){NULL;}int GetData() const{return m_nData;}private:int m_nData;
};template<typename TYPE>
TYPE Add(TYPE a, TYPE b)
{return a + b;
}// 函数模板的特化
template<>
CBase Add(CBase a, CBase b)
{CBase c(a.GetData() + b.GetData());return c;
}int main()
{CBase base1(66);CBase base2(88);CBase base = Add(base1, base2);std::cout << base.GetData() << std::endl;return 0;
}
8、当我们需要使用类型形参内部的数据类型时,编译器并不知道具体代表的是数据类型还是成员变量。在下面的示例代码中,我们在Test函数模板中声明了一个TYPE::NEW_INT类型的指针pData,但编译器会报错,提示"error C3861: 'pData': identifier not found"。
class CBase
{
public:CBase() : m_nData(66){NULL;}typedef int NEW_INT;int m_nData;
};template<typename TYPE>
void Test(TYPE a)
{TYPE::NEW_INT *pData = &a.m_nData;*pData = 88;std::cout << *pData << std::endl;
}int main()
{CBase base;Test(base);return 0;
}
解决该问题的方法是:在TYPE::NEW_INT前添加typename关键字,显式告诉编译器,这里声明的是一个数据类型。可参看下面的示例代码。
typename TYPE::NEW_INT *pData = &a.m_nData;
*pData = 88;
9、一般情况下,建议将函数模板的声明和实现均写在.h头文件中,否则,编译时会出现类似下面的错误信息:
error LNK2019: unresolved external symbol "int __cdecl Add<int>(int,int)" (??$Add@H@@YAHHH@Z) referenced in function _main
为了解决该问题,一般将.cpp文件改名为.hpp文件,并在调用处引用.hpp文件,而不是.h文件。
#pragma oncetemplate<typename TYPE>
TYPE Add(TYPE a, TYPE b);
上面为Test.h文件,下面为Test.hpp文件。
#include "Test.h"template<typename TYPE>
TYPE Add(TYPE a, TYPE b)
{return a + b;
}
在主函数中调用时的代码如下。
#include <iostream>
#include "Test.hpp"int main()
{std::cout << Add(66, 88) << std::endl;return 0;
}
类模板
1、类模板的定义如下:
template<typename T1, typename T2, ...>
class 类名
{
}
template等关键字在上面的函数模板中已经介绍过了,这里不再赘述。在下面的示例代码中,我们给出了一个类模板的声明。
template<typename TYPE>
class CBase
{
public:CBase(TYPE data) : m_data(data){NULL;}TYPE GetData() const{return m_data;}private:TYPE m_data;
};
2、类模板与函数模板有许多类似的地方,比如:类模板也支持非类型形参;类模板也不是类,只是一个用来实例化具体类的模板。对于这些类似的地方,可参考函数模板进行理解,这里就不再赘述了。
3、使用类模板时,不支持自动推导,必须显式指定数据类型。
int main()
{CBase base(66); // 不支持自动推导,编译出错CBase<int> base(66); // 显式指定数据类型,编译正常std::cout << base.GetData() << std::endl;return 0;
}
4、从模板类派生时,分为两种情况:一种是派生类不是模板类,此时需要指定基类的具体类型;另一种是派生类也是模板类,此时可以指定基类的具体类型,也可以用派生类的类型作为基类的类型。如果作为基类的模板类提供了自定义的构造函数,则需要在派生类构造函数的初始化列表中初始化基类对象。
// 派生类不是模板类
class CDerived1 : public CBase<int>
{
public:CDerived1() : CBase(88){NULL;}
};// 派生类是模板类
template<typename TYPE>
class CDerived2 : public CBase<TYPE>
{
public:CDerived2(TYPE data, TYPE data2) : CBase<TYPE>(data), m_data2(data2){NULL;}TYPE GetData2() const{return m_data2;}private:TYPE m_data2;
};CDerived1 derived1;
// 输出:88
std::cout << derived1.GetData() << std::endl;CDerived2<std::string> derived2("Hello", "CSDN");
// 输出:Hello,CSDN
std::cout << derived2.GetData() << "," << derived2.GetData2() << std::endl;
5、类模板中有模板友元函数时,如果声明和定义在不同的文件中,需要通过前置声明解决二次编译问题。可参看下面的示例代码。
#pragma once#include <iostream>// 前置声明CBase
template<typename TYPE>
class CBase;// 前置声明operator <<
template<typename TYPE>
std::ostream &operator <<(std::ostream &out, CBase<TYPE> &base);template<typename TYPE>
class CBase
{friend std::ostream &operator << <TYPE>(std::ostream &out, CBase<TYPE> &base);
public:CBase(TYPE data);TYPE GetData() const;private:TYPE m_data;
};
上面为Base.h文件,下面为Base.hpp文件。
#include "Base.h"template<typename TYPE>
std::ostream &operator <<(std::ostream &out, CBase<TYPE> &base)
{out << base.GetData();return out;
}template<typename TYPE>
CBase<TYPE>::CBase(TYPE data) : m_data(data)
{NULL;
}template<typename TYPE>
TYPE CBase<TYPE>::GetData() const
{return m_data;
}
在主函数中调用时的代码如下。
#include <string>
#include <iostream>
#include "Base.hpp"int main()
{CBase<std::string> base("CSDN");std::cout << base << std::endl;return 0;
}
6、类模板中有静态变量时,对于每一个具体类型,都会有不同的静态变量。可参看下面的示例代码。
#include <string>
#include <iostream>template<typename TYPE>
class CBase
{
public:CBase(TYPE data) : m_data(data){m_nInstanceCount++;}TYPE GetData() const{return m_data;}static int GetInstanceCount(){return m_nInstanceCount;}private:TYPE m_data;static int m_nInstanceCount;
};template<typename TYPE>
int CBase<TYPE>::m_nInstanceCount = 0;int main()
{CBase<int> base1(66);CBase<int> base2(88);CBase<std::string> base3("CSDN");std::cout << CBase<int>::GetInstanceCount() << std::endl; // 输出:2std::cout << CBase<std::string>::GetInstanceCount() << std::endl; // 输出:1return 0;
}
可以看到,我们声明了两个CBase<int>类型的实例,故CBase<int>::GetInstanceCount()返回值为2。声明了一个CBase<std::string>类型的实例,故CBase<std::string>::GetInstanceCount()返回值为1。