在上一节的priority_queue讲解完成后,完成了一些常用C++容器的基础使用和底层实现,这篇博客开始将继续向后学习C++的内容。
一.非类型模板参数
在之前的模板函数以及模板类中,模板的类型代替了具体的参数。这里我们再学一种新的模板参数------非类型模板参数。这个模板参数不代表一个具体的类型,而用来指代一个常量,如下面代码所示:
cpp
template<class T , size_t N = 10>
class arr
{
public:
private:
T _a[N];
};
这里实现了一个数组,并用类对数据实现了封装,同时实现为类模板,唯一不同的是,在模板参数列表中,我们添加了一个size_t类型的参数N,并设默认值为10。在数组中用N来实现具体实现多大的空间。具体使用方法如下面的代码所示:
cpp
//这样所有的内容都存储在类中,可能会出现栈溢出的问题
arr<int,10> ar1;
arr<int, 1000> ar2;
这里分别创建了一个大小为10个int的数组ar1和一个大小为1000个int的数组ar2。


通过调试列表可以看到,ar1数组内部有着10个int类型的数据;ar2有着1000个。这样就能通过显示的传递一个常量值来实现对于数组大小的控制。
这就叫做非类型模板参数:可以传递的类型为整形家族:char、int、size_t、long、long long等。
注意,非类型模板参数不能传递变量,只能传递一个常量。因为模板需要在编译时就确定模板参数,所以不能是常量。这也就决定了vs编译器下,哪怕利用该方法,也无法实现变长数组。
在这里,我们引出一个新的容器------array。
1.1 array容器

array容器,包含在array的头文件中。类模板的方式实现,内部有一个类型参数T和一个size_t的非类型模板参数N。其本质是对于数组的封装,内部可以理解为就是一个数组,然后利用类的方式对其进行了一定的封装, 取下标等操作均是调用相应的operator重载函数:
cpp
//一种新的容器array,C++中用来当作数组使用
array<int, 10> a1;
a1[0] = 1;
a1[9] = 100;
array<int, 10>::iterator it = a1.begin();
while (it != a1.end())
{
cout<< *it << " ";
it++;
}
cout << endl;
//从输出结果可以看出,array容器并不会对数据进行初始化
这里我们简单生成一个能存放10个int数据类型的array。将第一个数据改为1,第10个数据改为100。通过迭代器遍历来输出一下:

可以看出,array容器并没有对数组进行初始化。
这么来看,array容器封装起来的数组似乎和直接数组没有区别。
较为明显的差别就在于,array容器中的相关操作,例如: 下标等,是通过函数重载来实现,而数组数转换为*(arr+i)来实现。这就导致了两种情况下对于数组越界访问的处理情况不同:
cpp
a1[0] = 1;
a1[9] = 100;
array<int, 10>::iterator it = a1.begin();
while (it != a1.end())
{
cout<< *it << " ";
it++;
}
cout << endl;
//从输出结果可以看出,array容器并不会对数据进行初始化
//这样看似乎和直接生成一个数组差别不大
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//数组的越界,可以读取越界的内容,无法进行修改
//数组本质是*(arr+10),且编译器对其的检查是抽查,检查后两个标志位的数据是否被修改
//如果被修改,则报错;如果没有修改,正常运行,所以数组可以越界读取数据
cout << arr[10] << endl;
//arr[10] = 10;
//如果是array的容器,直接就会断言报错
//因为内部本质是调用[]运算符重载函数,可以利用传递的非类型模板参数N
cout << a1[10] << endl;
这里再生成一个普通数组arr,里面存放着10个数据。如果访问下标为10的数据,那么可以正常访问,但是不能修改。因为数组对于越界访问是抽查行为,会在数组后面的两个位置设为标志位,如果标志位的数据被改变了,就会报错。由于读取数据不会改变标志位的数据,所以数组越界,可以正常读,但不能写。但数组越界不是所有位置都能检查出来。
array就不同了,直接在重载函数内部利用断言下标和非类型模板参数N的大小即可,这样就能实现对于数组边界的严格控制。
二. 函数模板特化
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些
错误的结果,需要特殊处理:
这里用一个比较函数模板来举例讲解:
cpp
template<class T>
bool Less(T data1, T data2)
{
return (data1 < data2);
}
cout << Less(1, 2) << endl;
cout << Less(1.1, 2.2) << endl;
这是一个判断第一个数据是否小于第二个数据的函数模板。正常情况下,如果传递两个数据,都能进行比较。但不乏一些特殊情况:
cpp
int* p1 = new int(5);
int* p2 = new int(2);
cout << Less(p1, p2) << endl;
double* p3 = new double(1.1);
double* p4 = new double(2.2);
cout << Less(p3, p4) << endl;
这里分别创建了p1、p2、p3、p4这四个指针。其中p1和p2指针是一个整形数据指针,p3和p4是double类型的指针。如果直接利用该Less函数进行比较,那么比较的就是指针变量的大小,并不是这数据的大小,为了解决该问题,利用函数特化的新语法:
cpp
template<class T>
bool Less(T data1, T data2)
{
cout << "bool Less(T data1, T data2)" << endl;
return (data1 < data2);
}
//函数模板特化
template<>
bool Less<int*>(int* data1, int* data2)
{
cout << "bool Less<int*>(int* data1, int* data2)" << endl;
return *data1 < *data2;
}
template<>
bool Less<double*>(double* data1, double* data2)
{
cout << "bool Less<double*>(double* data1, double* data2)" << endl;
return *data1 < *data2;
}
首先,模板特化需要一个具体的模板,才能进行特化,所以需要在原Less函数的基础上进行特化。
写作方式为,template<>,内部不再需要加类型T,然后在函数名后,写出要特化的类型是什么,比如是int*,double*,之后分别指明要传递的数据类型为特化后的类型。这样该函数就实现了特化,编译器就知道了,传递的参数为一个指针类型,然后通过解引用操作符比较指针变量指向的内容即可。通过cout输出一些特定语句就可以辨别出是否调用了特化后的函数模板:
cpp
template<>
bool Less<int*>(int* data1, int* data2)
{
cout << "bool Less<int*>(int* data1, int* data2)" << endl;
return *data1 < *data2;
}
template<>
bool Less<double*>(double* data1, double* data2)
{
cout << "bool Less<double*>(double* data1, double* data2)" << endl;
return *data1 < *data2;
}
void test2()
{
cout << Less(1, 2) << endl;
cout << Less(1.1, 2.2) << endl;
int* p1 = new int(5);
int* p2 = new int(2);
cout << Less(p1, p2) << endl;
double* p3 = new double(1.1);
double* p4 = new double(2.2);
cout << Less(p3, p4) << endl;

可以看出,第一个示例,比较1和2、1.1和2.2都是调用函数模板进行比较。而两个指针对象调用了特化后的函数模板。并实现了数据之间的比较。
再看下面这个函数模板:
cpp
template<class T1,class T2>
void print_type(const T1& val1,const T2& val2)
{
//这里的const本质是对val1和val2做的const修饰,不能改变的是变量本身
cout << typeid(val1).name()<<":" << val1 << endl;
cout << typeid(val2).name()<<":" << val2 << endl;
cout << endl;
}
该函数模板,有着两个模板参数T1和T2。该函数实现了分别输出第一个参数和第二个参数的类型和数值。通过之前一些容器的学习,我们一版不会直接传递模板参数类型的数据T,这样的话如果是自定义对象会需要拷贝构造,效率较低。所以一般会const T& 的方式传参。
cpp
print_type(1, 2.2);
print_type(1.1, 2);

正常调用,则会正常输出。
与刚才的问题相同,如果没有函数特化,那么对于指针类型输出的将是指针。
cpp
print_type(1, 2.2);
print_type(1.1, 2);
int* p1 = new int(1);
double* p2 = new double(1.1);
print_type(p1, p2);
char* p3 = new char('a');
double* p4 = new double(1.1);
print_type(p3, p4);

如果仍然像针对其中的数据进行输出,就需要进行特化。注意下面模板特化的方式:
cpp
template<class T1,class T2>
void print_type(const T1& val1,const T2& val2)
{
//这里的const本质是对val1和val2做的const修饰,不能改变的是变量本身
cout << typeid(val1).name()<<":" << val1 << endl;
cout << typeid(val2).name()<<":" << val2 << endl;
cout << endl;
}
template<>
void print_type<int*,double*>(int* const & val1, double* const & val2)
{
//同理,这里也需要达到变量本身不能修改
//所以const应该修饰指针变量本身,而非指针指向的内容
cout << "void print_type<int*,double*>(int* val1, double* val2)" << endl;
cout << typeid((*val1)).name() << ":" << *val1 << endl;
cout << typeid((*val2)).name() << ":" << *val2 << endl;
cout << endl;
}
template<>
void print_type<char*, double*>(char* const & val1, double* const & val2)
{
cout << "void print_type<char*, double*>(char* val1, double* val2)" << endl;
cout << typeid((*val1)).name() << ":" << *val1 << endl;
cout << typeid((*val2)).name() << ":" << *val2 << endl;
cout << endl;
}
特化的方式仍然为在函数名后面加上两个模板参数分别是什么,然后替换参数的类型即可。唯一需要注意的是const的位置。
针对原模板参数,const修饰的是val1和val2,即该参数本身。而如果传递了指针参数,那么const也应该修饰的是该参数本身,即------指针不能改变。所以const应该放到*后面,用来修饰指针而非指针指向的内容。这是极其需要注意的点。

在函数特化之后,也就能通过内部的解引用,达到输出该数据的效果。并可以看出调用了两个特化后的函数模板。
三. 类模板特化
3.1 全特化、半特化
cpp
template<class T1,class T2>
class Data1
{
public:
Data1()
{
cout << "class Data1" << endl;
}
private:
T1 _d1;
T2 _d2;
};
这里,实现了一个两个模板参数的类模板。其中的成员变量_d1用T1的类型、_d2用T2的类型。
全特化就是将T1和T2两个参数均特化为具体的类型:
cpp
//全特化
template<>
class Data1<int,double>
{
public:
Data1()
{
cout << "class Data1<int,double>" << endl;
}
private:
int _d1;
double _d2;
};
与函数模板相同,template<>,之后在类名中分别加上要特化的具体类型,之后将成员变量的类型分别改为这两个具体的特化类型即可;
之后是半特化/偏特化,即针对其中的某些模板类型进行特化处理:
cpp
//半特化/偏特化
template<class T1>
class Data1<T1,double>
{
public:
Data1()
{
cout << "class Data1<T1,double>" << endl;
}
private:
T1 _d1;
double _d2;
};
template<class T2>
class Data1<int, T2>
{
public:
Data1()
{
cout << "class Data1<int, T2>" << endl;
}
private:
int _d1;
T2 _d2;
};
例如第一个特化,只对第二个模板参数T2进行特化为double,T1保留为正常的模板参数。类名后要保留T1和double。第二个特化将T1特化为int,保留第二个模板参数。
cpp
Data1<int, double> d1;
Data1<int, int > d2;
Data1<char, double>d3;
Data1<int, long>d4;
cout << endl;
进行简单调试输出:

第一个<int,double>调用了全特化,因为全特化恰好是int和double;第二个<int,int>,首先全特化不能满足,之后去看半特化,第二个半特化第一个参数为int,就会调用半特化,而非原始模板(可以考虑为哪怕是半成品,也要图省事去找这个半成品);第三个<char,double>,调用第二个模板参数为double的半特化。第四个和第二个相同的原理,会找到半特化。
3.2 对模板参数的进一步限制
和函数模板特化进行对比,如果传递的类型是指针,但是用户的目的又是对指针指向的内容进行相关操作,就用到了该语法的内容。
cpp
//对模板参数的进一步限制
template<class T1,class T2>
class Data2
{
public:
Data2()
{
cout << "class Data2 " << endl;
}
private:
T1 _d1;
T2 _d2;
};
template<class T1, class T2>
class Data2<T1*,T2*>
{
public:
Data2()
{
cout << "class Data2<T1*,T2*>" << endl;
}
void f1()
{
//通过输出也可以看出,T1和T2的类型仍然为原类型,而非指针类型
cout << typeid(_d1).name() << ":" << _d1 << endl;
cout << typeid(_d2).name() << ":" << _d2 << endl;
cout << typeid(*(_d1)).name() << ":" << _d1 << endl;
cout << typeid(*(_d2)).name() << ":" << _d2 << endl;
}
private:
T1* _d1;
T2* _d2;
};
重新实现了一个类模板Data2,内部有两个模板参数。同时实现了一个针对模板参数的进一步限制的函数模板,在类名后指定两个模板参数类型为指针类型,这样就可以利用该指针,也可以利用该指针指向的内容:
cpp
Data2<int*, double*> dd1;
dd1.f1();
这里传递的模板参数分别为int*和double*,之后调用内部的f1函数。

根据输出结果,就可以看出,这里成功实现了对于指针及指针内部数据的比较。
下面将举一些具体的示例进行说明:
cpp
priority_queue<Date> pqd;
pqd.push(Date(2019, 1, 1));
pqd.push(Date(2020, 1, 1));
pqd.push(Date(2021, 1, 1));
pqd.push(Date(2022, 1, 1));
pqd.push(Date(2023, 1, 1));
while (!pqd.empty())
{
cout << pqd.top() << " ";
pqd.pop();
}
cout << endl;
之前实现过一个日期类,这里如果在优先级队列中存放日期类成员,那么Less的仿函数进行比较的时候就会自动调用日期类内部的operator<的比较函数。那么日期也会排列成一个大根堆:

那么还是之前常谈的问题,如果是一个指针对象呢?
cpp
priority_queue<Date*> pqpd;
Date* pd1 = new Date(2019, 1, 1);
Date* pd2 = new Date(2020, 1, 1);
Date* pd3 = new Date(2021, 1, 1);
Date* pd4 = new Date(2022, 1, 1);
Date* pd5 = new Date(2023, 1, 1);
pqpd.push(pd1);
pqpd.push(pd2);
pqpd.push(pd3);
pqpd.push(pd4);
pqpd.push(pd5);
//这样的话就是比较地址,达不到想要的效果。
//如果想要通过地址达到想要的效果,就需要传递合适的仿函数
while (!pqpd.empty())
{
cout << *(pqpd.top()) << " ";
pqpd.pop();
}
cout << endl;
如果传递的是一个指针对象,那么会按照指针的大小排列,内容就不再是一个大根堆。

如果没有本节的模板特化,那么就需要显示的传递一个进行指针内容比较的仿函数:
cpp
template<class T>
class Less_p
{
public:
bool operator()(const T& data1, const T& data2)
{
return (*data1 < *data2);
}
};
priority_queue<Date*,vector<Date*>,Less_p<Date*>> pqpd;
Date* pd1 = new Date(2019, 1, 1);
Date* pd2 = new Date(2020, 1, 1);
Date* pd3 = new Date(2021, 1, 1);
Date* pd4 = new Date(2022, 1, 1);
Date* pd5 = new Date(2023, 1, 1);
pqpd.push(pd1);
pqpd.push(pd2);
pqpd.push(pd3);
pqpd.push(pd4);
pqpd.push(pd5);
//这样的话就是比较地址,达不到想要的效果。
//如果想要通过地址达到想要的效果,就需要传递合适的仿函数
while (!pqpd.empty())
{
cout << *(pqpd.top()) << " ";
pqpd.pop();
}
cout << endl;
通过显示调用Less_p的仿函数,就能定向实现对于指针对象中内容的排序。此时再运行,内容就变成了大根堆:

如果利用本节模板特化的指示,只需要对默认的Less仿函数进行一个针对于指针的特化即可:
cpp
template<class T>
class Less<T*>
{
public:
Less() { cout << "class Less<T*>" << endl; }
bool operator()(T* const data1, T* const data2)
{
return (*data1 < *data2);
}
};
这是通过对模板参数的进一步限制来实现的,定向识别模板参数为指针类型,比较的时候通过解引用针对内容进行比较即可。
cpp
template<>
class Less<Date*>
{
public:
Less() { cout << "class Less<Date*> " << endl; }
bool operator()( Date* const & data1, Date* const& data2)
{
return (*data1 < *data2);
}
};
这是通过函数模板的特化来实现,特化类型为Date*
cpp
Practice::priority_queue<Date*> pqpd;
Date* pd1 = new Date(2019, 1, 1);
Date* pd2 = new Date(2020, 1, 1);
Date* pd3 = new Date(2021, 1, 1);
Date* pd4 = new Date(2022, 1, 1);
Date* pd5 = new Date(2023, 1, 1);
pqpd.push(pd1);
pqpd.push(pd2);
pqpd.push(pd3);
pqpd.push(pd4);
pqpd.push(pd5);
while (!pqpd.empty())
{
cout << *(pqpd.top()) << " ";
pqpd.pop();
}
cout << endl;


可看出,利用本篇博客的内容,也可针对指针内容进行比较。但如果通过显示传递一个仿函数,那就没这些事了。
四. 模板的分离编译
之前在实现C++一些基本容器,比如vector list时,它们的实现方式为类模板,当时说的是,如果对模板函数声明和定义进行分离,就会出现编译错误,在这里详细讲解一下:

Fun.h头文件进行模板函数的声明,Fun.cpp进行函数的定义,main中进行调用。

此时会有两个编译错误,LNK201链接错误。
下面对C/C++的编译过程进行一次回顾:

预处理:展开头文件,宏替换,去掉注释,条件编译 (Fun.a test2.a)
编译:检查语法,生成汇编代码 (Fun.s test2.s)
汇编:生成二进制机器码 (Fun.o test2.o)
链接:多个文件进行合并,符号表合并,生成可执行程序 (a.out)
如果是正常函数的分离编译,那么每个文件会依次进行预处理-编译-汇编的过程,最后在符号表合并的时候,将函数定义的位置进行填补即可。
而模板会出现链接报错的原因就在于实例化阶段。
在main函数中首次调用Fun函数,并传递一个参数进行实例化,而在test2文件中,没有Fun函数的定义。而又由于各个文件在链接之前是分开进行各个步骤,这就代表这出现Fun函数定义的Fun.cpp中无法进行实例化,就不存在这个函数,当然会出现链接错误。而如果一定要分离编译,就需要下面这种做法:

在模板函数定义的下面template 然后参数的位置将类型T替换为想要的类型即可。

这样就能正常输出。
但这样过于麻烦,如果再想调用一个char类型的数据,那么就再次需要进行一次相同的声明,否则仍然会出现链接错误。一般就不要这样使用了,就将函数在声明的位置定义即可。或者不进行两个文件的分离编译,都在头文件中编译即可。


直接就可正常运行。
最后对模板的优缺点进行总结:
优点:
1.模板复用了代码,节省了程序员的编写成本,将复用内容交给编译器自行推导。C++的标准模板库也因此诞生。
2.模板增强了代码的灵活性。(不再需要像C语言一样针对特定的类型,特定的方式,实现特定的过程。而只需要针对特定的对象即可)
缺:
1.编译报错比较难以定位准确,错误信息凌乱。
2.会导致代码膨胀问题,但这个并不是类模板的缺点,哪怕人工实现,也是需要这么多代码。