泛型编程
如何实现一个通用的交换函数呢?
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
void Swap(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}
C++支持函数重载,因此无论是交换double类型还是int类型的变量的值,可以统一使用同一个函数名。
使用函数重载虽然可以实现,但是有以下几个不好的地方:
- 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数。
- 代码的可维护性比较低,一个出错可能所有的重载均出错。
很显然这些函数只有类型不同,那么我们能否写出一个模型,让编译器根据不同的类型利用模板来生成不同的函数呢?
就像下面的图一样,我们只需要制作模具,后面的事情就简单了许多。
![image-20220723160806842](https://cdn.jsdelivr.net/gh/sxfinn/CDN/img/202212021631018.png)
如果在C++中,也能够存在这样一个模具,通过给这个模具中填充不同材料(类型),来获得不同材料的铸件(即生成具体类型的代码),那将会节省许多时间。巧的是前人早已将树栽好,我们只需在此乘凉。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
函数模板
概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
格式
template<typename T1, typename T2,......,typename Tn>
return_type 函数名(参数列表){}template<typename T> void Swap(T& left, T& right) { T temp = left; left = right; right = temp; }
注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)
原理
就像机器人一样,我们将重复简单的事情交给他去做,它可以代替我们完成这些,为我们节省了大量时间和精力。
函数模板就是一个蓝图,它本身并不是一个函数,是编译器使用特定方式产生的具体类型函数的膜具,所以其实模板就是将本来应该我们做的重复的事情交给了编译器。
![image-20220729103947049](https://cdn.jsdelivr.net/gh/sxfinn/CDN/img/202212021632906.png)
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。
比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型及其他类型也是如此。
函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化,有两种方式:
- 隐式实例化:让编译器根据实参推演模板参数的实际类型;
- 显式实例化:在函数名后的<>中指定模板参数的实际类型;
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); Add(d1, d2); /* Add(a1, d1); 该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型 通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有 一个T, 编译器无法确定此处到底该将T确定为int 或者 double类型而报错 注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅 */ // 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化 Add(a1, (int)d1); Add<int>(a1, d1); return 0; }
模板函数的隐式实例化不允许自动类型转换(只能按照实参的类型去推导),但普通函数和显式实例化可以进行自动类型转换。
显式实例化的方式可以让模板参数有一个确定的类型,并且指定的调用使用该类型实例化出的函数,不再需要实参去推导类型,这样就和调用普通函数一样,参数类型不匹配时,会尝试类型转换操作,如果无法转换成功编译器将会报错。
如何证明模板函数会实例化呢?来看看上面的代码第10、11行的汇编:
可以看到,表面上好像无论实参类型是int还是double我们都是调用的Add函数,但事实上它们调用的是实例化出的函数Add<int>
和Add<double>
,它们是不同的函数。
模板参数的匹配原则
- 一个非模板函数可以和一个同名的模板函数同时存在,而且该函数模板还可以被实例化为这个非模板函数功能完全相同的一个函数。
- 对于非模板函数和同名函数模板,如果其他条件(参数类型)都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板,也可以强行调用显式实例化出的函数。
int Add(int left, int right) { cout << "int Add(int left, int right)" << endl; return left + right; } template<typename T> T Add(T left, T right) { cout << "T Add(T left, T right)" << endl; return +right; } int main() { Add(1, 2);//与非模板函数匹配,不需要将模板函数实例化,直接调用 Add<int>(2, 3);//调用显式实例化出的函数Add<int> Add(3, 4);//与非模板函数匹配,不需要将模板函数实例化,直接调用 Add(1.0,2.0);//没有完全匹配的函数存在,模板函数可以生成类型更加匹配的版本,编译器根据实参类型推导生成更加匹配的Add函数 return 0; }
输出:
int Add(int left, int right)
T Add(T left, T right)
int Add(int left, int right)
T Add(T left, T right)
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数,但两者是不同的函数,它们有不同的函数地址
int Add(int left, int right) { cout << "int Add(int left, int right)" << endl; return left + right; } template<typename T> T Add(T left, T right) { cout << "T Add(T left, T right)" << endl; return +right; } int main() { int (*p1)(int, int) = Add; int (*p2)(int, int) = Add<int>; cout << p1 << endl; cout << p2 << endl; return 0; }
输出:
00007FF793BD1217
00007FF793BD142E
模板函数和非模板函数的冲突问题
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
template<typename T>
T Add(T left, T right)
{
cout << "T Add(T left, T right)" << endl;
return +right;
}
int main()
{
Add(1, 2);//与非模板函数匹配,不需要将模板函数实例化,直接调用
Add<int>(2, 3);//调用显式实例化出的函数Add<int>
return 0;
}
前面我们已经知道Add(1, 2);
和Add<int>(2, 3);
调用的不是同一个函数,那么这样为什么不会有符号表的冲突呢?
猜测:非模板函数的符号表存放的是Add
,而模板函数实例化出的函数符号表存放的是Add<int>
,因此不会发生符号表冲突。
隐式实例化过程(Add(1, 2);
)实际上是:
- 调用
Add
时,去符号表里找函数Add
,若未找到则执行第3步。 - 若找到了,接着检查类型是否匹配:如果匹配则直接用函数地址调用此函数。若不匹配则执行第3步;
- 寻找是否有名为Add的模板函数;
- 若没有找到相应的模板函数则报错。若找到了执行第5步;
- 查找此函数模板实例化出的函数中是否有与之类型匹配的函数;
- 若有,则调用此函数。若没有则尝试实例化出相应类型匹配的函数
Add<int>
,执行第7步; - 实例化出的函数
Add<int>
会进入符号表,然后调用Add<int>
函数;
显式实例化过程(Add<int>(2, 3);
)实际上是:
- 调用
Add<int>
时,去符号表找Add<int>
; - 若找到了则直接调用此函数。若未找到则执行第3步;
- 寻找名为Add的模板函数;
- 使用模板函数实例化出相应函数;
- 实例化出的函数
Add<int>
会进入符号表,然后调用Add<int>
函数;
类模板
类模板的定义格式
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
// 动态顺序表
// 注意:Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具
template<class T>
class Vector
{
public:
Vector(size_t capacity = 10)
: _pData(new T[capacity])
, _size(0)
, _capacity(capacity)
{}
// 使用析构函数演示:在类中声明,在类外定义。
~Vector();
void PushBack(const T& data);
void PopBack();
// ...
size_t Size() { return _size; }
T& operator[](size_t pos)
{
assert(pos < _size);
return _pData[pos];
}
private:
T* _pData;
size_t _size;
size_t _capacity;
};
// 注意:类模板中函数放在类外进行定义时,需要加模板参数列表
template <class T>
Vector<T>::~Vector()
{
if (_pData)
delete[] _pData;
_size = _capacity = 0;
}
类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
// Vector类名,Vector<int>才是类型
Vector<int> v1;//用Vector<int>这个类型定义出对象v1
Vector<char> v2;//用Vector<double>这个类型定义出对象v2
汇编如下:
模板参数
template<typename T1, typename T2…>中被<>括起来的就叫做模板参数,一个template<>只能对一个函数或者一个类生效,也就是一个template<>中模板参数只能在代码下方离他最近的一个函数或者类中使用。
模板参数与函数参数非常相似,不过模板参数既可以是类型又可以是值,而函数参数只能是值。
类型模板参数
类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称,可以有缺省,缺省的规则和函数参数相同。
template<typename T1, typename T2 = char>//半缺省
class S
{
public:
S()
:a(0)
,b(0)
{}
private:
T1 a;
T2 b;
};
int main()
{
S<int, int> s1;
S<int> s2;
return 0;
}
模板参数和函数参数一样,缺省参数(默认实参)只能在模板参数列表的结尾。
非类型模板参数
非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
namespace sx
{
template<class T, size_t N = 10>
class array
{
public:
T& operator[](size_t index) { return _array[index]; }
const T& operator[](size_t index)const { return _array[index]; }
size_t size()const { return _size; }
bool empty()const { return 0 == _size; }
private:
T _array[N];
size_t _size;
};
}
int main()
{
sx::array<int> arr1;
sx::array<char, 15> arr2;
return 0;
}
注意:
- 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
- 非类型的模板参数必须在编译期就能确认结果(常量值)。
多重模板参数
template<typename T1 , typename T2 >
class S
{
public:
S()
:a(0)
,b(0)
{}
~S();
template<typename K>
void test(K k);
private:
T1 a;
T2 b;
};
template<typename T1, typename T2 >
S<T1, T2>::~S()
{}
template<typename T1, typename T2 >
template<typename K>
void S<T1, T2>::test(K k)
{
cout << k << endl;
}
int main()
{
S<int, int> s1;
s1.test<double>(1.1);
s1.test(1);
return 0;
}
模板类和模板函数区别
模板类的实例化方式:
- 显式实例化
模板函数的实例化方式:
- 显式实例化
- 隐式实例化
通常模板函数我们使用隐式的实例化就足够了,但有些特殊情况,例如函数没有参数或者函数参数的类型并不是模板参数,无法通过实参去推导出类型,就需要显式地去实例化函数。而类的实例化只有显式实例化一种方式。
template<typename T>
void print(int n)
{
T t = 0;
for (int i = 0; i < n; i++)
{
cout << t << endl;
}
}
int main()
{
print<int>(10);
return 0;
}
标签:13,初阶,函数,int,C++,Add,实例,类型,模板
From: https://www.cnblogs.com/ncphoton/p/16950525.html