第一章:泛型编程
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++中,也能够存在这样一个模具,通过给这个模具中填充不同材料(类型),来获得不同材料的铸件(即生成具体类型的代码)
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
第二章:函数模板
2.1 函数模板概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
2.2 函数模板格式
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表){}
//template <class T>
//template <typename T> //单参数
//template <typename T1, typename T2>//多参数。模版参数定义的是类型
//注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)
//编译器用模版实例化生成对应的Swap函数
template <typename T>
void Swap(T& left, T& right) {
T temp = left;
left = right;
right = temp;
}
int main() {
int i = 0, j = 1;
Swap(i, j);
double x = 1.1, y = 2.2;
Swap(x, y);
//这两个Swap调用的并不是同一个函数
return 0;
}
注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)
2.3 函数模板的原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
2.4 函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
template<class T>
T Add(const T& left, const T& right) {
return left + right;
}
int main() {
//1. 隐式实例化:让编译器根据实参推演模板参数的实际类型
cout << Add(1, 2) << endl;
cout << Add(1.1, 2.2) << endl;
//cout << Add(1.1, 2) << endl;//报错
//该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
//通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,
//编译器无法确定此处到底该将T确定为int 或者 double类型而报错
//注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅
//此时有两种处理方式:1.用户自己来强制转化 2. 使用显式实例化
//解决方案一:用户自己来强制转化
cout << Add((int)1.1, 2) << endl;
cout << Add(1.1, (double)2) << endl;
//2. 显式实例化:在函数名后的<>中指定模板参数的实际类型
//解决方案二:显示实例化
cout << Add<int>(1.1, 2) << endl;
cout << Add<double>(1.1, 2) << endl;
return 0;
}
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
为什么要有显示实例化?
//下方示例没有显示实例化,编译器无法推导类型
template <class T>
T* func(int n) {
return new T[n];
}
int main() {
//无法调用,无法推导。10只是传递给n,用于指定数组的大小。
//但是,T 是什么类型,编译器无从得知,因为 n 和 T 没有直接关联。
//可能会想:ptr 是 int*,那 func(10) 不是返回 int* 吗?为什么不能反推 T=int 呢?
//原因:C++ 的模板推导只根据函数参数进行,不能根据返回值进行推导。
//编译器的工作方式:
//1.编译阶段:当遇到 func(10),编译器需要先确定 T 是什么类型,然后实例化 func<T>。
//2.但是 10 无法提供 T 的信息,编译器只能看到 func(n),而 n 只是 int,跟 T 没有直接关系。
//3.返回值类型在调用时才起作用,但在模板推导阶段,C++ 不会反向推导 T。
//int* ptr = func(10);
int* ptr = func<int>(10);
return 0;
}
2.5 模板参数的匹配原则
1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
template<class T>
T Add(T left, T right) {
return left + right;
}
int Add(int a, int b) {
return a + b;
}
int main() {
int x = 0, y = 1;
swap(x, y);//C++库提供了交换函数模版
double a = 1.1, b = 2.2;
swap(a, b);
cout << Add(x, y) << endl;//调用的第二个函数,与非模板函数匹配,编译器不需要特化
cout << Add<int>(x, y) << endl;//指定调用模板,调用编译器特化的Add版本
return 0;
}
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.1 类模板的定义格式
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
栈示例
typedef int DataType;
class Stack {
public:
Stack(size_t capacity = 4) {
cout << "Stack()" << endl;
_array = new DataType[capacity];
_capacity = capacity;
_size = 0;
}
void Push(DataType data) {
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack() {
cout << "~Stack()" << endl;
delete[] _array;
_array = nullptr;
_size = _capacity = 0;
}
private:
// 内置类型
DataType* _array;
int _capacity;
int _size;
};
int main() {
//typedef虽然可以解决栈类换数据类型,
//但如果下方需要st1存int,st2存double就不能很好解决
//因为又会回到类似交换函数的问题,只是更改typedef的类型和栈的名字。其余部分都相同
Stack st1;//int
Stack st2;//double
return 0;
}
解决方法:使用类模板
3.2 类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
template <class T>
class Stack {
public:
Stack(size_t capacity = 4) {
cout << "Stack()" << endl;
_array = new T[capacity];
_capacity = capacity;
_size = 0;
}
void Push(const T& data) { //待插入的数据不需要修改,且传引用增加效率
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack() {
cout << "~Stack()" << endl;
delete[] _array;
_array = nullptr;
_size = _capacity = 0;
}
private:
// 内置类型
T* _array;
int _capacity;
int _size;
};
int main() {
//typedef虽然可以解决栈类换数据类型,
//但如果下方需要st1存int,st2存double就不能很好解决
//因为又会回到类似交换函数的问题,只是更改typedef的类型和栈的名字。其余部分都相同
//Stack st1;//int
//Stack st2;//double
//这里编译器无法推断类型,所以需要显示实例化
Stack<int> st1;
Stack<double> st2;
//上方依然是2个类
st1.Push(1);
st1.Push(2);
st2.Push(1.1);
st2.Push(2.2);
return 0;
}
动态顺序表
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;
};
//之前的类名就是类型,但类模版的类名不是类型
//类模版 类名:Vector
//类模版 类型:Vector<T>
//类上方的template可以用的范围是类里面,所以声明和定义分离还要在单独声明模版参数
template<class T>
Vector<T>::~Vector() {
delete[] _pData;
_pData = mullptr;
}
int main() {
Vector<int> v;
return 0;
}
作业
1. 下列描述错误的是( )
A.编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础
B.函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具
C.模板分为函数模板和类模板
D. 模板类跟普通类以一样的,编译器对它的处理时一样的
答案:D
A.模板是代码复用的重要手段
B.函数模板不是一个具体函数,而是一个函数家族
C.目前涉及到的模板就两类,函数模板与类模板
D.模板类是一个家族,编译器的处理会分别进行两次编译,其处理过程跟普通类不一样
2. 下列的模板声明中,其中几个是正确的( )
1)template
2)template<T1,T2>
3)template<class T1,T2>
4)template<class T1,class T2>
5)template<typename T1,T2>
6)template<typename T1,typename T2>
7)template<class T1,typename T2>
8)<typename T1,class T2>
A.2
B.3
C.4
D.5
答案:B
A.1.模板语法错误,2.没有关键字class或typename指定类型,3.T2缺少class或typename
B.正确, 4,6,7为正确声明
C.5.T2缺少class或typename
D.8.缺少template
3. 下列关于模板的说法正确的是( )
A.模板的实参在任何时候都可以省略
B.类模板与模板类所指的是同一概念
C.类模板的参数必须是虚拟类型的
D.类模板中的成员函数全是模板函数
答案:D
A. 模板实参省略意思为隐式实例化,一般情况下都使用隐式实例化,不需指定模板类型参数,让编译器进行推导,但有些情况下编译器推导时可能会有歧义,比如:模板参数只有一个类型T,但是用两个不同类型隐式实例化
template<class T>
T Add(const T& x, const T& y) {
return x + y;
}
int main() {
Add(10, 20); // 正确
Add(1.2, 30); // 编译失败, 改正 Add<int>(1.2, 20) 或者 Add((int)1.2, 20)
return 0;
}
所以模板参数不是任何情况下都可以省略,要结合具体的使用场景,因此A的说法是错误的。
B.类模板是一个类家族,模板类是通过类模板实例化的具体类
C.C++中类模板的参数即为模板参数列表中内容,有两种方式:类型参数和非类型参数
类型参数:即类型参数化,将来实例化为具体的实际类型,有点像函数的形参,形参可以接受不同值的实参
非类型参数:在定义时给定了具体的类型,用该类型定义的为常量,比如:
template<class T, size_t N>
class array {
// ...
};
D.正确,定义时都必须通过完整的模板语法进行定义。 因为所有类模板的成员函数,放在类外定义时,需要在函数名前加类名,而类名实际ClassName<T>,所以定义时还需加模板参数列表
template<class T>
size_t Stack<T>::size() {
return _size;
}
因此类模板中的成员函数都是函数模板,D正确。
4. 在下列对fun的调用中,错误的是( )
template <class T>
T fun(T x, T y) {
return x * x + y * y;
}
A.fun(1, 2)
B.fun(1.0, 2)
C.fun(2.0, 1.0)
D.fun<float>(1, 2.0)
答案:B
A.通过参数推导,T为int,不存在二义性,调用正确
B.由于参数类型不一样,模板不支持类型转换,推导参数会产生二义性,编译错误
C.通过参数推导,T为float,不存在二义性,调用正确
D.通过类型实例化函数,调用正确
5. 下面有关C++中为什么用模板类的原因,描述错误的是? ( )
A.可用来创建动态增长和减小的数据结构
B.它是类型无关的,因此具有很高的可复用性
C.它运行时检查数据类型,保证了类型安全
D.它是平台无关的,可移植性
答案:C
A.模板可以具有非类型参数,用于指定大小,可以根据指定的大小创建动态结构
B.模板最重要的一点就是类型无关,提高了代码复用性
C.模板运行时不检查数据类型,也不保证类型安全,相当于类型的宏替换,故错误
D.只要支持模板语法,模板的代码就是可移植的