模板、函数模板、类模板
一、模板
泛型(Generic Programming)即是指具有在多种数据类型上皆可操作的含意。泛型编程的代表作品 STL 是一种高效、泛型、可交互操作的软件组件。
泛型编程最初诞生于 C++中,目的是为了实现 C++的 STL(标准模板库)。其语言支持机制就是模板(Templates)。模板的精神其实很简单:参数化类型。换句话说, 把一个原本特定于某个类型的算法或类当中的类型信息抽掉,抽出来做成模板参数 T。
所谓函数模板,实际上是建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表。这个通用函数就称为函数模板。
二、函数模板
1.语法格式
template 是语义是模板的意思,尖括号中先写关键字 typename 或是class,后面跟一个类型 T,此类即是虚拟的类型。至于为什么用 T,用的人多了,也就是 T 了。
2.函数模板的实例
调用过程是这样的,先将函数模板实在化为函数,然后再发生函数调用。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; template<class T> void MySwap(T& a, T& b) { T temp = a; a = b; b = temp; } int main(void) { int a = 10; int b = 20; //1.自动类型推导 cout << "a:" << a << ",b:" << b << endl;//a:10,b:20 MySwap(a, b);//编译器根据你传的值,进行类型自动推导 cout << "a:" << a << ",b:" << b << endl;//a:20,b:10 //2.显式的指定类型 MySwap<int>(a, b); cout << "a:" << a << ",b:" << b << endl;//a:10, b:20 double da = 10.01; double db = 20.02; cout << "da:" << da << ",db:" << db << endl;//da:10.01,db:20.02 MySwap(da, db);//编译器根据你传的值,进行类型自动推导 cout << "da:" << da << ",db:" << db << endl;//da:20.02,db:10.01 MySwap<double>(da, db); cout << "da:" << da << ",db:" << db << endl;//da:10.01,db:20.02 return 0; }
函数模板,只适用于函数的参数个数相同而类型不同,且函数体相同的情况。如果个数不同,则不能用函数模板。
3.函数模板与普通函数的区别
- 函数模板不允许自动类型转化
- 普通函数能够自动进行类型转化
4.函数模板和普通函数在一起调用规则
- 函数模板可以像普通函数那样被重载;
- C++编译器优先考虑普通函数;
- 如果函数模板可以产生一个更好的匹配,那么选择匹配;
- 可以通过空模板实参列表的语法限定编译器只能通过模板匹配。
5.编译器对模板机制剖析
编译器编译原理
总结:
(1)编译器并不是把函数模板处理成能够处理任意类的函数;
(2)编译器从函数模板通过具体类型产生不同的函数;
(3)编译器会对函数模板进行两次编译,在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。
函数模板案例——char和int类型数组排序
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; //对char类型和int类型数组进行排序 //打印函数 template<class T> void PrintArray(T* arr, int len) { for (int i = 0;i < len;i++) { cout << arr[i] << " "; } cout << endl; } //排序 template<class T> void MySort(T* arr, int len) { for (int i = 0;i < len;i++) { for (int j = i + 1;j < len;j++) { //从大到小排序 if (arr[i] < arr[j]) { T temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } } } int main(void) { int arr[] = { 2,6,1,8,9,2 }; int len = sizeof(arr) / sizeof(int); cout << "排序前:"; PrintArray(arr,len);//排序前:2 6 1 8 9 2 MySort(arr, len); cout << "排序后:"; PrintArray(arr, len);//排序后:9 8 6 2 2 1 char chArr[] = { 'a','c','q','d','t', }; len = sizeof(chArr) / sizeof(char); cout << "排序前:"; PrintArray(chArr, len);//排序前:a c q d t MySort(chArr, len); cout << "排序后:"; PrintArray(chArr, len);//排序后:t q d c a return 0; }
三、类模板
1.类模板定义
类模板与函数模板的定义和使用类似,有时,有两个或多个类,其功能是相同的,仅仅是数据类型不同,所以将类中的类型进行泛化。
类模板示例:
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; template<class T> class Person { public: Person(T id, T age) { this->m_id = id; this->m_age = age; } void Show() { cout << "id:" << this->m_id << ",age:" << this->m_age << endl; } public: T m_id; T m_age; }; void test() { //函数模板在调用的时候,可以自动类型推导 //类模板必须显式指定类型 Person<int> p(10, 20); p.Show(); } int main(void) { test();//id:10,age:20 return 0; }
2.类模板派生普通类、类模板派生类模板
#include <iostream> using namespace std; template<class T> class Person { public: Person(T age) { this->age = age; } private: T age; }; //模板类派生普通类 //为什么继承的类型需显式,而不是T? //原因:类去定义对象,这个对象需要编译分配内存,所以要在 //public Person<int>这里显式的指定类型,可以知道给父类分配多少内存 class SubPerson1 :public Person<int> { public: SubPerson1(int age, int id) :Person<int>(age) { this->id = id; } private: int id; }; //模板类派生模板类 template<class T> class SubPerson2 :public Person<T> { public: SubPerson2(T age, T id) :Person<T>(age) { this->id = id; } private: int id; };
3、类模板实现
(1)类模板类内实现
(2)类模板类外实现(在一个.cpp中)
模板类不要轻易使用友元函数
(3)类模板类外实现(在.h和.cpp中)
由于二次编译,模板类在.h在第一次编译之后,并没有最终确定类的具体实现,只是编译器的词法校验和分析。在第二次确定类的具体实现后,是在.hpp文件生成的最后的具体类,所以main函数需要引入.hpp文件。
引入hpp文件一说也是曲线救国之计,所以实现模板方法建议在同一个文件.h中完成。
(4)类模板中的static
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; template<class T> class Person { public: static T a; }; //类外初始化 template<class T> T Person<T>::a = 0; int main(void) { Person<int> p1, p2, p3; Person<char> pp1, pp2, pp3; p1.a = 10; pp1.a = 'c'; cout << p1.a << " " << p2.a << " " << p3.a << endl;//10 10 10 cout << pp1.a << " " << pp2.a << " " << pp3.a << endl;//c c c
//通过以上结果,说明p1,p2,p3是属于Person<int>家族的,他们共享Person<int>::a; //pp1,pp2,pp3是属于Person<char>家族的,他们共享Person<char>::a; return 0; }
练习:
设计一个数组模板类(MyArray),完成对int、char类型元素的管理。
需要实现 构造函数 拷贝构造函数 [] 重载=操作符。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; template<class T> class MyArray { public: MyArray(int capacity) { this->mCapacity = capacity; this->mSize = 0; //申请内存 this->pAddr = new T[this->mCapacity]; } MyArray(const MyArray<T>& arr) { this->mCapacity = arr.mCapacity; this->mSize = arr.mSize; //申请内存空间 this->pAddr = new T[this->mCapacity]; //数据拷贝 for (int i = 0; i < this->mSize;i++) { this->pAddr[i] = arr.pAddr[i]; } } T& operator[](int index) { return this->pAddr[index]; } MyArray<T> operator=(const MyArray<T>& arr) { if (this->pAddr != NULL) { delete[] this->pAddr; } this->mCapacity = arr.mCapacity; this->mSize = arr.mSize; //申请内存空间 this->pAddr = new T[this->mCapacity]; //数据拷贝 for (int i = 0; i < this->mSize;i++) { this->pAddr[i] = arr.pAddr[i]; } return *this; } void PushBack(T& data) { //判断容器中是否有位置 if (this->mSize >= this->mCapacity) { return; } //调用拷贝构造 =操作符 //1.对象元素必须能够被拷贝 //2.容器都是值寓意,而非引用寓意 向容器中放入元素,都是放入的元素的拷贝 //3.如果元素的成员有指针,注意深拷贝和浅拷贝问题 this->pAddr[this->mSize] = data; this->mSize ++ ; } //T&& 对右值取引用 void PushBack(T&& data) { //判断容器中是否有位置 if (this->mSize >= this->mCapacity) { return; } this->pAddr[this->mSize] = data; this->mSize++; } ~MyArray() { if (this->pAddr != NULL) { delete[] this->pAddr; } } public: //一共可以容下多少元素 int mCapacity; //当前数组有多少元素 int mSize; //保存数据的首地址 T* pAddr; }; class Person { }; void test2() { Person p1, p2; MyArray<Person> arr(10); arr.PushBack(p1); arr.PushBack(p2); } void test1() { MyArray<int> marray(20); int a = 10, b = 20, c = 30, d = 40; marray.PushBack(a); marray.PushBack(b); marray.PushBack(c); marray.PushBack(d); //不能对右值取引用 //左值 可以在多行使用 //临时变量 只能当前行使用 marray.PushBack(100); marray.PushBack(200); marray.PushBack(300); for (int i = 0;i < marray.mSize;i++) { cout << marray[i] << " "; } cout << endl; } int main(void) { test1(); return 0; }