【C++】模板初阶

模板初阶

【C++】模板初阶_第1张图片

文章目录

  • 模板初阶
  • 1.泛型编程
  • 2.函数模板
    • 2.1.函数模板概念
    • 2.2.函数模板格式
    • 2.3.函数模板的原理
    • 2.4.函数模板的实例化
    • 2.5.模板参数的匹配原则
  • 3.类模板
    • 3.1.类模板的定义格式
    • 3.2.类模板的实例化
  • 扩展
  • 4.补充
    • 4.1.函数模板一定是推演?类模板一定是指定?
    • 4.2.模板的分离编译

1.泛型编程

**泛型编程:**不再是针对某种类型,能适应广泛的类型

  • 如下的交换函数:
//交换int类型
void Swap(int& left, int& right)
{
	int temp = left;
	left = right;
	right = temp;
}
//利用C++支持的函数重载交换double类型
void Swap(double& left, double& right)
{
	double temp = left;
	left = right;
	right = temp;
}

使用函数重载虽然可以实现不同类型的交换函数,但是有以下几个不好的地方:

  1. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数,使得代码重复性高,过渡冗余
  2. 代码的可维护性比较低,一个出错可能所有的重载均出错

那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?

【C++】模板初阶_第2张图片

如果在C++中,也能够存在这样一个模具,通过给这个模具中填充不同材料(类型),来获得不同材料的铸件(即生成具体类型的代码),那将会节省许多时间和精力。巧的是前人早已将树栽好,我们只需在此乘凉。

泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础

模板分为如下两类:

  • 函数模板
  • 类模板

2.函数模板

2.1.函数模板概念

函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本

2.2.函数模板格式

template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表)
{
    //……
}
注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)

因此,交换函数就可以这样套用模板:

template<typename T>//或者template
void Swap(T& left, T& right)
{
	T tmp = left;
	left = right;
	right = tmp;
}

调用情况如下:

【C++】模板初阶_第3张图片

2.3.函数模板的原理

**问题:**我上述交换函数调用过程中的Swap都是调用的同一个函数吗?

当然不是,这里我三次Swap不是调用同一个函数,其实我Swap的时候根据不同的类型通过模板定制出专属你的类型的函数,然后再调用,这里可以通过反汇编观察到:

【C++】模板初阶_第4张图片

函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。
所以其实模板就是将本来应该我们做的重复的事情交给了编译器。
【C++】模板初阶_第5张图片

在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。

  • 补充:

其实库里面有一个swap函数,因此我们也不需要自己写模板了:

【C++】模板初阶_第6张图片

直接套用swap即可:

int main()
{
	int a = 1, b = 5;
	double c = 1.2, d = 6.66;
	swap(a, b);
	swap(c, d);
}

2.4.函数模板的实例化

用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。

  • 隐式实例化:让编译器根据实参推演模板参数的实际类型
template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
	Add(a1, a2); //编译器推出T是int
	Add(d1, d2); //编译器推出T是double
}

但是我调用的时候如若这样就会出错:

int main()
{
    int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
	Add(a1, d1); //err 编译器推不出来
	/*
    该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
    通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有
    一个T,编译器无法确定此处到底该将T确定为int 或者 double类型而报错
    注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅
    */
}

【C++】模板初阶_第7张图片

编译器无法确定这里的T到底是int还是double。此时有两种处理方式:

**法一:**用户自己来强制转化

int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
    Add(a1, (int)d1); //强制类型转换。或者Add((double)a1, d1);
}

**法二:**使用显式实例化

  • 显示实例化:在函数名后的<>中指定模板参数的实际类型
template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
    //显示实例化
	Add<int>(a1, d1); //double隐式类型转换成int 
	Add<double>(a1, d2); 
}
  • 补充:模板支持多个模板参数。
// 专门处理int的加法函数
int Add(int left, int right)
{
	return left + right;
}
template<class T1, class T2>
T1 Add(const T1& left, const T2& right)
{
	return left + right;
}

int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.1, d2 = 20.2;
	// 自动推演实例化
    // 与非函数模板类型完全匹配,不需要函数模板实例化
	cout << Add(a1, a2) << endl;
	cout << Add(d1, d2) << endl;
	// 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
	cout << Add(a1, d2) << endl;
	cout << Add(d1, a2) << endl;
	Add<int, char>(1, 'A'); //多个模板参数也可指定显示实例化不同类型

	return 0;
}

2.5.模板参数的匹配原则

  • 原则1: 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
//专门处理int的加法函数
int Add(int left, int right)
{
	return left + right;
}
//通用加法函数
template<class T>
T Add(T left, T right)
{
	return left + right;
}
int main()
{
	Add(1, 2); //会调用哪个Add函数?
}

首先,这俩Add可以同时存在,关键是我调用Add时调的是模板函数Add,还是专门的Add?

【C++】模板初阶_第8张图片

通过反汇编得知,调用的是专属Add函数。得出结论:编译器在调用时,有现成的就调用现成的,没有就套用模板。当然,我们也有办法强制让编译器走模板函数,如下:

void Test()
{
    Add(1, 2); // 与非模板函数匹配,编译器不需要特化
    Add<int>(1, 2); // 调用编译器特化的Add版本
}
  • 原则2:对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
// 专门处理int的加法函数
int Add(int left, int right)
{
	return left + right;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
	return left + right;
}
void Test()
{
	Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
	Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
}
  • 原则3:模板函数不允许自动类型转换,但普通函数可以进行自动类型转换

3.类模板

3.1.类模板的定义格式

template<class T1, class T2, ..., class Tn>
class 类模板名
{
    // 类内成员定义
};

如下的栈示例:

//typedef int STDataType; //C语言的做法
template<class T>
class Stack
{
public:
	Stack(int capacity = 10)
	{
		_a = new T[capacity];
		_capacity = capacity;
		_top = 0;
	}
	~Stack()
	{
		delete[]_a;
		_capacity = _top = 0;
	}
private:
	T* _a;
	int _top;
	int _capacity;
};

3.2.类模板的实例化

类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的
类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。

int main()
{
	Stack<int>st1; //int类型
	Stack<double>st2;//double类型
    
    stack1 = stack2; //报错,不同类型,匹配不上
}

上述可以看出类模板是要显示实例化的,因为类模板没有推演时机,而函数模板是不需要自己显示实例化的,编译器会自动帮我推演(并不是所有都会推演,下文会讲)

他们是同一个类模版实例化出来的,但是模板参数不同,他们就是不同类型。

扩展

#define N  10
namespace bit
{
	template<class T>
	class array
	{
	public:
		T& operator[](size_t i)
		{
			assert(i < N);
			return _a[i];
		}
	private:
		T _a[N];
	};
}

int main()
{
	//int a2[10];
	//a2[20] = 0; 
	//a2[10];

	bit::array<int> a1;
	for (size_t i = 0; i < N; ++i)
	{
		// a1.operator[](i)= i;

		a1[i] = i;
	}

	for (size_t i = 0; i < N; ++i)
	{
		// a1.operator[](i)
		cout << a1[i] << " ";
	}
	cout << endl;

	for (size_t i = 0; i < N; ++i)
	{
		a1[i]++;
	}

	for (size_t i = 0; i < N; ++i)
	{
		cout << a1[i] << " ";
	}
	cout << endl;

	//a1[20];
	//a1[10];


	return 0;
}

这里的a1是自定义类型对象,但是我们却可以像使用数组一样。我们是怎么做到的呢?

这里我们用到了运算符重载。

而且他比起全局的数组有一个好处,全局的数组越界是抽查。但是这里我们用assert(i < N)就避免了这个问题。

但是他也有缺陷,就是效率低,因为需要重复调用。不过如果我们把他定义成内联,那效率就提高了。

库里面有一个array,这里我们加上命名空间。

4.补充

4.1.函数模板一定是推演?类模板一定是指定?

函数不一定都能推演,但是类模板一定要指定,假设有如下的函数模板:

【C++】模板初阶_第9张图片

这里的模板就推不出T的类型。因此我们就要对其显示实例化。

template<class T>
T* func(int n)
{ 
	return new T[n];
}
int main()
{
	//函数模板显示实例化
	int* p1 = func<int>(10);
	double* p2 = func<double>(10);
}

因此如果函数模板不能自动推演,就要显示实例化,指定模板参数。

4.2.模板的分离编译

模板的声明和定义是可以分离的(前提是声明和定义在一个文件)。像下面这样就可以:

//类模板定义
template<class T>
Stack<T>::Stack(int capacity)
{
	cout << "Stack(int capacity = )" << capacity << endl;

	_a = (T*)malloc(sizeof(T)*capacity);
	if (_a == nullptr)
	{
		perror("malloc fail");
		exit(-1);
	}

	_top = 0;
	_capacity = capacity;
}

template<class T>
Stack<T>::~Stack()
{
	cout << "~Stack()" << endl;

	free(_a);
	_a = nullptr;
	_top = _capacity = 0;
}

template<class T>
void Stack<T>::Push(const T& x)
{
	// ....
	// 扩容
	_a[_top++] = x;
}
//类模板声明
template<typename T>
class Stack
{
public:
	Stack(int capacity = 4);
	~Stack();
	void Push(const T& x);

private:
	T* _a;
	int _top;
	int _capacity;
};

模板不支持声明和定义放到两个文件中的(xxx.h和xxx.cpp),会出现链接错误。

image-20221027170440427

为什么不支持声明和定义分别放到两个文件呢?

这里跟实例化有关系。

根据我们已有的经验,源文件在生成可执行程序的过程会经历预处理、编译、汇编、链接这四大模块。我们画图演示器过程:

【C++】模板初阶_第10张图片

我template.i在编译后生成对应的.s文件以及后续的.o文件其实都是空的,编译器下不了手,因为不知道T是啥,这也就导致符号表是空的,没有地址。而调用的地方就没问题,因为main函数里头已经实例化显示出了T的类型。随后就去符号表里找到对应函数的地址,但是找不到。所以链接就会出错

那我非要声明和定义放两个文件,有何办法呢?

  • 解决办法1:在template.cpp中针对要使用的模板类型显示实例化指定

image-20221027172609868

你调用函数的地方有哪些类型,就要指定显示实例化哪些类型。加上了显示实例化,此时就能链接上了。 不过这种方法不实用,不推荐使用。因为如果你用double型的数据,这里又会报错。需要这样:

image-20221027172743405

  • 解决办法2:将声明和定义放到一个文件 “xxx.hpp” 里面或者xxx.h其实也是可以的。推荐使用这种。

模板的声明和定义一般是要放到一个文件中,有些地方就会命名成xxx.hpp,寓意就是头文件和定义实现内容合并一起,但并不一定是**.hpp**,.h也是可以的。

【C++】模板初阶_第11张图片

此时我头文件在预处理时在.cpp文件展开,根本不需要找函数地址,也不需要链接,因为既有声明又有定义。

你可能感兴趣的:(C++,c++,算法,开发语言)