C++模板之泛型编程

文章目录

  • 1. 模板概述
  • 2. 模板分类
    • 2.1 函数模板
      • 2.1.1 模板类型参数的多用途
    • 2.2 类模板
      • 2.2.1 类模板概述
      • 2.2.2 类模板定义
        • 2.2.3
      • 2.2.4
  • 3. 模板参数作用域
  • 4. 模板默认实参

1. 模板概述

     模板是泛型编程的基础。使用模板,可以编写出与类型无关的代码;并且它可以使用户为类或者函数声明一种一般模式(类似设计图),使得类中的某些数据成员或者成员函数的参数、返回值取得任意类型。模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一份说明书。模板的定义是以关键字 template开始,后跟用小括号(<)大括号(>)包围起来的若干以逗号(,)分隔的模板参数列表。如:

template<typename T , typename U>

其中template是建立模板的关键字,typename 是一个必须出现在模板类型参数前面的关键字。在创建一个模板时候,typename 等同于 class,其效果是一样的,如:

template<typename T , class U>

你可以在创建一个模板的时候,将这两个关键字混合使用,或是使用其中任意一个都是可以的。但是值得注意的是,templae后面的参数列表中,每个类型参数的前面都需要有一个关键字typename/class, 比如:template< typename T, U> 是错误的,编译时候会报错。模板的参数列表数量需要根据自己的实际情况去定义。 当编译器遇到一个模板定义时候,它其实并不会去生成代码。只有当我们 实例化出模板的一个特定版本时,编译器才会生成代码。所谓“实例化”是这样一个概念。如下面的函数CompareParameterSizes中其参数类型使用的是模板参数中的类型T。在未调用函数 CompareParameterSizes 的时候,是不知道T的具体数据类型的,只有在实际调用的时候,编译器才能根据函数调用的实参变量类型而推导出模板参数中T的实际数据类型。这样的一个过程就是“实例化”,通俗易懂的陈述就是“编译器根据目标创建类或函数的过程就是实例化”。如图1所示,共有2个实例化,一个是int,另外一个是char。
C++模板之泛型编程_第1张图片
                        图1 函数模板实例化过程

/*
 * CompareParameterSizes函数的功能是比较参数 tParameterA和tParameterB的大小。
 * 因为使用了模板,所以这里的参数tParameterA和tParameterA的数据类型可能是int,float,double,char等
 * (备注:因为是说明用例,所以函数实现极其简单)。在该函数CompareParameterSizes还未调用之前,是无法
 * 确定T的数据类型的,因此只要在实际调用了该函数CompareParameterSizes的时候,编译器才能够根据函数调用
 * 中的参数值而推导出来其T的数据类型。比如:CompareParameterSizes(1,2);可以推导出T为int数据类型,这
 * 样一个过程就是所谓的“实例化”。
 */
template<typename T>
int CompareParameterSizes(T &tParameterA, T &tParameterB)
{
	if(tParameterA < tParameterB) return -1;
	else if(tParameterA > tParameterB) return 1;
	else if(tParameterA == tParameterB) return 0;
}

此外,模板定义中,其模板参数列表不能为空;且模板参数列表中类型参数是在编译时候确定的。

template<>   //模板参数列表为空,编译时候会报错

2. 模板分类

     C++中模板共有两种形式,分别是“函数模板”和“类模板”。函数模板与类模板它们一起出现于C++98中,在C++98中,函数模板不支持默认实参类型,而C++11中,可以支持带有默认参数类型的模板。函数模板针对仅参数类型不同的函数,而类模板针对仅数据成员和成员函数类型不同的类。下面分别对“函数模板”和“类模板”做介绍。

2.1 函数模板

     在第1节中提到的函数 " CompareParameterSizes " 就是一个函数模板。其函数参数tParameterA和tParameterB的数据类型使用了模板参数类型T来进行定义。在该函数没有被调用(即没有被实例化)的时候,其参数的数据类型是未知的。只有在该函数被调用的时候,编译器才会根据函数调用实参的数据类型而推导出模板参数列表中的对应数据类型。这里仍然用该函数来说明。
     

template<typename T>
int CompareParameterSizes(T &tParameterA, T &tParameterB)
{
    if(tParameterA < tParameterB) return -1;
    else if(tParameterA > tParameterB) return 1;
    else return 0;
}

int main()
{
	 /*1. 当调用CompareParameterSizes(a, b)函数时候,编译器会根据函数实参来推导出模板
	  * 模板实参,即根据函数调用实参int类型推导出模板实参类型T也为int,并将它绑定到模板
	  * 参数T。
	  * int CompareParameterSizes(int &tParameterA, int &tParameterB)
	  */
    int a = 1, b = 2;
    printf("[int]: %d\n", CompareParameterSizes(a, b));
    
    /* 2. int CompareParameterSizes(float &tParameterA, float &tParameterB)
     * 根据函数实参c,d而推导出模板实参类型T为 float
     */
    float c = 1.0, d = 2.0;
    printf("[float]:%d\n", CompareParameterSizes(c, d));
    
    /* 3. int CompareParameterSizes(char &tParameterA, char &tParameterB)
     *  根据函数实参 e,f 而推导出模板实参类型 T 为 char
     */
    char e = 'h', f = 'b';
    printf("[char]:%d\n", CompareParameterSizes(e, f));
	return 0;
}

打印结果:
在这里插入图片描述
     函数模板具有隐式类型转换的默认规则

2.1.1 模板类型参数的多用途

     对于一个模板,可以将模板参数看着是类型说明符,如同内置的数据类型一样去进行使用。模板参数可以作为函数返回类型、函数形参、函数体内部声明变量及类型间转换。

template<class U>
/* 1. U 作为函数PrintParameterValue的返回类型
 * 2. U 当做内置数据类型来声明PrintParameterValue函数的形参u的数据类型
 */
U PrintParameterValue(U &u)
{
    /* 3. 将模板参数U当做一个内置的数据类型来进行变量声明
     * 其中指针p的数据类型是指向形式参数 u 的数据类型
     /
    const U *p = &u;
    cout<<"u: "<

打印结果:
在这里插入图片描述

2.2 类模板

2.2.1 类模板概述

     类模板是用来生成类的蓝图的。如果要更深刻的理解这句话,回味一下STL(标注模板库)的强大,就可以知道类模板的作用和地位了。顺提一下,STL中的系列容器,它们是模板,而非类型。比如:vector, list。类模板和函数模板不同,编译器不能为类模板推断出其模板参数的类型。因此,为了使用类模板,我们总是/必须在模板名字的后面跟一对尖括号“<>”,且在该尖括号中放上用来代替模板参数的模板实参列表。

2.2.2 类模板定义

     在说明类模板的定义规则之前,先看下官方对vector模板的定义方式。
C++模板之泛型编程_第2张图片
     从上面的截图可以看到,类模板的定义方式和函数模板的定义方式是相同的。都以关键字template开头,后跟一堆尖括号,然后尖括号中是模板参数列表。

template<teypename T, class U>

     同样可以对模板参数列表进行指定默认实参。正如上面图中的template > 。下面自己来实现一个模板类ProfessionalContainer,进一步说明模板类的定义和使用。

/////////// 学生 ///////////
typedef struct{
    string m_Name;      //姓名
    string m_School;    //学校
    string m_Subject;   //学科
    int    m_Age;       //年龄
    int    m_StudentId; //学号
}Student;


////////// 教师 /////////////
typedef struct{
    string m_Name;      //姓名
    string m_Course;    //教学科目
    string m_Address;
}Teacher;


template<typename T>
class ProfessionalContainer{
public:
   typedef size_t	size_type;
    typedef T       value_type;
   ProfessionalContainer(initializer_list<T> initDataList){
        typedef decltype(initDataList.begin()) itEr;
        for(itEr it = initDataList.begin(); it != initDataList.end(); ++it)
        {
            m_Professional.push_back(*it);
        }
   };

    //获取m_Professional容器的大小
    size_type size()
    {
        return m_Professional.size();
    }

    //判断m_Professional是否为空
    bool empty()
    {
        return m_Professional.empty();
    }

    vector<T> m_Professional;
};


int main()
{
	Student stu1, stu2, stu3;
    stu1.m_Age = 22;
    stu1.m_Name = "Name_001";
    stu1.m_School = "School_001";
    stu1.m_Subject = "Subject_001";
    stu1.m_StudentId = 1234;

    stu2.m_Age = 23;
    stu2.m_Name = "Name_002";
    stu2.m_School = "School_002";
    stu2.m_Subject = "Subject_002";
    stu2.m_StudentId = 3456;

    stu3.m_Age = 24;
    stu3.m_Name = "Name_003";
    stu3.m_School = "School_004";
    stu3.m_Subject = "Subject_004";
    stu3.m_StudentId = 5678;

	//正如上面描述过的,因为编译器不能为类模板推导出模板参数类型,所有需要显示的实参。
    ProfessionalContainer<Student> StuT = {stu1, stu2, stu3};
    printf("size:%d\n", StuT.size());
    return 0;
}

打印结果:
C++模板之泛型编程_第3张图片
     上面示例代码中的模板类ProfessionalContainer使用时候,需要显著提供模板实参,即 ProfessionalContainer< Student > StuT; 这里的 Student 就是一个显示模板实参,它们被绑定到模板参数,编译器使用这些模板实参来实例化出特定的类。若去掉这个显示模板实参 Student,则编译报错。如下图所示。(备注:关于 initializer_list 的使用请参考另一篇文章 《C++11之列表初始化》

C++模板之泛型编程_第4张图片

     
     

2.2.3

     
     
     
     
     

2.2.4

     
     
     
     
     

3. 模板参数作用域

     对于模板中的参数列表,其名字可以是任意满足标识符命名规则的变量名。

//以下这些模板的定义都是合法的,其参数类型名都遵循着标识符命名规则
template<typename T> 
template<typename People>
template<typename Foo>
template<class U>

而且其模板参数同样有着普通变量作用域的规则。模板参数列表中参数名的作用域(可见范围)是在其声明之后,到模板声明/定义结束之前。同时它也会隐藏外层作用域中声明的相同变量名的名字。模板内不能重用模板参数列表名;并且模板参数列表中的参数名字不能重用。

/* 1. 模板参数列表中,重复使用同一个变量名U; 报错
 *compare.cpp:3:43: error: conflicting declaration ‘class U’
 *compare.cpp:3:22: error: ‘U’ has a previous declaration as ‘class U’
 */
template<typename M, typename U, typename U>

/* 2. 模板参数名I会覆盖前面的 “int类型别名I”,这里I是模板参数类型,需要根据实例化
 * 的调用推导得出.
 */
typedef int I;
template<class I>
void testFunc(I &a){}

/* 3. 重声明模板参数名a;报错
 * compare.cpp: In function ‘void testFunc(I&)’:
 * compare.cpp:6:23: error: declaration of ‘I a’ shadows a parameter
 */
typedef int I;
template<class I>
void testFunc(I &a){I a;}

4. 模板默认实参

     众所周知,C++中是可以对函数指定默认实参;如void AddTwoNumber(int a, int b = 10); 其中参数b默认值是10。因此,当调用该函数的时候,有两种方式:

//方法1. 只传参数a,而参数b采用默认值(10);
AddTwoNumber(10);

//方法2. 此刻我们不想使用参数b默认值10,因此,传入参数a和参数b;
AddTwoNumber(10, 20);

     和为函数指定默认实参一样,我们同样可以为模板提供默认实参。新标准中,可以为函数类模板提供默认实参,但是更早的C++标注里是只能为类模板提供默认实参的。需要注意的是,当模板有多个参数列表的时候,其实参默认值必须从做往右依次添加(这个和函数默认实参的规则保持一致)。template< typename T=int, typename U> 编译失败,因为不满足默认参数的规则。修改为template 则编译通过。

/*1. 模板参数 U 默认类型为int, 因为PrintParameterValue函数内部可以直接使用 %d 格式符来
 * 打印其参数b的值. 而参数 T 的类型未知,因此使用 cout来进行打印。
 * /
template
void PrintParameterValue(T &a, U &b)
{
    std::cout<<"a:"< 
 * 函数对象模板. 由于Stu类没有 < 比较操作,因此我们自己实现一个比较
 * Stu类型对象的函数CompareLengthOfName。
 */
struct Stu
{
    Stu(string strName){m_Name = strName;}
public:
    string m_Name;
};

template<typename T, typename B = less<T>>
int CompareTwoNumber(T &a, T &b, B bCompare = B())
{
    if(bCompare(a, b)) return -1;
    else if(bCompare(a, b)) return 1;
    return 0;
}

bool CompareLengthOfName(Stu &nameA, Stu &nameB)
{
    return  nameA.m_Name < nameB.m_Name;
}

int main()
{
	 Stu s1("Lixiaogang5"), s2("Lushihui");
     printf("Ret:%d\n", CompareTwoNumber(s1, s2, CompareLengthOfName));
     //打印结果:Ret: -1
     return 0;
}


     
     
     
     
     
     
     
     
     
     
     
     











你可能感兴趣的:(C++语言)