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