前言
模板是C++中的重大板块,是使C++真正超越C语言的工具,在C++模板没有设计出来之前其实C++是没有那么被行业和社会所认可的,本节我们将初步了解C++中的模板(仅作大致讲解,具体的细枝末节将会再过几节讲解),那么废话不多说,我们正式进入今天的学习
1.泛型编程
在学习模板之前,假设我们需要写一个 Swap 函数,此时我们就会用C++中的函数重载来实现,将其重载三次,如下面的代码所示:
void Swap(int& left, int& right)
{int temp = left;left = right;right = temp;
}void Swap(char& left, char& right)
{char temp = left;left = right;right = temp;
}void Swap(double& left, double& right)
{double temp = left;left = right;right = temp;
}
用函数重载来完成Swap函数的确是可以完成任务,但是这样做会有两个不足点:
1. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数
2. 代码的可维护性比较低,一个出错可能所有的重载均出错
那能不能创建一个“模子”,让编译器根据不同的类型利用这个“模子”来生成代码呢?
所以C++中创建了一个模板的概念,用来把这些相似度极高且功能相同的函数整合起来。通过给这个模具中填充不同材料(类型),来获得不同材料的铸件(即生成具体类型的代码)进而实现泛型编程
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
下面我们来看看模板的语法:
模板的关键字是:template,具体使用方法如下:
template<class T>
void Swap(T& left, T& right)
{T temp = left;left = right;right = temp;
}
函数模板是一个模具,它本身并不是一个函数,是编译器使用特定的方式产生特定的类型函数的模具,在本质上模板就是将本来应该由我们来完成的代码编写交给了编译器完成,不同的类型调用的并不是同一个函数,而是编译器在我们调用的时候自动生成的,没有减少代码量
模板分为函数模板和类模板,下面我们来一一分析:
2.函数模板
2.1函数模板的概念
函数模板代表了一个函数蓝图,函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本
函数模板的格式:template<typename T1, typename T2,……,typename Tn>,其中关键字typename也可以使用class,二者在使用上没有任何的区别。如果有多个参数的话就要用逗号去分离。
**************************************************************************************************************
拓展讲一点,我们在日常编写代码的时候经常会遇到交换变量的情况,每次都自己写swap函数就会很麻烦。所以C++官方库中就加入了swap函数
我们一般不需要去包含这个头文件,因为在大部分情况下这个头文件都会被间接包含
**************************************************************************************************************
2.2 函数模板的实例化
刚才我们说到,如果要在模板中有多个参数就需要用逗号去分离,那么在什么情况下我们需要用到多个参数呢?我们来举一个例子:
假设我们在只有一个模板参数的条件下写Add函数:
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, d1);Add(a2, d2);
}
该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型。通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T, 编译器无法确定此处到底该将T确定为int还是double类型所以报错了
要想在保持一个参数的前提下解决这个问题有两种办法:
1.推导实例化(使用强制类型转换)
int main()
{int a1 = 10, a2 = 20;double d1 = 10.0, d2 = 20.0;Add(a1, a2);Add(d1, d2);cout << Add(a1, (int)d1) << endl;cout << Add((double)a1, d1) << endl;}
使用强制类型转换在本质上解决的是推导冲突的问题,但是在某些情况下强制类型转换可能会导致结果出现错误和偏差
2.显示实例化
int main()
{int a1 = 10, a2 = 20;double d1 = 10.0, d2 = 20.0;Add(a1, a2);Add(d1, d2);cout << Add<int>(a1, d1) << endl;cout << Add<double>(a1, d1) << endl;}
显示实例化在某些情况也可能会导致结果出现错误和偏差
我们在遇到这种情况时最好是传两个模板参数
template<class T1, class T2>
T1 Add(const T1& left, const T2& right)
{return left + right;
}
int main()
{int a1 = 10, a2 = 20;double d1 = 10.0, d2 = 20.0;Add(a1, a2);Add(d1, d2);cout << Add(a1, d1) << endl;cout << Add(a1, d1) << endl;}
我们之前说过,T的类型是根据实参的类型推导出来的,我们用一个例子来解释一下:
template<class T>
T* func1(int n)
{return new T[n];
}
int main(void)
{func1(0);return 0;
}
在这种情况下编译会失败,因为实参已经给了一个明确的类型,T此时就不能推导出类型出来,这就验证了“T是根据实参类型推导出来的”这一结论
此时要想让代码正常运行就需要运用刚才学习过的显示实例化:
int main(void)
{double* p1 = func1<double>(0);return 0;
}
2.3 函数模板的匹配机制
假设我们编写了一个与模板函数同名的具体函数,那么在具体函数和模板函数同时存在的情况下在主函数中调用函数调用的是哪一个呢?
template<class T>
T Add(const T& left, const T& right)
{cout << "T Add" << endl;return left + right;
}int Add(const int& x, const int& y)
{cout << "int Add" << endl;return x + y;
}int main(void)
{int a1 = 10, a2 = 20;Add(a1, a2);return 0;
}
我们可以看到在这种情况下,优先走了具体的函数。因为模板函数并不是现有的,还要通过模拟器自己推演生成。而在具体函数存在的情况下,编译器会优先使用现有的函数,不会去做“二次加工”
3. 类模板
3.1 类模板的相关概念
类模板的定义格式与之前的函数模板基本相同:
template<class T1, class T2 …… class Tn>
class 类模板名字
{类内成员定义
};
那么类模板在使用上有什么好处呢?我们在C语言中学习了typedef,在大部分的情况下好像也能够完成模板的功能,我们来用栈的代码来举一个例子
我们用C++的风格写出栈的代码如下:
如果我们要改成C++的模式写代码,我们需要注意:C++中没有realloc及和realloc功能类似的函数,如果要实现扩容的话就需要自己写扩容的代码(C++不设置扩容函数是有原因的,具体原因在学vector的时候我们就会知道)
template<typename T>
class Stack
{
public:Stack(size_t capacity = 4):_array(new T[n]),_size(0),__capacity(n){}~Stack(){delete[] _array;_array = nullptr;_size = _capacity = 0;}void Push(const T& x);private:T* _array;size_t _capacity;size_t _size;
};
// 模版不建议声明和定义分离到两个文件.h 和.cpp会出现链接错误,具体原因后面会讲
template<class T>
void Stack<T>::Push(const T& x)
{if (_size == _capacity){T* tmp = new T[_capacity * 2];memcpy(tmp, _array, sizeof(T) * size);delete[] _array;_array = tmp;_capacity *= 2;}_array[_size++] = x;
}
int main()
{Stack<int> st1; // intStack<double> st2; // doublereturn 0;
}
我们需要注意:
1.定义的模板参数只能给当前的函数或者类域使用,每一个函数模板或者类模板都要定义自己的模板参数,出了函数或者类域以后当前的模板参数就无法使用。
2.模版不建议声明和定义分离到两个文件.h 和.cpp,会出现链接错误,具体原因后面会讲
我们通过代码可以知道:我们有了模板以后,可以实现一个栈存int类型的数据,一个栈存double类型的数据,而C语言中的栈就只能存一种类型的数据,要想在C语言中实现写两种类型的栈的话就需要将原代码再复制粘贴一份,再解决命名和类型的问题。
3.2 类模板的实例化
类模板实例化与函数模板实例化不同,类模板都是显示实例化。类模板实例化需要在类模板名字后跟< >,然后将实例化的类型放在< >中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
// Stack是类名,Stack<int>才是类型
Stack<int> st1; // intStack<double> st2; // double
结尾
本节我们对模板进行了初步的了解,在C++中利用模板实现泛型编程有很多优越之处。那么本节的内容就到此结束了,希望可以给您带来帮助,谢谢您的浏览!!!