【C++】模板进阶

模板进阶

1、非类型模板参数

模板参数分为类类型形参非类型形参

类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。

非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用(类似于替代宏)

c 复制代码
//定义一个模板类型的静态数组
//define N 100
template<class T = int, size_t N = 100>
class Stack
{
public:
	//成员函数
	
private:
	T _a[N];
	int _top;
	int _capacity;
};

int main()
{
	Stack<int, 10>   s1;		// 实例化一个栈可以存储10个整形数据
	Stack<double, 1000> s2;		// 实例化一个栈可以存储1000个double类型
	Stack<int> s3;			// 100  //缺省值
	Stack<> s4;					// 100  //全缺省
	
	int x;
	cin >> x;
	Stack<int, x> s5;		//报错,不能传变量
	return 0;
}

实现一个静态的栈,存储的数据确认后无法扩容,一般是通过define(宏)来定义一个标识符常量,并给一个初始值,例如N,最终栈只能存储N个数据 ,但当我们想存储多组数据并且每组数据的数量不相同 ,比如上面的s1存储10个int类型 的数据,s2存储1000个double类型 的数据,这时会造成空间浪费,而非类型模板可以很好解决这个问题,我们可以根据需要传递参数

注意:

  1. 非类型模板参数的类型必须是整数 ,即int、size_t、char 等。浮点数、类对象以及字符串是不允许作为非类型模板参数的
  2. 非类型模板一定是一个常量 ,在类中不能修改(N在编译时被固定,运行时不能修改)
  3. 非类型的模板参数必须在编译期就能确认结果

2、模板的特化

2.1 概念

在原模板的基础上,针对某些特殊类型进行特殊化处理,特殊类型会优先匹配特化的模板,模板特化分为函数模板特化和类模板特化

2.2 特化的使用场景

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板

c 复制代码
template<class T>
bool Less(T left, T right)
{
	return left < right;
}

int main()
{
	int a = 1;
	int b = 2;
	cout << Less(a, b) << endl;

	int* p1 = &a;
	int* p2 = &b;
	cout << Less(p1, p2) << endl; // 可以比较,结果错误

	return 0;
}

第一次需要比较a和b的大小,模板参数T被实例化为int,Less中进行两个整数的比较,第二次需要通过a和b的地址比较两个的大小,此时模板参数T被实例化为int*,因此Less函数是进行两个地址的比较,并没有比较两个地址中存储的数据大小,因此结果错误,所以要对Less函数进行特化

当编译器识别到传过来的是两个指针,会优先匹配这个特化模版

c 复制代码
template<>
bool Less<int*>(int* left, int* right)
{
	return *left < *right;
}

2.3 函数模板特化

函数模板的特化步骤:

  1. 必须要先有一个基础的函数模板
  2. 关键字template后面接一对空的尖括号<>
  3. 函数名后跟一对尖括号<>,尖括号中指定需要特化的类型
  4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误

注意:特化模板的const的修饰位置

c 复制代码
template<class T>
bool Less(T left, T right)
{
	return left < right;
}

template<>
bool Less<int*>(int* left, int* right)
{
	return *left < *right;
}

int main()
{
	int a = 1;
	int b = 2;
	cout << Less(a, b) << endl;

	int* p1 = &a;
	int* p2 = &b;
	cout << Less(p1, p2) << endl; // 可以比较,结果错误

	return 0;
}

如果在函数模版处加上const和&修饰

c 复制代码
template<class T>
bool Less(const T& left, const T& right)
{
	return left < right;
}

如果T为int*,找不到匹配的模板,会报错

此时可能会想给特化模版也加上const和&修饰

c 复制代码
template<>
bool Less<int*>(const int*& left,const int*& right)
{
	return *left < *right;
}

同样报错

注意:const在*的左边是修饰指针指向的对象不能修改,const在*的右边修饰的是指针本身,比如 const T* p1 修饰的是*p1T const * p2 修饰的是*p2T* const p3 修饰的是p3

解决方法:

1、修改const模板的const指向问题

c 复制代码
template<>
bool Less<int*>(int* const& left, int* const& right)
{
	return *left < *right;
}

修饰的是left和right

2、不用特化模板,直接实例化一个函数

c 复制代码
bool Less(int* left, int* right)
{
	return *left < *right;
}

加入const后代码

c 复制代码
template<class T>
bool Less(const T& left, const T& right)
{
    return left < right;
}

template<>
bool Less<int*>(int* const& left, int* const& right)
{
    return *left < *right;
}

int main()
{
    int a = 1;
    int b = 2;
    cout << Less(a, b) << endl; // 比较 int,输出 1

    int* p1 = &a;
    int* p2 = &b;
    cout << Less(p1, p2) << endl; // 特化版本,比较 *p1 < *p2,输出 1

    return 0;
}

2.4 类模板特化

2.4.1 全特化

全特化是指将模板参数列表中所有的参数都确定化

c 复制代码
template<class T1, class T2>
class Data
{
public:
	Data() { cout << "Data<T1, T2>" << endl; }
	
private:
	T1 _d1;
	T2 _d2;
};

// 全特化  可以认为是一个全新的类,和模板类没有关系
template<>
class Data<int, char>
{
public:
	Data() { cout << "Data<int, char>" << endl; }
};

int main()
{
	Data<int, int> d1;   //使用类模板
	Data<int, char> d2;  //使用全特化
	return 0;
}

输出结果:

Data<T1, T2>

Data<int, char>

2.4.2 偏特化

偏特化是指允许一部分类型是模板参数,一部分类型是固定的特化类型,偏特化有两种表现方式:

  • 部分特化
c 复制代码
template<class T1, class T2>
class Data
{
public:
	Data() { cout << "Data<T1, T2>" << endl; }
	
private:
	T1 _d1;
	T2 _d2;
};

//将第二个参数特化为int
template<class T1>
class Data<T1, int>
{
public:
	Data() { cout << "Data<T1, char>" << endl; }
private:
	T1 _d1;
	T2 _d2;
};

int main()
{
	Data<int, int> d1;   //使用部分特化
	Data<char, int> d2;  //使用部分特化
	Data<int, char> d3;  //使用类模板
	return 0;
}

将原类模板第一个参数仍然使用模板T1,第二个模板参数特化成int型,此后只要第二个参数传的是int,就会使用特化后的类

  • 参数进一步限制
c 复制代码
template<class T1, class T2>
class Data<T1*, T2*>
{
public:
	Data() { cout << "Data<T1*, T2*>" << endl; }

private:
	T1 _d1;
	T2 _d2;
};

template<class T1, class T2>
class Data<T1&, T2&>
{
public:
	Data() { cout << "Data<T1&, T2&>" << endl; }

private:
	T1 _d1;
	T2 _d2;
};

template<class T1, class T2>
class Data<T1*, T2&>
{
public:
	Data() { cout << "Data<T1*, T2&>" << endl; }
	
private:
	T1 _d1;
	T2 _d2;
};

int main()
{
	Data<char*, char*> d4;
	Data<int*, char*> d5;
	Data<double*, double*> d6;
	Data<double&, double&> d7;
	Data<double*, double&> d8;
	return 0;
}

输出结果:

Data<T1*, T2*>

Data<T1*, T2*>

Data<T1*, T2*>

Data<T1&, T2&>

Data<T1*, T2&>

3、模板分离编译

3.1 什么是分离编译?

一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。

3.2 模板的分离编译

将模板的声明与定义分离,头文件中进行分离,源文件中完成定义

项目中有很多,cpp文件(源文件),编译器是一个一个单独编译的,编译完每个.cpp会生成一个 .obj 文件,最后链接器把所有 .obj 拼在一起,变成一个可运行的 exe,这就是分离编译

c 复制代码
// a.h
template<class T>
T Add(const T& left, const T& right);

// a.cpp
#include"a.h"

template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}

// main.cpp
#include"a.h"

int main()
{
	Add(1, 2);
	Add(1.0, 2.0);
	return 0;
}

上面的代码产生了错误链接

编译过程:

  1. 编译 test.cpp:看到 Add 调用,但不知道实现,先留个空位,等着链接时填地址。
  2. 编译 a.cpp:里面有 Add 的实现,生成二进制代码。
  3. 链接:链接器去 a.obj 里找到 Add,把空位填上,完美运行

但模板的分离编译不同,模板不是真正的函数类,只有被使用时才会生成真正的代码,错误写法:

  • a.h:模板声明
  • a.cpp:模板实现
  • test.cpp:调用模板

为什么报链接错误?

  1. 编译test.cpp:调用了Add,但只看到声明,看不到实现,只能留空位等链接。
  2. 编译a.cpp:里面虽然有模板实现,但没人用它!模板规则:不用就不生成真实代码。所以a.obj里空空如也,没有任何函数二进制代码。
  3. 链接:链接器去a.obj里找Add,啥也找不到→ 直接报错!
  • 没有使用模板的分离编译
c 复制代码
// a.h
int Add(int left, int right);

//a.cpp
#include "a.h"

int Add(int left, int right)
{
	return left  + right
}

//main.cpp
#include "a.h"

int main()
{
	Add(1, 2);
	return 0;
}

f在编译main.cpp时,编译器不知道Add函数的实现,因为a.h的头文件中只有关于Add函数的声明,所以当编译器碰到对Add函数的调用时只是给出一个指示,指示链接器应该为它寻找Add函数的实现体,也就是说main.obj中没有关于Add函数的任何一行二进制代码

在编译a.cpp是,编译器找到了Add函数的实现,也就是对应的二进制代码出现在a.obj离

链接时,链接器在a.obj中找到了Add函数的实现代码地址。然后将main.obj中的call XXX地址改成Add函数的实际地址

  • 模板需要实例化

模板函数的代码不能直接编译生成二进制代码,要有实例化的过程

c 复制代码
//test.cpp
template<typename T>
void Swap(T& left, T& right)
{
	T temp = left;
	left = right;
	right = temp;
}

int main()
{
	int i1 = 10;
	int i2 = 20;
	Swap(i1, i2);

	double d1 = 1.1;
	double d2 = 2.2;
	Swap(d1, d2);
}

如果在test,cpp中没有调用Swap函数,Swap函数就得不到实例化,从而test.obj中也就没有关于Swap的任何一行二进制代码,如果调用了Swap(i1, i2);和Swap(d1, d2);此时test.obj中就有了Swap和Swap两个函数的二进制码段

模板分离编译的解决方法:

1、将声明和定义放到同一个文件 "xxx.hpp" 里面 或者 "xxx.h"里 。(推荐)

c 复制代码
// a.h
#pragma once

// 模板声明 + 实现 全部写在头文件里
template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}

// test.cpp
#include "a.h"

int main()
{
	Add(1, 2);
	Add(1.1, 2.2);
	return 0;
}

2、模板定义的位置显式实例化。这种方法不实用,不推荐使用。

c 复制代码
// a.h
#pragma once

// 只有声明
template<class T>
T Add(const T& left, const T& right);

// a.cpp
#include "a.h"

// 实现
template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}

// 必须手动写:显式实例化
template int Add<int>(const int&, const int&);
template double Add<double>(const double&, const double&);

// test.cpp
#include "a.h"

int main()
{
	Add(1, 2);
	Add(1.1, 2.2);
	return 0;
}

4、模板总结

  • 优点
  1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
  2. 增强了代码的灵活性
  • 缺点
  1. 模板会导致代码膨胀问题,也会导致编译时间变长
  2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误

如果文章中有错误或不足,欢迎大家指正交流。

相关推荐
繁星蓝雨3 小时前
Qt多界面创建的优化问题(main函数或主界面中创建?)—————附带详细方法
c++·qt·架构·多界面管理
Cx330❀3 小时前
Qt 入门指南:从零搭建开发环境到第一个图形界面程序
xml·大数据·开发语言·网络·c++·人工智能·qt
蜡笔小马3 小时前
02.C++设计模式—建造者模式详解
c++
诙_3 小时前
深入理解C++设计模式
c++·设计模式
昵称小白3 小时前
C++ 刷题语法速查
c++·算法
Qt程序员4 小时前
【无标题】
linux·c++·消息队列·共享内存·c/c++·管道·信号量
十五年专注C++开发4 小时前
Qt程序设计涉及到的开发软件
开发语言·c++·qt
邪修king5 小时前
C++ typename & auto 彻底讲透:核心作用、推导规则、避坑指南
开发语言·c++
姆路6 小时前
Qt尺寸策略
c++·qt