C++ - 模板

一、泛型编程

泛型编程:跟具体类型无关的代码

1.1 模板参数和函数参数的区别

  1. 传的东西
  • 函数参数传的是变量
  • 模板参数传的是类型
  1. 传递的时间
  • 函数参数是运行时传递过去的:因为类型确定,所以可以直接编译(不管用不用,反正会把它编译好)
  • 模板参数是编译时传过去的:编译时就确定类型,然后推导出有类型的函数代码,然后才能编译(用哪个类型的就编译哪个类型的)

1.2 模板类型和参数的传递过程

  1. 传类型,传参数的过程
    在这里插入图片描述

  2. 仿函数,函数传入模板的过程
    在这里插入图片描述

  3. 全局函数,成员函数传入模板的过程
    在这里插入图片描述

二、函数模板

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 Xclass Y>
void func(const X& x,const Y& y)
{
cout<<x<< endl;
cout<<y<<endl;
}

这里 cout 就比 printf 更适合(printf需要指明对象)

//同类不同类都可以调用
func(1.12.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 原理

  1. 模板的推演
    实参传递给形参,推断类型的过程。
  2. 模板的实例化
    推出类型后,用模板生成函数的过程。
    不同类型调用的不是同一个函数(实例化生成了不同函数)

2.3 函数模板和普通函数可以同时存在

// 普通函数
int Add(int left, int right)
{
	return left + right;
}
//函数模板
template<class T>
T Add(T left, T right)
{
	return left + right;
}

在这里插入图片描述

匹配调用原则:

  1. 有普通的适配函数,优先调普通函数
  2. 没有模板函数,那就会类型转换一下(可以转换的),去调普通函数
  3. 有模板函数,而普通函数又不匹配,那就去调模板函数生成个合适的

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> st1stack<double> st2是不同的类型,显示实例化的类型不同,他们就是不同的类。

3.2.2 调用

类模板只能显式实例化调用,不能推导。

在这里插入图片描述

3.2.3 构造和析构

Stack(); //right
Stack<T>(); //wrong

构造和析构函数要求函数名与类名相同,而不是跟类型相同。

3.2.4 按需编译,按需实例化

实例化类模板时,类模板中可能会有很多个函数,但只会实例化那些调用的函数。

所以,如果有的函数可能有错误,但不会报错,因为没有实例化。

3.3 声明和定义

  • 不允许声明和定义分离到两个文件:
    当编译器遇到一个模板定义时,不会生成代码,只有在遇到实例化语句时,才根据模板定义生成实例代码,此时,编译器需要知道模板的定义。(后面有详细说明)

  • 成员函数声明和定义形式:

  1. 类内:跟没有模板的类没有区别
template<class T>
class A
{
	_size =0.
public:
	int msize();//成员函数的声明
}:
  1. 类外:必须重新言明 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;	// 错误的

在这里插入图片描述

  • p1p2 因为在栈上开空间,而 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(2024524));
	q.push(new Date(2024,523));
	q.push(new Date(2024525));
	q.push(new Date(2024526));
	while(!q.empty())
	{
		cout<< *q.top()<< endl;
		q.pop();
	}
}

结果会反复变,不确定。

  • 注意: 我们假设是有实现的 Dateoperator < / > 的函数,和仿函数 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 结果错误原因

  1. priority_queue 中放的是 Date 类的对象时,结果是正确的,其调用逻辑如下:
    在这里插入图片描述
    传入的类型是 Date时,会变成 Date Date 的比较(自定义操作,会去调用Dateoperator < 函数) ,结果正确。

  2. priority_queue 中放的是Date类的指针时,结果是错误的,其调用逻辑如下:
    在这里插入图片描述
    传入的类型是 Date* 时会变成 Date*Date* 的比较(内置类型,直接比较),比较地址,结果不对。

6.4.2 解决方案

结果错误,归根结底是比较函数 less 的不当,所以可以从两个方面改动:

  1. 重新写一个比较函数/仿函数,可以比较 Date* 的,调用这个
  2. 将模板 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++)决定调哪个编译器。


本文到此就结束啦,如果有帮助到您,希望可以得到您的一个赞!(如有哪里有错漏,望您指点!)

  • 12
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值