- 模板
- 函数模板
- 普通函数与函数模板的区别
- 普通函数与函数模板的调用规则
- 模板的局限性
- 类模板
- 类模板对象做函数参数
- 类模板与继承
- 类模板成员函数类外实现
- 类模板分文件编写
- 类模板与友元
- 类模板案例
本阶段主要针对 C++ 泛型编程和 STL 技术做详细讲解
模板模板就是建立通用的模具,大大提高复用性
模板的特点
- 模板不能被直接使用,它只是一个框架
- 模板的通用并不是万能的
C++ 另一种编程思想称为泛型编程,主要利用的技术就是模板
C++ 提供二种模板机制:函数模板和类模板
函数模板语法及其作用
》》函数模板语法
template// 函数声明或定义 // 使用函数模板 // 函数名<具体的数据类型>(实参);
解释:template 声明创建模板;typename 表明其后面的符号是一种数据类型,可以用 class 代替;T 通用的数据类型,名称可以代替,通常为大写字母
》》关于函数模板的作用,用一个具体的实例加以演示
#includeusing namespace std; // 用于任意数据类型二个数的加法模板 template T Add(T num1, T num2) { return (num1 + num2); } int main() { // 1. 用于二个整型数据的加法运算 cout << Add (1, 2) << endl; // 3 // 2. 用于二个浮点数据的加法运算 cout << Add (1.2, 4.5); // 5.7 system("pause"); return 0; }
函数模板作用:建立一个通用函数,其函数返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代替
函数模板注意事项
- 自动类型推导,必须推导出一致的数据类型 T,才可以使用
- 模板必须要确定出 T 的数据类型才可以使用
解释:第一条注意事项表明了一个 T 只能推导出一种数据类型;第二条注意事项表明了模板中的 T 必须在函数体的实现中看得出其数据类型,否则不可以使用
#includeusing namespace std; // 用于任意数据类型二个数的加法模板 template T Add(T num1, T num2) { return (num1 + num2); } template void func() { cout << "无法使用的模板!" << endl; } int main() { // 1. 自动推导类型必须推导出一致的数据类型才可以使用 cout << Add(1, 2) << endl; // 3 // cout << Add(1, 3.4) << endl; // Error! // 2. 模板必须确定出 T 的数据类型才可以使用 // func(); // Error! system("pause"); return 0; }
函数模板案例
- 利用函数模板封装一个排序的函数,可以对不同数据类型数组进行排序
- 排序规则:从小到大;排序算法:选择排序
- 分别利用 char 数组和 int 数组进行测试
#include普通函数与函数模板的区别using namespace std; // 交换二个数的模板 template void Swap(T &a, T &b) { T temp = a; a = b; b = temp; } // 打印数组模板 template void PrintArray(T arr[], int length) { for (int i = 0; i < length; i++) { cout << arr[i] << " "; } cout << endl; } // 排序模板 template void Sort(T arr[], int length) { // 选择排序 for (int i = 0; i < length; i++) { int min = i; // 记录最小值的索引 for (int j = i + 1; j < length; j++) { if (arr[min] > arr[j]) { min = j; } } if (min != i) { // 交换 arr[min] 和 arr[i] 元素 Swap(arr[min], arr[i]); } } } int main() { // 1. 测试 int 类型的数组 int arr_int[5] = { 1,3,4,2,5 }; Sort (arr_int, 5); // 排序后输出 PrintArray(arr_int, 5); // 1 2 3 4 5 // 2. 测试 char 类型的数组 char arr_ch[5] = { 'a','c','b','d','e' }; Sort (arr_ch, 5); // 排序后输出 PrintArray(arr_ch, 5); // a b c d e system("pause"); return 0; }
区别:普通函数调用时可以发生自动类型转换(隐式类型转换);函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换;如果利用指定类型的方式则可以发生自动类型转换
#includeusing namespace std; // 普通函数 int Add1(int a, int b) { return (a + b); } // 函数模板 template T Add2(T a, T b) { return (a + b); } int main() { // 1. 普通函数实现二个数的加法 cout << Add1(1, 3) << endl; // 4 cout << Add1(1, 'a') << endl; // 98 -- 发生类型转换 // 2. 函数模板实现二个数的加法 cout << Add2(2, 3) << endl; // 5 cout << Add2(2.2, 3.5) << endl; // 5.7 // cout << Add2(2, 'a') << endl; // Error! -- 函数模板自动类型推导不会发生隐式转换 cout << Add2 (2, 'a') << endl; // 99 system("pause"); return 0; }
总结:建议使用显示指定数据类型的方式,调用函数模板时,可以自己确定通用类型 T
普通函数与函数模板的调用规则调用规则
- 如果同时存在普通函数和函数模板,则优先调用普通函数
- 可以通过空模板参数列表来强调调用函数模板
- 函数模板也可以发生重载
- 如果函数模板可以产生更好的匹配,则优先调用函数模板
#include模板的局限性using namespace std; // 普通函数 void func(int a, int b) { cout << "普通函数调用,和为:" << a + b << endl; } // 函数模板 template void func(T a, T b) { cout << "函数模板调用,和为:" << a + b << endl; } // 函数模板重载 template void func(T a, T b, T c) { cout << "函数模板调用,和为:" << a + b + c << endl; } int main() { // 1. 当普通函数和函数模板同时存在时 -- 优先调用普通函数 func(1, 2); // 普通函数调用,和为:3 // 2. 利用空模板参数列表强制调用函数模板 func<>(1, 3); // 函数模板调用,和为:4 // 3. 函数模板可以发生重载 func(1, 3, 5); // 函数模板调用,和为:9 // 4. 函数模板可以产生更好的匹配则优先调用 // 调用函数模板时只需要将通用参数 T 看成 char 即可;而调用普通函数则会发生隐式类型转换(较麻烦) func('a', 'c'); // 函数模板调用,和为:196 system("pause"); return 0; }
示例 1:求二个数据和的模板
#includeusing namespace std; template void Add(T a, T b) { cout << a + b << endl; } int main() { // 1. 求二个整型数据的和 Add (1, 3); // 4 // 2. 求二个数组的和 // Add({ 1,2,3 }, { 4,5,6 }); // Error! system("pause"); return 0; }
模板并不是万能的,有些特定数据类型需要用具体化的方法做特殊实现
优化上述模板:使其功能为求二个数组所有元素的总和并输出
#includeusing namespace std; template void Add_Arr(T *a, T *b, int len1, int len2) { // 总和 int sum = 0; for (int i = 0; i < len1; i++) { sum += a[i]; } for (int j = 0; j < len2; j++) { sum += b[j]; } // 输出总和 cout << "sum = " << sum << endl; } int main() { int arr1[] = { 1,2,3 }; int arr2[] = { 4,5,6,7 }; // arr1 数组的长度 int len_arr1 = sizeof(arr1) / sizeof(arr1[0]); // 3 // arr2 数组的长度 int len_arr2 = sizeof(arr2) / sizeof(arr2[0]); // 4 Add_Arr(arr1, arr2, len_arr1, len_arr2); // sum = 28 system("pause"); return 0; }
实例 2:比较二组数据是否相等的模板
#include类模板using namespace std; #include class Person { public: // 构造函数初始化 Person(string name, int age) { Name_ = name; Age_ = age; } public: string Name_; int Age_; }; template bool Compare(T a, T b) { if (a == b) { return true; } else { return false; } } // 利用具体化的 Person 版本实现代码,具体化优先调用 template<> bool Compare(Person a, Person b) { if (a.Name_ == b.Name_ && a.Age_ == b.Age_) { return true; } else { return false; } } int main() { // 1. 比较二个整型数据是否相等 cout << Compare(1, 2) << endl; // 0 -- false cout << Compare(1, 1) << endl; // 1 -- true // 2. 比较二组对象是否相等 Person p1("苏苏", 19); Person p2("涵涵", 18); cout << Compare(p1, p2) << endl; // 0 -- false cout << Compare(p1, p1) << endl; // 1 -- true system("pause"); return 0; }
作用
建立一个通用类,类中的成员数据类型可以不具体制定,用一个虚拟的类型代表
程序示例
#includeusing namespace std; #include template class Person { public: // 构造函数初始化 Person(N name, A age) { this->Name_ = name; this->Age_ = age; } public: N Name_; A Age_; }; int main() { Person p1("Su", 19); cout << p1.Name_ << endl; // Su system("pause"); return 0; }
总结:类模板需要指定参数列表
类模板和函数模板的区别
- 类模板没有自动类型推导的使用方式
- 类模板在模板参数列表中可以有默认参数
解释:第一条在上述代码中已经实现;第二条表明可以将通用类型指定为 typename T = 数据类型;
#includeusing namespace std; #include template class Person { public: // 构造函数初始化 Person(N name, A age) { this->Name_ = name; this->Age_ = age; } public: N Name_; A Age_; }; int main() { Person p1("Su", 19); cout << p1.Name_ << endl; // Su system("pause"); return 0; }
类模板中成员函数创建时机
- 普通类中的成员函数一开始就可以创建
- 类模板中的成员函数在调用时才创建
#include类模板对象做函数参数using namespace std; #include class Person1 { public: void ShowPerson1() { cout << "我是 Person1" << endl; } }; class Person2 { public: void ShowPerson2() { cout << "我是 Person2" << endl; } }; template class MyPerson { public: T obj; public: void func1() { obj.ShowPerson1(); } void func2() { obj.ShowPerson2(); } }; int main() { MyPerson p1; p1.func1(); // 我是 Person1 // p1.func2(); // Error! MyPerson p2; // p2.func1(); // Error! p2.func2(); // 我是 Person2 system("pause"); return 0; }
三种传参方式
- 指定传入的类型 – 直接显示对象的数据类型
- 参数模板化 – 将对象中的参数变成模板进行传递
- 整个类模板化 – 将这个对象类型模板化进行传递
#include类模板与继承using namespace std; #include template class Person { public: // 构造函数初始化 Person(T1 name, T2 age) { this->Name_ = name; this->Age_ = age; } // 显示学生信息的函数 void ShowPerson() { cout << "姓名:" << Name_ << " 年龄:" << Age_ << endl; } public: T1 Name_; T2 Age_; }; void printPerson1(Person &p) { p.ShowPerson(); } template void printPerson2(Person &p) { p.ShowPerson(); } template void printPerson3(T &p) { p.ShowPerson(); } int main() { // 1. 指定传入类型 Person p1("Su", 19); printPerson1(p1); // 姓名:Su 年龄:19 // 2. 参数模板化 Person p2("Du", 18); printPerson2(p2); // 姓名:Du 年龄:18 // 3. 整个类模板化 Person p3("Qu", 8); printPerson3(p3); // 姓名:Qu 年龄:8 system("pause"); return 0; }
注意事项
- 当子类继承的父类是一个类模板时,子类在声明的时候要指定出父类中 T 的类型(如果不指定,编译器无法给子类分配内存)
- 如果想要灵活的指定出父类中 T 的类型,子类也需要变为类模板
#include类模板成员函数类外实现using namespace std; #include template class base { public: T Id_; }; // class Son :public base { // 错误!必须知道父类中 T 的数据类型,才能继承给子类 // class Son :public base { // 指定父类中 T 的数据类型 template class Son :public base { // 子类也变成模板 public: Son(T1 score) { this->Score_ = score; } void printMessage() { cout << "成绩:" << Score_ << endl; } public: T1 Score_; }; int main() { Son s(100); s.printMessage(); // 成绩:100 system("pause"); return 0; }
#includeusing namespace std; #include template class Person { public: // 构造函数初始化 Person(T1 name, T2 age); // 显示学生信息的函数 void ShowPerson(); public: T1 Name_; T2 Age_; }; template Person ::Person(T1 name, T2 age) { // Person :: 代表类模板作用域,Person:: 代表普通类作用域 this->Name_ = name; this->Age_ = age; } template void Person ::ShowPerson() { cout << "姓名:" << Name_ << " 年龄:" << Age_ << endl; } int main() { Person p("Su", 19); p.ShowPerson(); // 姓名:Su 年龄:19 system("pause"); return 0; }
总结:类模板中成员函数类外实现时需要加上模板参数列表 template
问题:类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到
person.h
#pragma once #includeusing namespace std; template class Person { public: // 构造函数初始化 Person(T1 name, T2 age); // 显示学生信息的函数 void ShowPerson(); public: T1 Name_; T2 Age_; };
person.cpp
#include "person.h" templatePerson ::Person(T1 name, T2 age) { // Person :: 代表类模板作用域,Person:: 代表普通类作用域 this->Name_ = name; this->Age_ = age; } template void Person ::ShowPerson() { cout << "姓名:" << Name_ << " 年龄:" << Age_ << endl; }
main.cpp
#includeusing namespace std; #include #include "person.h" int main() { Person p("Su", 19); p.ShowPerson(); // 姓名:Su 年龄:19 system("pause"); return 0; }
如上代码将运行出错!解决办法如下
第一种:修改 main.cpp 中的包含文件
#includeusing namespace std; #include #include "person.cpp" int main() { Person p("Su", 19); p.ShowPerson(); // 姓名:Su 年龄:19 system("pause"); return 0; }
第一种:将 person.h 和 person.cpp 中的内容写到一个文件中(后缀为 .hpp)
person.hpp
#pragma once #includeusing namespace std; template class Person { public: // 构造函数初始化 Person(T1 name, T2 age); // 显示学生信息的函数 void ShowPerson(); public: T1 Name_; T2 Age_; }; template Person ::Person(T1 name, T2 age) { // Person :: 代表类模板作用域,Person:: 代表普通类作用域 this->Name_ = name; this->Age_ = age; } template void Person ::ShowPerson() { cout << "姓名:" << Name_ << " 年龄:" << Age_ << endl; }
main.cpp
#include类模板与友元using namespace std; #include #include "person.hpp" int main() { Person p("Su", 19); p.ShowPerson(); // 姓名:Su 年龄:19 system("pause"); return 0; }
学习目标:掌握类模板配合友元函数的类内和类外实现
》》全局函数类内实现:直接在类内声明友元即可
》》全局函数类外实现:需要提前让编译器知道全局函数的存在
#include类模板案例using namespace std; #include // 提前声明 Person 类 template class Person; // 提前让编译器知道全局函数的存在 template void printMessage_2(Person &p) { cout << "姓名:" << p.Name_ << " 年龄:" << p.Age_ << endl; } template class Person { // 1. 全局函数类内实现 friend void printMessage_1(Person &p) { cout << "姓名:" << p.Name_ << " 年龄:" << p.Age_ << endl; } // 2. 全局函数类外实现 // 2.1 空参数列表使其为 “类模板函数” // 2.2 提前声明全局函数 friend void printMessage_2<>(Person &p); public: // 构造函数初始化 Person(T1 name, T2 age) { Name_ = name; Age_ = age; } private: // 私有成员属性 T1 Name_; T2 Age_; }; int main() { // 1. 利用全局函数类内实现打印信息 Person p1("Su", 19); printMessage_1(p1); // 姓名:Su 年龄:19 // 2. 利用全局函数类外实现打印信息 Person p2("Ku", 9); printMessage_2(p2); // 姓名:Ku 年龄:9 system("pause"); return 0; }
案例描述:实现一个通用的数组类,要求如下
- 可以对内置数据类型以及自定义数据类型的数据进行存储
- 将数组中的数据存储到堆区
- 构造函数中可以传入数组的容量
- 提供对应的拷贝构造函数以及 operator= 防止浅拷贝问题
- 提供尾插法和尾删法对数组中的数据进行增加和删除
- 可以通过下标的方式访问数组中的元素
- 可以获取数组中当前元素个数和数组的容量
MyArray.hpp(通用数组模板类)
// 自己通用的数组类 #pragma once #includeusing namespace std; // 类模板 template class MyArray { public: // 有参构造函数 MyArray(int capacity) { this->Capacity_ = capacity; this->Size_ = 0; // 从堆区开辟数组:指针 = new 数据类型[长度]; this->pAddress_ = new T[this->Capacity_]; } // 拷贝构造函数 MyArray(const MyArray &arr) { this->Capacity_ = arr.Capacity_; this->Size_ = arr.Size_; // this->pAddress_ = arr.pAddress_; // 浅拷贝 // 深拷贝 this->pAddress_ = new T[arr.Capacity_]; // 将 arr 中的数据都拷贝过来 for (int i = 0; i < arr.Size_; i++) { this->pAddress_[i] = arr.pAddress_[i]; } } // operator= 防止浅拷贝 MyArray& operator=(const MyArray& arr) { // 先判断原有堆区是否有数据,如果有先释放 if (this->pAddress_ != NULL) { delete[]this->pAddress_; this->pAddress_ = NULL; this->Capacity_ = 0; this->Size_ = 0; } // 深拷贝 this->Capacity_ = arr.Capacity_; this->Size_ = arr.Size_; this->pAddress_ = new T[arr.Capacity_]; // 将 arr 中的数据都拷贝过来 for (int i = 0; i < arr.Size_; i++) { this->pAddress_[i] = arr.pAddress_[i]; } return *this; } // 尾插法 void Push_Back(const T &value) { // 判断容量是否等于大小 if (this->Capacity_ == this->Size_) { cout << "数组容量已满!" << endl; return; // 插入失败 } this->pAddress_[this->Size_] = value; // 在数组尾部插入数据 this->Size_++; // 更新数组大小 } // 尾删法 void Pop_Back() { // 让用户访问不到最后一个元素,为逻辑上的尾删 if (this->Size_ == 0) { cout << "数组中无元素!无法进行删除操作!" << endl; return; } this->Size_--; } // 通过下标访问数组元素 // 想让返回的值 arr[0] = 1 作为左值存在,则函数类型为引用 T& operator[](int index) { return this->pAddress_[index]; } // 返回数组容量 int getCapacity() { return this->Capacity_; } // 返回数组大小 int getSize() { return this->Size_; } // 析构函数 ~MyArray() { if (this->pAddress_ != NULL) { delete []this->pAddress_; this->pAddress_ = NULL; } } private: T *pAddress_; // 指针指向堆区开辟的真实数组 int Capacity_; // 数组容量 int Size_; // 数组大小 };
main.cpp(测试 int 数据类型)
#includeusing namespace std; #include "MyArray.hpp" int main() { MyArray arr(5); // 容量为 5 的整型数组 // 利用尾插法插入元素 for (int i = 0; i < arr.getCapacity(); i++) { arr.Push_Back(i + 1); } // 通过下标的方式访问数组中的元素 for (int i = 0; i < arr.getSize(); i++) { cout << arr[i] << " "; // 1 2 3 4 5 } cout << endl; // 打印容量及大小 cout << "数组容量为:" << arr.getCapacity() << endl; // 数组容量为:5 cout << "数组大小为:" << arr.getSize() << endl; // 数组大小为:5 // 尾删法删除元素 arr.Pop_Back(); arr.Pop_Back(); // 打印容量及大小 cout << "数组容量为:" << arr.getCapacity() << endl; // 数组容量为:5 cout << "数组大小为:" << arr.getSize() << endl; // 数组大小为:3 system("pause"); return 0; }
main.cpp(测试自定义数据类型)
#includeusing namespace std; #include "MyArray.hpp" class Person { public: // 构造函数 Person() {} Person(int id, int score) { this->Id_ = id; this->Score_ = score; } public: int Id_; int Score_; }; void printPersonArray(MyArray & arr) { for (int i = 0; i < arr.getSize(); i++) { cout << "学号:" << arr[i].Id_ << " 分数:" << arr[i].Score_ << endl; } } int main() { MyArray arr(5); // 创建数据组 Person p1(1, 100); Person p2(2, 90); Person p3(3, 80); Person p4(4, 70); Person p5(5, 60); // 尾插法插入数据 arr.Push_Back(p1); arr.Push_Back(p2); arr.Push_Back(p3); arr.Push_Back(p4); arr.Push_Back(p5); // 输出数组 printPersonArray(arr); // outputs:学号:1 分数:100 // 学号:2 分数:90 // 学号:3 分数:80 // 学号:4 分数:70 // 学号:5 分数:60 system("pause"); return 0; }



