目录
1.问题引入
函数重载
之前在CD4.【C++ Dev】函数重载文章中讲过,例如以下代码
void function(int val)
{
cout<<"int function(int val)"<<endl;
}
void function(int val,float f)
{
cout<<"void function(int val,float f)"<<endl;
}
设想一下,如果function函数有很多不同的重载类型,如果这样写会造成代码量上涨,代码复用率比较低,可维护性比较低
能不能用宏呢?
例如在95.【C语言】解析预处理(3)文章中提到过的批量定义函数:
#define TYPE_MAX(type) \
type type_max(type x,type y)\
{\
return x > y ? x : y; \
}
TYPE_MAX(int)
TYPE_MAX(float)
TYPE_MAX(double)
TYPE_MAX(char)
int main()
{
return 0;
}
看起来比较简洁,但宏有缺点:1.不能调试 2.写法容易出错,可能有优先级的问题 3.可能可读性较差
4.宏没有作用域的概念,只是简单的文本替换,并不检查类型,这可能导致类型错误或命名冲突
(宏的"副作用"的详细阐述参见94.【C语言】解析预处理(2))
5.宏不能递归,但模版可以
C++引入模版来解决
2.模版 template
定义
可以类比模具,优点:将数据类型作为参数传递,这样就不需要为不同的数据类型编写相同的代码
模版其实是有泛型编程的思想: 编写与类型无关的通用代码,是代码复用的一种手段
两种写法
template <typename T>//一般名称写成T或者U
template <class T>
//注:没有template <struct T>这样的写法,不能写成struct
区别
没有区别,在geeksforgeeks: templates-cpp网上是这么说的:
C++ adds two new keywords to support templates: ‘template’ and ‘typename’. The second keyword can always be replaced by the keyword ‘class’. It means that we can interchangeably use class and typename keywords.(可互换使用class和typename)
函数模版的代码示例:交换函数
template <typename T>(注意是尖括号,此数据类型T作用范围只有一个函数)
void my_swap(T& x, T& y)
{
T tmp = x;
x = y;
y = tmp;
}
测试以下代码:
int main()
{
int a = 1, b = 2;
my_swap(a, b);
char c = 'c', d = 'd';
my_swap(c, d);
double e = 1.5, f = 5.7;
my_swap(e, f);
return 0;
}
下断点到return 0,打开监视窗口查看各个变量的值:
成功交换,而且写法比C语言要简洁
当然也可以交换自定义类型:
#include <iostream>
using namespace std;
class Myclass
{
public:
Myclass(int val1, double val2, char val3)
:_val1(val1)
, _val2(val2)
, _val3(val3)
{ }
int _val1;
double _val2;
char _val3;
};
template <typename T>
void my_swap(T& x, T& y)
{
T tmp = x;
x = y;
y = tmp;
}
int main()
{
Myclass obj1(1, 3.14, 'a');
Myclass obj2(2, 4.13, 'b');
my_swap(obj1, obj2);
return 0;
}
下断点到return 0,打开监视窗口查看各个变量的值:
反汇编分析
交换函数的测试代码交换了3种类型(int、char和double)的变量,3次交换调用的是相同的函数吗?
VS转到反汇编:
发现三次call的函数的尖括号中填充的类型不同,而且call的地址不同(06E115Eh、06E10D2h和06E125Dh),所以调用的是不同的函数
解释调用的是不同的函数:模版的实例化,调用的不是模版,调用的是函数模版实例化生成的具体函数
可以这样画图理解:
(可以理解为将T替换成不同的类型)
因此编译器会针对不同的类型自动生成不同的函数,降低代码的冗余程度
数据类型T作用范围
数据类型T只会匹配一个函数或者一个类,有如下错误代码:
template <class T>
void function1(T a)
{
}
void function2(T a)
{
}
可以这样改:
template <class T>
void function1(T a)
{
}
template <class T>
void function2(T a)
{
}
和宏的区别
虽然模版会在编译时像宏一样展开,但是在编译前编译器会进行类型检查,这是宏没有的
无法推导的情况,类型不一样
例如以下代码编译会报错:
#include <iostream>
using namespace std;
template <typename T>
T my_add(T& x, T& y)
{
return x + y;
}
int main()
{
int a = 1;
double b = 3.14;
my_add(a, b);
return 0;
}
!模板函数不允许自动类型转换!
解决方法
方法1:手动强制类型转换,向一方妥协
根据不同的需求,有两种改法:
my_add(a, (int)b);
my_add((double)a, b);
但这样写仍然会报错:
复习临时类型转换
临时类型转换使用临时变量接收,且临时变量具有常性,如果使用(T& x, T& y)来接收参数,会导致权限的放大,应该两个参数都要加上const,况且my_add中没有改变x和y,应该加上const,提高代码的健壮性
T my_add(const T& x, const T& y)
{
return x + y;
}
方法2:显式实例化
根据不同的需求,也有两种改法:
my_add<int>(a, b);
my_add<double>(a, b);
即手动将T替换为具体的类型,也会有隐式类型转换,my_add的参数必须用const修饰
举例必须显式实例化的情况
template <typename T>
T* my_alloc(int num)
{
return new T[num];
}
为不同类型动态申请空间:
int* ptr1 = my_alloc<int>(5);//T替换为int
char* ptr1 = my_alloc<char>(5);//T替换为char
short* ptr1 = my_alloc<short>(5);//T替换为short
(如果不显式实例化,编译器不知道怎么动态申请空间)
例如这些也是显式实例化
vector<int>
queue<string>
注:显式实例化不能写在参数表中
模版也可以有多个参数
template <typename T, typename U>
void function(T& x, U& y)
{
cout << "x=" << x << " y=" << y << endl;
}
测试代码:
int main()
{
int a = 1, b = 2;
char c = 'c', d = 'd';
double e = 1.5, f = 5.7;
function(a, e);
function(c, b);
return 0;
}
运行结果:
3.类模板
使用模版来改造自定义类型的Mystack的代码示例
改造CD14.【C++ Dev】类和对象(5) 析构函数和拷贝构造函数文章的代码
#include <stdlib.h>
typedef int STDataType;
class MyStack
{
public:
MyStack(STDataType* a, int init_capacity)
{
a = (STDataType*)malloc(sizeof(STDataType) * init_capacity);
if (a == nullptr)
{
perror("malloc fail");
return;
}
capacity = init_capacity;
top = 0;
}
~MyStack()
{
//需要手动释放资源
free(a);
}
STDataType* a;
int top;
int capacity;
};
将typedef改成模版,改造后:
template <class T>
class MyStack
{
public:
MyStack(int init_capacity)
{
try
{
a = new T[init_capacity];
}
catch (const exception& e)
{
cout << e.what() << endl;
}
capacity = init_capacity;
top = 0;
}
~MyStack()
{
delete a;
a = nullptr;
top = capacity = 0;
}
T* a;
int top;
int capacity;
};
4.非模板函数和模版函数同名的情况处理
如果非模板函数和模版函数同名,且其他条件都相同,编译器的处理策略:1.会优先调用非模
板函数而不会将该模板实例化. 2.如果模板可以产生一个具有更好匹配的函数,那么将选择模板函数.
代码示例
#include <iostream>
using namespace std;
void function(int x)
{
cout << "function(int x)" <<endl;
}
template <typename T>
void function(T x)
{
cout << "void function(T x) " << endl;
}
int main()
{
function(10);
function(3.14);
return 0;
}
分析
分别在 function(10)和 function(3.14)下断点,转到反汇编
发现function(10)调用了非模版函数,而function(3.14)调用了模版示例化后的函数,因为比非模版函数更匹配
5.类名和类型
普通类
类名和类型相同,例如以下代码:
class Myclass{};
int main()
{
Myclass obj;
return 0;
}
obj的类名和类型都为Myclass
类模版
类名和类型不相同,例如以下成员函数的声明和定义分离的代码:
template <class T>//不能省略
class array
{
public:
array(size_t capacity);
};
template <class T>
array<T>::array(size_t capacity)
{
// 在这里可以添加构造函数的实现代码
}
注:第一行的template <class T>不能省略,如果下面的array<T>认为是类模版,而上面却没有说明是类模版,因此语法冲突:类声明缺少模版参数导致模版实现不匹配
array<T>::array(size_t capacity)中array<T>是类型,array是类名
又如:
vector<int> arr;
vector是类名,而vector<int>是类型,类模板名字不是真正的类,而实例化的结果才是真正的类
注:在有模版的类中,不建议声明和定义分离到两个文件中,在一个文件中声明和定义分离是可以的