一、泛型编程
泛型编程:跟具体类型无关的代码
1.1 模板参数和函数参数的区别
- 传的东西
- 函数参数传的是变量
- 模板参数传的是类型
- 传递的时间
- 函数参数是运行时传递过去的:因为类型确定,所以可以直接编译(不管用不用,反正会把它编译好)
- 模板参数是编译时传过去的:编译时就确定类型,然后推导出有类型的函数代码,然后才能编译(用哪个类型的就编译哪个类型的)
1.2 模板类型和参数的传递过程
-
传类型,传参数的过程
-
仿函数,函数传入模板的过程
-
全局函数,成员函数传入模板的过程
二、函数模板
2.1 定义
2.1.1 形参单类型
template<class T>
void Swap(T& left,T& right)
{
T temp= left;
left = right;
right = temp;
}
template<class T>
中 class
可以用 typename
代替。
- 调用:
- 推演实例化:
不能推演成两个类型:int a = 0,b = 1; double c = 1.1,d = 2.2; Swap(a,b); Swap(c,d);
- 显式实例化:
Swap<int>(a,b); Swap<double>(c,d);
2.1.2 形参多类型
template<class X,class Y>
void func(const X& x,const Y& y)
{
cout<<x<< endl;
cout<<y<<endl;
}
这里 cout
就比 printf
更适合(printf
需要指明对象)
//同类不同类都可以调用
func(1.1,2.2);
func(1.1,2);
2.1.3 形参无类型
形参不涉及泛型,就无法确定 T
的类型,只能显式实例化调用。
template<class T>
T*f()
{
T* p = new T[10];
return p;
}
//无法推倒类型,只能显示实例化调用
double* p = f<double>();
2.2 原理
- 模板的推演
实参传递给形参,推断类型的过程。 - 模板的实例化
推出类型后,用模板生成函数的过程。
不同类型调用的不是同一个函数(实例化生成了不同函数)
2.3 函数模板和普通函数可以同时存在
// 普通函数
int Add(int left, int right)
{
return left + right;
}
//函数模板
template<class T>
T Add(T left, T right)
{
return left + right;
}
匹配调用原则:
- 有普通的适配函数,优先调普通函数
- 没有模板函数,那就会类型转换一下(可以转换的),去调普通函数
- 有模板函数,而普通函数又不匹配,那就去调模板函数生成个合适的
2.4 普通函数和模板函数调用格式
三、类模板
3.1 模板和 typedef
利用 typedef
可以让一个类类型试用于不同的类型。
但typedef
无法解决的问题:
//想用一个栈,一个存int,一个存char,但typedef 无法实现
typedef int STDataType;
class Stack
{
public:
STDataType* _a;
};
//cann't achieve the outcome below
Stack st1; // int
Stack st2; //char
3.2 类模板
3.2.1 定义
//类模板示例
template<class T>
class Stack
{
public:
stack(int capacity = 4)
{
_a = new T[capacity];
_capacity = capacity;
}
}
- 类模板的类型:
stack<int> st1
和stack<double> st2
是不同的类型,显示实例化的类型不同,他们就是不同的类。
3.2.2 调用
类模板只能显式实例化调用,不能推导。
3.2.3 构造和析构
Stack(); //right
Stack<T>(); //wrong
构造和析构函数要求函数名与类名相同,而不是跟类型相同。
3.2.4 按需编译,按需实例化
实例化类模板时,类模板中可能会有很多个函数,但只会实例化那些调用的函数。
所以,如果有的函数可能有错误,但不会报错,因为没有实例化。
3.3 声明和定义
-
不允许声明和定义分离到两个文件:
当编译器遇到一个模板定义时,不会生成代码,只有在遇到实例化语句时,才根据模板定义生成实例代码,此时,编译器需要知道模板的定义。(后面有详细说明) -
成员函数声明和定义形式:
- 类内:跟没有模板的类没有区别
template<class T>
class A
{
_size =0.
public:
int msize();//成员函数的声明
}:
- 类外:必须重新言明
template
,类域也要带模板类型
template<class T>
int A<T>::msize()
{
return _size;
}
四、非类型模板参数
4.1 宏定义
使用宏可以定义一个需要变化的常量:
#define N 10
缺点: 对不同的使用区域,总是固定的那个常量值。
4.2 非类型模板参数
template<class T,size_t N>
class Stack
{
private:
T _a[N];
int _top;
}
非类型模板参数,只能是整形常量。
- 常量:
template<class T,size_t N>
class Stack
{
public:
void f()
{
N++; //error
}
private:
T _a[N];
int _top;
}
- 整形:
template<class T,double X> //error
template<class T,string S> //error
4.3 容器 array(使用非类型模板参数的一个容器)
array
是固定数组:
- 查看它的成员函数:
因为它的大小固定,所以没有头插头删,尾插尾删,直接用[ ]
访问就行。
4.3.1 内部结构
数组空间是直接定义在类中的,没什么特别的,就是一个数组封装到了一个类中。
4.3.2 优点
- 检查更严格:
普通数组越界读一般不会检查,越界写才会检查,而array
越界的读都不让读。
array<int,10> a1;
a1[10]; //error
但是,仅仅是检查的严格,那我们可以直接用 vector
,它的检查也是比较严格的。
vector<int> a2(10);
a2[10] ; //error
五、函数模板的特化
特化: 对模板进行的特殊处理。
5.1 来源
template<class T>
bool Less(T a, T b)
{
return a < b;
}
cout<<Less(1,2)<<endl;
Date d1(2024,6,13);
Date d2(2024,6,14);
cout<<Less(d1,d2)<<endl; // 正确的
Date* p1 = &d1;
Date* p2 = &d2;
cout<<Less(p1,p2)<<endl; // 错误的
Date* p3=new Date(2024,6,13);
Date* p4= new Date(2024,6,14);
cout<<Less(p3,p4)<<endl; // 错误的
p1
和p2
因为在栈上开空间,而p1
先于p2
定义(栈先用高地址空间),所以p1 > p2
p3
,p4
是在堆上开辟的空间,new
出来的地址大小不确定。堆区空间申请也是由低到高申请,但不能排除有之前申请后来释放回来的空间,所以不能确定(还有内存池的问题)。
PS: 因为begin()
和 end()
是指向 ListNode
的指针,而ListNode
的空间是 new
出来的,地址不确定,所以迭代器不能用 <
去比较,而是用 !=
来比较判断。
显然,在次模板函数的使用中有些类型无法使用,和我们预期的效果不相符,所以需要特殊化处理。
5.2 解决
5.2.1 利用特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left< *right;
}
5.2.2 利用匹配
增加一个普通函数,这个比模板更合适,就不会走模板了。
bool Less(Date* left, Date* right)
{
return *left< *right;
}
六、类模板的特化
6.1 全特化
原模板类:
template<class T1, class T2>
class Data
{
public:
Data() { cout << "Data<T1, T2>" << endl; }
};
全特化模板:特化所有参数
template<>
class Data<int, char>//对 T1,T2都具体化了
{
public:
Data() { cout << "Data<int, char>" << endl; }
private:
int _d1;
char _d2;
}
6.2 偏特化
原模板类:
template<class T1, class T2>
class Data
{
public:
Data() { cout << "Data<T1, T2>" << endl; }
};
偏特化的情况可以分为两种情况:特化部分参数和对参数类型进行限制的两种情况。
6.2.1 特化部分参数
template <class T1> //对T2进行了特化,但T1仍是泛型
class Data<T1, int>
{
public:
Data() { cout << "Data<T1, int>" << endl; }
private:
T1 _d1;
int _d2;
};
6.2.2 对参数类型进行一定限制
对指针和引用进行的限制,因为这两个实在很特殊。
template<class T1,class T2>
class Data<T1*,T2*>
{
public:
Data() { cout << "Data<T1, int>" << endl; }
private:
T1* _d1;
T2* _d2;
}
6.3 匹配原则
6.4 示例
一个优先级队列,里面放了 Date
类的对象的指针,优先级队列中是需要比较大小的。
int main()
{
zjy::priority_queue<Date*> q;
q.push(new Date(2024,5,24));
q.push(new Date(2024,5,23));
q.push(new Date(2024,5,25));
q.push(new Date(2024,5,26));
while(!q.empty())
{
cout<< *q.top()<< endl;
q.pop();
}
}
结果会反复变,不确定。
- 注意: 我们假设是有实现的
Date
的operator < / >
的函数,和仿函数less
的
template<class T>
class less
{
public:
bool operator()(const T&x,const T& y)
{
return x<y;
}
};
bool operator<(const Date& d)const
{
return (_year<d._year)||(_year == d._year&& _month<d._month)||(_year == d._year&& _month == d._month &&_day< d._day);
}
6.4.1 结果错误原因
-
当
priority_queue
中放的是Date
类的对象时,结果是正确的,其调用逻辑如下:
传入的类型是Date
时,会变成Date
和Date
的比较(自定义操作,会去调用Date
的operator <
函数) ,结果正确。 -
当
priority_queue
中放的是Date
类的指针时,结果是错误的,其调用逻辑如下:
传入的类型是Date*
时会变成Date*
和Date*
的比较(内置类型,直接比较),比较地址,结果不对。
6.4.2 解决方案
结果错误,归根结底是比较函数 less
的不当,所以可以从两个方面改动:
- 重新写一个比较函数/仿函数,可以比较
Date*
的,调用这个 - 将模板
less
特化处理,遇到Date*
就去调用特化版本的
- 方法一:改变比较方式
struct PDateLess //一个可以比较Date* 的仿函数
{
bool operator()(const Date* p1,const Date* p2)
{
return *p1< *p2;
}
};
改变比较方式后的调用逻辑:
-
方法二:对 less 进行特化
- 对
less
全特化:
template<> class less<Date*> { public: bool operator()(const Date* x, const Date* y) { return *x< *y; } };
- 对
less
偏特化:
template<class T> class less<T*> { public: bool operator()(const T* x,const T* y) { return *x < *y; } };
- 对
对 less
特化后的调用逻辑:
- 对比
对less
进行特化,特化后的类仍是less
(只是类型具体了些而已),而less
是缺省的类型,不用额外传参数
七、模板声明和定义分离
7.1 声明和定义分离原因
一般的多文件形式:.h
中放声明,一个 .cpp
中放定义(func.cpp
),一个 .cpp
中是使用的代码(test.cpp
)。
而模板不推荐定义单独放在一个文件 .cpp
中。
原因:
在链接之前,两个 cpp
是单线的,没有进行交互。
-
test.cpp
编译时,调用函数时会利用声明时的地址(非有效地址),等链接时再去找定义时的地址(有效地址)。 -
func.cpp
编译时,如果类型确定,会直接编译,而如果是模板,那T
不确定,模板无法实例化,不会编译。
test.cpp
知道 T 的类型,但没有模板的定义,而 func.cpp
有定义,但不能确定类型 的类型,所以不会编译,最后导致没有实例化后的函数,无法调用使用。
7.2 解决方案
7.2.1 显式实例化
template
int Add<int>(const int&,const int&);
在 .cpp
中告诉它类型。
因为模板定义的文件编译时,不知道类型,所以显式告诉它。
注意跟特化的区别:
7.2.2 声明和定义不分离
将定义放到 .h
中,而所有用模板的地方都会包 .h
文件 。
以前 .h
里面只有声明,只能链接的时候去找,又找不到;现在直接有定义,调用的时候可以实例化了,实例化了就有地址了,call 地址就行了,不用链接的时候再去找了。
注意:
模板有时候会定义成 .hpp
。
Vs 使用后缀识别文件,然后决定调哪个编译器,而linux 根据指令(gcc/g++)决定调哪个编译器。
本文到此就结束啦,如果有帮助到您,希望可以得到您的一个赞!(如有哪里有错漏,望您指点!)