> 文章列表 > 模板的使用大全

模板的使用大全

模板的使用大全

概述

        在C++中,有两种特别重要的编程思想。一种是我们熟知的面向对象编程,另一种是泛型编程。所谓泛型编程,就是以一种不依赖任何特定数据类型的方式编写代码。在C++ STL标准库中,有许多泛型编程的例子,像vector、list、map等,都用到了泛型编程。模板是泛型编程的基础,它使用参数化的类型来创建函数和类,分别对应函数模板和类模板。通过模板,可以实现数据类型的多态化,可以编写支持多种数据类型的函数和类,大大提高了代码的复用性。

函数模板

        1、函数模板的定义如下:

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

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

          {

                     函数体

          }

        其中,template是关键字,用于声明模板。尖括号<>中的参数是模板的形参,可以有一个,也可以有多个。typename是关键字,用于表示后面的符号是一种数据类型。也可以用class代替typename,但为了与类申明时的class区别,推荐使用typenameT1T2为通用的数据类型,名称可以更改,一般推荐全大写。在下面的示例代码中,我们给出了一个函数模板的声明。

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、有时候在使用函数模板时,会发现传入的数据类型并不支持指定的操作和运算。在下面的示例代码中,我们希望对base1base2进行相加操作,但我们并没有重载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。