C++——模板

目录

1. 泛型编程

2. 函数模板

2.1 函数模板格式

2.3 函数模板的原理

2.4 函数模板的实例化

2.4.1 隐式实例化:让编译器根据实参推演参数类型

2.4.2 显式实例化:在模板函数名后加< >,并在尖括号里指定模板参数类型

2.5模板参数的匹配原则

3.类模板

3.1定义和实例化类模板

3.2类模板定义和声明分离定义


1. 泛型编程

如何实现Swap呢?

void Swap(int& left, int& right)
{
	int tmp = left;
	left = right;
	right = tmp;
}

void Swap(double & left, double& right)
{
	double tmp = left;
	left = right;
	right = tmp;

}

//再有各种各样的类想交换,都要再写函数
//.....过于麻烦了吧


int main()
{
	int a = 1, b = 2;
	Swap(a,b);

	double c = 1.1, d = 2.2;
	Swap(c, d);

	return 0;
}

虽然使用函数重载可以是现实,但是每次有新的类型都要写一个新的函数。且代码可维护性低,一个出错可能所有重载都出错。

所以引入模板,让编译器根据不同类型利用改模板生成代码。

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

模板运行时不检查数据类型,也不保证类型安全。

2. 函数模板

2.1 函数模板格式

template<typename T1,typename T2,typename T3.....,typename Tn>//定义一个虚拟类型T,名字可以随便起。
//或者:template<class T>//用class和typename都行
注意:typename 是用来定义模板参数的关键字, 也可以用class。
例:写一个模板Swap
#include<iostream>
using namespace std;

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

int main()
{
	char a = 'a', b = 'b';
	Swap(a, b);

	int c = 1, d = 2;
	Swap(c,d);
	
	double e = 1.1, f = 2.2;
	Swap(e, f);

	return 0;
}

2.3 函数模板的原理

上面不同类型的Swap调的都是模板函数吗?

不是。参数类型不同->开辟栈帧大小就不同。模板只是一个蓝图,并不是函数。实际上调用的是编译器根据模板生成的函数。

模板函数的实例化:用函数模板 实例化具体的我们想要的函数。让编译器根据实参推演模板参数的实际类型。

int类型的实参,用int替代T,生成一份处理int类型的代码。

实参是int类型,实例化一次。char类型实例化一次。double类型实例化一次。

实参类型相同的函数只会实例化一次该类型的函数,调用同一函数。

例:

    int c = 1, d = 2;
	Swap(c,d);
	int x = 1, y = 2;
	Swap(x, y);//都是int,不用实例化两次。调用的也是同一个函数。

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

int main()
{
	int a = 1;
	double b = 1.1;
	Swap(a, b);
	return 0;
}

报错:模板参数T不明确,可能是double可能是int。

为什么不会发生类型转换,把int换成double?

提升或截断的隐式类型转换发生在赋值或传参时,但是这里报错在根据实参推演函数参数类型,还没实例化成功,也没有到传参调用那一步。

在下面说明解决方法。

2.4 函数模板的实例化

2.4.1 隐式实例化:让编译器根据实参推演参数类型

template<typename T>

T Add(const T& left, const T& right)//这里加const的原因是我们的例子会涉及隐式类型转换。(类型转换会产生中间变量,具有常性。引用不能权限放大,要加const)
{
	return left + right;
}


int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.1, d2 = 10.3;

	cout << Add(a1, a2);
	cout << Add(d1, d2);

	//cout << Add(a1, d1)<<endl;//int+double 加不了哈哈


    //解决方法一:把实参直接强转。方法二:显式实例化
	cout << Add((double)a1, d1) << endl;
	cout << Add(a1, (int)d1) << endl;

	return 0;
}

2.4.2 显式实例化:在模板函数名后加< >,并在尖括号里指定模板参数类型

	cout << Add<double>(a1, d1) << endl;
	cout << Add<int>(a1, d1) << endl;//隐式类型转换

告诉编译器,不用推演了,按我们的想法实例化。

也可以定义多个模板参数解决上面问题。

template<typename T1,typename T2>

T1 Add(const T1& left, const T2& right)
{
	return left + right;
}

int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.1, d2 = 10.3;


	cout << Add(a1, d1)<<endl;//int+double,函数返回值类型是T1,所以输出整数类型	cout << Add(d2, a1) << endl;


	return 0;
}

2.5模板参数的匹配原则

1.普通函数和同名的模板函数可以同时存在,且被实例化出来的函数可以和非模板函数一样。

template <typename T>
T Add(T left, T right)
{
	cout << "T" << endl;
	return left + right;
}

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

int main()
{
	int a = 1, b = 2;
	Add(a, b);//优先调非模板参数,编译器也懒得自己推了

	Add<int>(a, b);//这样子就调模板函数,被实例化出来的函数可以和非模板函数一样

	return 0;
}

由此可知,由模板函数实例化出的函数的函数名修饰规则和普通函数不一样。

2.如果普通函数和模板函数同时存在,能不用函数模板就不用。调普通函数参数类型不匹配就会用函数模板实例化一个函数。

template <typename T1,typename T2>
T1 Add(T1 left, T2 right)
{
	return left + right;
}


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

int main()
{
	int a = 1,c=2;
	double b = 1.2;

	Add(a, c);//与非模板函数 参数类型完全匹配,不实例化模板函数
	Add(a, b);//调非模板函数还要隐式类型转换,所以编译器决定实例化模板函数,生成类型更匹配的函数。

	return 0;
}

3.模板函数不允许自动类型转换,普通函数可以自动类型转换

3.类模板

3.1定义和实例化类模板

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

// 类模板函数在类外定义时,需要加模板参数列表
template <class T>
Vector<T>::~Vector()
{
if(_pData)
delete[] _pData;
_size = _capacity = 0;
}

// Vector是类名,Vector<int>才是类型
Vector<int> s1;
Vector<double> s2;

类模板中的成员函数全是模板函数。

3.2类模板定义和声明分离定义

拿栈距举例:我们用 typedef 来解决可维护性,想要int的栈就 typedef int STDataType;

想要double的栈就typedef double STDataType;

但当我们想要两个栈,一个int,一个double就很麻烦了。可以写两个类(class StackInt和class StackDouble),不用typedef。或者使用类模板。

typedef增强的是可维护性,让我们尽量少的改动并复用代码,不是泛型编程。泛型编程要求编写和类型无关的代码。

栈:

template<typename T>
class Stack
{
public:
	Stack(int capacity = 4)
	{
		cout << "Stack(int capacity = )" <<capacity<<endl;

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

		_top = 0;
		_capacity = capacity;
	}
	
	~Stack()
	{
		cout << "~Stack()" << endl;

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

	void Push(const T& x)//【用引用更好】因为T也有可能是自定义类型,不用传值传参(效率低,还会调拷贝构造)
	{
		// ....
		// 扩容
		_a[_top++] = x;
	}

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

int main()
{
	//类模板一般没有推演的时机,函数模板实参传递形参,推演模板参数
	//类模板显示实例化

	Stack <double> st1;
	st1.Push(1.1);

	Stack <int>st2;
	st2.Push(1);

	//st1和st2是同一个类模板实例化出来的,但是模板参数不同,就是不同的类型
	return 0;
}

array:

#define N  10

template<class T>

class Array//不能用小写array,会和库里面的冲突。或者执意不想改名字,可以用命名空间。

{
public:

	//毕竟是函数调用,会建立栈帧影响效率。但用上inline以后基本不会影响效率了
	inline T& operator[](size_t i)//引用返回,可以改变实体
	{
		assert(i < N);//加了assert强制检查,越界就一定会报错
		return _a[i];
	}
private:
	T _a[N];
};

int main()
{
	//int a2[10];
	//a2[10] = 0;//越界了,能检查出来
	//a2[20] = 0;//没检查出来,因为编译器对静态数组越界访问的检查是抽查的
	//
	//a2[10];//越界读检查不出来,写还是抽查,只读检查不出来越界

    Array<int> a1;
	for (size_t i = 0; i < N; ++i)
	{

		a1[i] = i;
	}

	for (size_t i = 0; i < N; ++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;
}

模板类的成员函数不支持定义和声明分离。

Stack.h(写函数声明)  Stack.cpp(定义Stack类) Test.cpp(创建Stack对象并调用函数)

.h 会在两个.cpp展开,然后就没有.h了, 预处理变更成.i

|

预处理

Stack.i   Test.i        

|

编译(生成汇编指令)

Stack.s   Test.s

语法问题会在编译阶段报错

Stack.s里面都是函数定义和声明。Test.

s里面有函数声明,编译就可以通过,生成call指令。但是找不到函数地址。

|

汇编(翻译成机器能看懂的二进制代码)

Stack.o   Test.o

|   .o文件合并

链接(生成可执行程序)

xxx.exe / a.out

链接的时候找函数地址。在Stack.o的符号表里找函数地址。

为什么模板定义和声明分离会报链接错误?

Stack里定义函数,模板要实例化出来才能有函数地址,而Stack里没实例化,就没有函数地址,没法进符号表里。Test里在定义Stack对象的时候显示实例化,但是只有声明,没有定义,也没有实例化出函数。链接的时候Test文件内部没有函数定义,去Stack.o的符号表里找函数也找不到。

解决方法一(不常用):定义和声明分离,在声明里显式实例化。

缺点是换个类型的栈还要重新实例化,还是不算泛型编程。

Stack.cpp文件

#include"Stack.h"

template<typename T>//每次都要再声明一下模板参数
Stack<T>::Stack(int capacity)//Stack <int>st2; 模板类的类型是Stack<T>
{
	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<typename T>

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

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


template<typename T>
void Stack<T>::Push(const T& x)//【用引用更好】因为T也有可能是自定义类型,不用传值传参(效率低,还会调拷贝构造)
{
	// ....
	// 扩容
	_a[_top++] = x;
}


template
class Stack<int>;//显式实例化

Stack.h文件

#include<iostream>
using namespace std;

template<typename T>
class Stack
{
public:
	Stack(int capacity = 4);


	~Stack();
	

	void Push(const T& x);//【用引用更好】因为T也有可能是自定义类型,不用传值传参(效率低,还会调拷贝构造)


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

解决方法二:声明和定义分离,并放在同一个.h文件。

不需要链接了,当.h展开的时候,既有声明又有定义,不用再去链接找函数地址了,直接就是知道函数地址,展开后和main函数在一起,显示实例化的时候把定义也实例化了。

那为啥模板类的成员函数不直接定义在类里面呢?

为了可读性。方便能看到都有什么方法。

using namespace std;
#include<iostream>
template<typename T>
class Stack
{
public:
	Stack(int capacity = 4);


	~Stack();


	void Push(const T& x);//【用引用更好】因为T也有可能是自定义类型,不用传值传参(效率低,还会调拷贝构造)


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


template<typename T>//每次都要再声明一下模板参数
Stack<T>::Stack(int capacity)//Stack <int>st2; 模板类的类型是Stack<T>
{
	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<typename T>

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

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


template<typename T>
void Stack<T>::Push(const T& x)//【用引用更好】因为T也有可能是自定义类型,不用传值传参(效率低,还会调拷贝构造)
{
	// ....
	// 扩容
	_a[_top++] = x;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值