目录
这里是oldking呐呐,感谢阅读口牙!先赞后看,养成习惯!
个人主页:oldking呐呐
专栏主页:深入CPP语法口牙
11 C++的模板(前篇)
11.1 什么是模板
-
模板是分割C和C++的重要的分水线,决定了C语言注定不能取代C++
-
设计模板一般咱称作为泛型编程,泛型编程即通过数据类型编写代码,使得同一段代码能够不断复用,提高了代码的复用性
-
模板一般分为
- 函数模板
- 类模板
11.2 函数模板
- 假设我们需要写很多函数重载,如以下场景
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
void Swap(double& x, double& y)
{
double tmp = x;
x = y;
y = tmp;
}
//...
- 虽然函数重载避免了函数重名的问题,但开发起来还是很麻烦,还是要手动一个个挫,所以,模板横空出世!
template<class T>//模板的开头
void Swap(T& x, T& y)//T是模板内的类型,未来会被替换成其他类型
{
T tmp = x; //其他地方像写正常函数一样写就行
x = y;
y = tmp;
}
- 模板就是一个蓝图,蓝图上有一些细节内容(即需要被替换的类型)需要手动编辑,大体框架不变
- 即,此时我想让这个函数变成交换
int
类型的函数,只要替换掉模板里面占位的类型就行,我们称替换掉占位的过程称为"XX类型推演",比方在这个例子中,就是"int
类型推演" - 此时这个函数就构建完了,就已经能够正常调用了
int main()
{
int x = 1, y = 2;
double m = 1.1, n = 2.2;
cout << x << " " << y << endl;
Swap(x, y);
cout << x << " " << y << endl;
cout << m << " " << n << endl;
Swap(m, n);
cout << m << " " << n << endl;
return 0;
}
- 咱如果此时打开反汇编
- 不难发现,这俩函数实际在调用的时候根本就不是同一个函数
11.2.1 函数模板的格式
//函数模板一般就是template后加尖括号,尖括号里放变量类型
//C++从C98开始支持关键字 typename
//以下把class替换成 typename 也可以,两者使用没有差异
//类型可以不止一个,可以有很多很多个
template<class Type1, class Type2>
void func1(const Type1& a, Type2* pb)
{
//...
}
int main()
{
int x = 1;
double m = 2;
//调用上,按咱写的模板传值就行了
//编译器会自定识别传进去的参数的类型
func1(x, &m);
return 0;
}
- 其实也不难发现,函数模板其实像是生成函数的函数,区别在于,函数括号里放的是形参,传进来的是具体的参数,模板括号里放的是类型,传进来的是类型
11.2.2 模板实例化
-
我们写出一个模板出来,编译器实际使用了这个模板并且生成了一个函数出来,我们就称这个过程为模板实例化,而实例化又有两种分类
-
对于上面写的
Swap()
函数,因为类型只有一个,所以调用的时候两个参数的类型必须要相同
-
现在传了两种类型的参数进来,编译器不知道应该用哪个类型,所以报错
-
但如果我们一定要传两个不同类型的值进去,为了解决这个问题,我们可以试图这么办
-
(以下是错误代码!!!)
template<class T>
void Swap(const T& x, const T& y)
{
T tmp = x;
x = y;
y = tmp;
}
int main()
{
int x = 1;
double m = 2;
Swap((double)x, m);
return 0;
}
- 咱们传不了,强转嘛,但强转也会伴随着问题,强转之后生成的对象是临时对象,临时对象具有常性,所以要把模板里函数的参数加上
const
,但加上const
又会面临不能修改的问题,就很头疼
-
我们一般称这种为了适配模板而被迫强转形参类型,然后通过模板成功生成函数的过程称为推导实例化(通过模板生成函数依旧是编译器自动执行生成的)
-
所以说,推导实例化一般用在只读的场景,比方说以下场景
template<class T>
T Add(const T& x, const T& y)
{
return (x + y);
}
int main()
{
int x = 1;
double m = 1.1;
cout << Add((double)x, m) << endl;
return 0;
}
- 还有一种实例化方式,我们还是拿上面
Add()
的例子
int main()
{
int x = 1;
double m = 1.1;
cout << Add<int>(x, m) << endl;
return 0;
}
-
这种实例化方式也是可以的
-
我们之前说,模板是通过判断传进来的参数类型来生成函数的,在
Add()
的例子中,编译器因为传进来两个不同类型的参数,不能判断应该用哪个类型生成函数,于是我们可以手动指定编译器生成指定的函数,就是以上的方式 -
在尖括号中像填参数一样填入指定的类型,如果有多个类型就填多个类型,编译器会根据你填的参数来生成函数
-
通过尖括号手动填参数指定编译器生成函数的操作称为显示实例化
-
如果实在不想使用显式实例化或者推导实例化,也可以直接在写模板的时候写两个类型,这样生成函数的时候就不会判断有没有冲突了
-
有些时候,是必须要用显式实例化的,例如以下这个模板
template<class T>
T Func1(int x)
{
return (T)x;
}
- 在这个模板里,编译器没法根据传进来的参数判断
T
的类型是什么,如果直接调用就会报错
- 所以此时一定要显式实例化
int main()
{
int x = 1;
Func1<double>(x);
return 0;
}
11.2.3 模板与函数同名
- 如以下情景
int Add(const int& x, const int& y)
{
cout << "int Add(const int& x, const int& y)" << endl;
return (x + y);
}
template<class T>
T Add(const T& x, const T& y)
{
cout << "T Add(const T& x, const T& y)" << endl;
return (x + y);
}
int main()
{
int x = 1, y = 2;
double m = 1.1, n = 2.2;
cout << Add(x, y) << endl; //调用函数
cout << Add(m, n) << endl; //用模板生成函数
return 0;
}
- 有现成的函数,编译器就会调现成的函数,没有现成的函数,编译器就会用模板生成
11.2.4 函数模板的意义
-
函数重载,是为了让函数调用起来更加多元,更加灵活,各个重载之间大体方向差不多,或者说功能类似,咱用函数重载
-
但模板就不太一样,两个不同参的函数调用之间的差别仅有参数类型不同,函数功能,执行方向完全一样
-
可以说灵活度提升,意味着开发效率,代码量的增加,要代码量少,开发效率快,就必然要在灵活程度上做取舍(建立在同等效率,稳定性相同的情况下)
11.3 类模板
11.3.1 类模板书写格式
- 类模板和函数模板书写方式差不多,比方这里我就写了一个Stack模板
template<class T>
class Stack
{
public:
Stack(int n = 4)
:_arr(new T[n])
,_size(0)
,_capa(n)
{}
~Stack()
{
delete[] _arr;
_arr = nullptr;
_size = 0;
_capa = 0;
}
bool isEmpty()
{
return _size == 0 ? true : false;
}
void Push(const T& x)
{
if (_size == _capa)//CPP里扩容需要手动扩容
{
T* tmp = new T[_capa * 2];
memcpy(tmp, _arr, _capa * 2 * sizeof(T));
delete[] _arr;
_arr = tmp;
_capa *= 2;
}
_arr[_size++] = x;
}
const T& Top()
{
if (isEmpty())
{
cout << "Stack is empty!" << endl;
}
return _arr[_size - 1];
}
void Pop()
{
if (!isEmpty())
{
_size--;
}
else
{
cout << "Stack is empty!" << endl;
return;
}
}
//...
private:
T* _arr;
int _capa;
int _size;
};
int main()
{
Stack<int> st1; //类模板只能显式实例化
Stack<double> st2;
//Stack<int>是一个类型,Stack<double>是另一个类型,这俩不是同一个类型
st1.Push(1);
st1.Push(2);
st1.Push(3);
st1.Push(4);
cout << st1.Top() << endl;
st1.Pop();
cout << st1.Top() << endl;
st2.Push(1.1);
st2.Push(2.2);
st2.Push(3.3);
st2.Push(4.4);
cout << st2.Top() << endl;
st2.Pop();
cout << st2.Top() << endl;
st2.Pop();
st2.Pop();
st2.Pop();
cout << st2.Top() << endl;
return 0;
}
- 类模板只能显示实例化,因为编译器不知道该怎么判断你要的类型,干脆就你自己手动传类型进来吧
11.3.1 类模板内成员函数的声明和定义的拆分
template<class T>
class Stack
{
public:
Stack(int n = 4)
:_arr(new T[n])
,_size(0)
,_capa(n)
{}
~Stack()
{
delete[] _arr;
_arr = nullptr;
_size = 0;
_capa = 0;
}
bool isEmpty()
{
return _size == 0 ? true : false;
}
void Push(const T& x);
const T& Top()
{
if (isEmpty())
{
cout << "Stack is empty!" << endl;
}
return _arr[_size - 1];
}
void Pop()
{
if (!isEmpty())
{
_size--;
}
else
{
cout << "Stack is empty!" << endl;
return;
}
}
//...
private:
T* _arr;
int _capa;
int _size;
};
template<class T>
void Stack<T>::Push(const T& x) //尖括号里要注明Stack的T类型和这个T类型是同一个类型(话句话说,既然这里的T和Stack的T不是同一个T,那我换成别的什么X啥的都是没问题的,当然这种换名字的操作肯定不建议哈)
{
if (_size == _capa)
{
T* tmp = new T[_capa * 2];
memcpy(tmp, _arr, _capa * 2 * sizeof(T));
delete[] _arr;
_arr = tmp;
_capa *= 2;
}
_arr[_size++] = x;
}
-
一个模板声明,其声明的类型只能在一个函数或者类里用,所以函数声明和定义分离只能另声明模板
-
模板不支持声明和定义分离到两个文件,这么写会报链接错误