目录
实现泛型的交换函数
我们实现一个对所有类型都通用的交换函数,可以用函数重载来实现。
cpp
void Swap(int& x, int& y)
{
int temp = x;
x = y;
y = temp;
}
void Swap(double& x, double& y)
{
double temp = x;
x = y;
y = temp;
}
void Swap(char& x, char& y)
{
char temp = x;
x = y;
y = temp;
}
//......
使用函数重载可以实现,但有几个缺点:
1.重载函数仅仅是类型不同,代码复用率低,当有新类型出现的时候,就又需要增加对应的函数。
2.代码的可维护性低,一个出错就可能导致所有的重载出错。
那我们能不能自己实现一个交换函数的模具,我们传递不同类型的值,就根据这个类型生成对应的交换函数?
就如同下面的浇筑过程。
答案是可以的,这就是C++中的模板 ,模板分为函数模板和类模板,模板又是泛型编程的基础。
泛型编程:编写与类型无关的通用代码,实现代码复用。
函数模板
1.概念
函数模板代表一个函数家族,函数模板与类型无关,在使用时被参数化,根据实参产生函数的特定类型版本。
2.格式
模板都需要用一个关键字template,使用格式如下:
cpp
//template<typename T1, typename T2, ...... , typename Tn>
template<class T1, class T2, ......, class Tn>
返回值类型 函数名 (参数列表哦){ }
**typename是用来定义模板参数的关键字,也可以使用class,**但是不能用struct代替。
用模板实现交换函数
cpp
template<class T>
void Swap(T& x, T& y)
{
T temp = x;
x = y;
y = temp;
}
具体例子
cpp
template<class T>
void Swap(T& x, T& y)
{
T temp = x;
x = y;
y = temp;
}
int main()
{
int i1 = 10;
int i2 = 50;
cout << "交换前i1、i2为:" << i1 << " " << i2 << endl;
Swap(i1, i2);
cout << "交换后i1、i2为:" << i1 << " " << i2 << endl;
cout << endl;
double d1 = 28.8;
double d2 = 67.6;
cout << "交换前d1、d2为:" << d1 << " " << d2 << endl;
Swap(d1, d2);
cout << "交换后d1、d2为:" << d1 << " " << d2 << endl;
cout << endl;
char ch1 = 'c';
char ch2 = 'x';
cout << "交换前ch1、ch2为:" << ch1 << " " << ch2 << endl;
Swap(ch1, ch2);
cout << "交换后ch1、ch2为:" << ch1 << " " << ch2 << endl;
cout << endl;
return 0;
}
3.原理
函数模板是一个蓝图,他本身并不是函数,是编译器产生特定具体类型函数的模具。
模板就是将本来我们做的重复的事情交给了编译器。
在编译器编译阶段 ,对于函数模板的使用,编译器根据传入的实参类型来生成对应类型的函数以供调用。比如:当double模板使用函数模板时,编译器通过实参的类型确定T为double,然后生成一个double类型的函数,对于其他类型也是如此。
4.函数模板实例化
不同类型的参数使用函数模板时,称为函数模板的实例化 。函数模板实例化分为:隐式实例化和显式实例化。
实例化出来的函数就是模板函数,模板函数的生成就是将函数模板的类型形参实例化的过程。
1.隐式实例化:编译器根据传入实参推算模板参数的实际类型
cpp
template<class T>
T Add(const T& x, const T& y)
{
return x + y;
}
int main()
{
int i1 = 20, i2 = 30;
cout << "i1 + i2 =" << Add(i1, i2) << endl;
cout << endl;
double d1 = 15.0, d2 = 20.0;
cout << "d1 + d2 =" << Add(d1, d2) << endl;
cout << endl;
return 0;
}
注意以下这种方式时不能通过编译的:
cpp
Add(i1, d1);
因为在编译阶段,编译器已经根据i1的类型推断T为int类型 ,而模板参数列表T只有一个 ,当编译器接收到d1时,发现时double类型,那编译器就会因为无法确定将T确定为int还是double而报错。
在模板中,编译器一般不会进行类型转换操作,如果转换出问题,那编译器就需要背黑锅。
这种报错有两种解决方式:1.我们自己强制转化 2. 显式实例化
cpp
//用户自己强制转化
Add(i1, (int)d1);
//显式实例化
Add<int> (i1, d1);
2.显式实例化:在函数名<>中指定模板参数的实际类型
cpp
int main()
{
int i1 = 10;
double d1 = 10.0;
//显式实例化
Add<int>(i1, d1);
return 0;
}
如果类型不匹配,编译器会尝试进行隐式类型转换,若不成功编译器就会报错。
5.函数模板参数的匹配原则
1.一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板可以实例化为这个非模板函数。
cpp
// 专门处理int的加法函数
int Add(int x, int y)
{
return x + y;
}
// 通用加法函数
template<class T>
T Add(T x, T y)
{
return x + y;
}
void Test()
{
Add(1, 2); // 与非模板函数匹配,编译器不需要生成对应类型的模板函数
Add<int>(1, 2); // 调用编译器特化的Add版本
}
2.对于非模板函数与同名的函数模板,在条件相同的情况下,在调用时会优先调用非模板函数而不会使用函数模板产生实例。如果函数模板可以实例化产生更加匹配的模板函数,那么会选择模板函数。
cpp
// 专门处理int的加法函数
int Add(int x, int y)
{
return x + y;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 x, T2 y)
{
return x + y;
}
void Test()
{
Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
}
3.模板函数不允许自动类型转换,但普通函数可以。
类模板
1.定义格式
cpp
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
例子
cpp
// 类模版
template<typename T>
class Stack
{
public:
Stack(size_t capacity = 4)
{
_array = new T[capacity];
_capacity = capacity;
_size = 0;
}
void Push(const T& data);
private:
T* _array;
size_t _capacity;
size_t _size;
};
// 模版不建议声明和定义分离到两个文件.h 和.cpp会出现链接错误。
template<class T>
void Stack<T>::Push(const T& data)
{
// 扩容
_array[_size] = data;
++_size;
}
int main()
{
Stack<int> st1; // int
Stack<double> st2; // double
return 0;
}
2.实例化
类模板的实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,并且将实例化的类型放中间。
类模板名字不是真正的类,实例化的结果才是真正的类。
cpp
// Stack是类名,Stack<int>才是类型
Stack<int> st1; // int
Stack<double> st2; // double
3.声明与定义问题
为什么在c++中,模板(函数)的声明和定义不建议分离到两个文件?
func.h 函数声明
func.cpp 函数定义
test.cpp 调用函数
报链接错误的直接原因就是链接时,符号表没有对应函数的地址。
1.代码开始编译的时候,首先就预处理,把头文件展开、宏替换、条件编译、去掉注释,.h和对应对的.cpp文件合在一起生成.i文件;
2.然后就到编译,根据语法树,检查语法,生成对应对的汇编代码,模板这时候问题就出在这,函数的.i文件,有声明有定义,没有具体类型,test.i中有函数的声明,有类型,但是没有定义,所以就不能生成具体的函数 ,符号表也就没有对应的地址,函数.i文件普通函数有声明有定义有类型,可以生成,这时test.i还是转换成汇编 call func(?),等着链接时把地址连接上,也没有报错,由.i文件生成.s文件;
3.编译完就到了汇编,汇编代码转换成二进制机械码,生成.obj文件;
4.链接时把目标文件合并在一起生成可执行程序,并把需要的函数地址等连接上。
解决方案:声明定义不分离;显式实例化模板。
拜拜,下期再见😏
摸鱼ing😴✨🎞