什么是模板?
模板就是通用的工具,用于提高代码复用性。
模板只是一个架子,不能直接使用。
模板只是通用,并不是万能。
为什么要学习模板?
学习模板并不是为了写模板,而是在STL中能够运用系统提供的模板
C++ 中的模板
C++ 的编程思想之一【泛型编程】,主要是有点额技术就是【模板】。
C++ 有两种模板机制:函数模板和类模板。
函数模板
函数模板:在声明、定义函数时,涉及到的函数返回值类型、参数类型可以使用一个虚拟的类型代替,而不用指定具体的类型,这样就大大提高了该函数的通用性。
函数模板的语法
template<typename T>
紧接着写函数声明、定义
template
关键字:声明创建模板typename
关键字:声明使用虚拟类型,可以使用class
关键字代替T
:模板中是有点额虚拟类型的名称,可以自定义
函数模板使用
例如,编写一个交换两个变量值得函数,需要针对不同的数据类型编写对应的交换函数:
交换两个Int 类型的值如下:
//交换整型函数
void swapInt(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
//交换浮点型函数
void swapDouble(double& a, double& b) {
double temp = a;
a = b;
b = temp;
}
int main() {
int a = 1;
int b = 2;
swapInt(a,b);
double c = 1.25;
double d = 2.33;
swapDouble(c,d);
system("pause");
return 0;
}
如果还需要交换其他类型的,还需要再编写对应类型的交换函数,类似这种相同逻辑的代码就可以使用函数模板来提高复用性:
- 函数模板定义
// 利用模板提供通用的交换函数
template<typename T>
void mySwap(T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}
- 函数模板调用方式一:自动类型推导
int main() {
// 自动类型推导
int a = 1;
int b = 2;
my_swap(a, b);
cout << "a = " << a << endl;
cout << "b = " << b << endl;
system("pause");
return 0;
}
- 函数模板调用方式二:显示指定类型
int main() {
// 显示指定类型
float c = 1.25f;
float d = 0.05f;
my_swap<float>(c, d);
cout << "c = " << c << endl;
cout << "d = " << d << endl;
system("pause");
return 0;
}
函数模板使用注意事项
- 自动类型推导,必须能够推导出一致的数据类型才能调用
- 必须能够确定出 T 的类型才能调用函数
//利用模板提供通用的交换函数
template<class T>
void mySwap(T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}
// 1、自动类型推导,必须推导出一致的数据类型T,才可以使用
void test01()
{
int a = 10;
int b = 20;
char c = 'c';
mySwap(a, b); // 正确,可以推导出一致的T
//mySwap(a, c); // 错误,推导不出一致的T类型
}
// 2、模板必须要确定出T的数据类型,才可以使用
template<class T>
void func()
{
cout << "func 调用" << endl;
}
void test02()
{
//func(); //错误,模板不能独立使用,必须确定出T的类型
func<int>(); //利用显示指定类型的方式,给T一个类型,才可以使用该模板
}
int main() {
test01();
test02();
system("pause");
return 0;
}
函数模板实现数组排序案例
利用函数模板封装一个排序的函数,可以对不同数据类型数组进行排序
排序算法采用选择排序和冒泡排序进行升序排序
#include <iostream>
#include <string>
using namespace std;
// 打印数组
template<class T>
void print_array(T arr[], int size) {
for (int i = 0; i < size; i++) {
cout << arr[i] << " ";
}
cout << endl;
}
// 使用函数模板实现选择排序
template<class T>
void selection_sort(T* arr, int size) {
for (int i = 0; i < size; i++) {
int min_index = i;
for (int j = i + 1; j < size; j++) {
if (arr[j] < arr[min_index]) {
min_index = j;
}
}
if (min_index != i) {
T temp = arr[i];
arr[i] = arr[min_index];
arr[min_index] = temp;
}
}
}
// 使用函数模板实现冒泡排序
template<class T >
void bubble_sort(T* arr,int size) {
for (int i = 0; i < size - 1; i++) {
for (int j = 0; j < size - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
T temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
// 测试选择排序:int型
void test_02_1() {
int arr[10] = { 2,1,4,7,3,8,9,0,5,6 };
print_array(arr, 10);
selection_sort(arr, 10);
print_array(arr, 10);
}
// 测试选择排序:char 型
void test_02_2() {
char arr[10] = { 'S','s','A','b','a','C','c','D','e','d'};
print_array(arr, 10);
selection_sort(arr, 10);
print_array(arr, 10);
}
// 测试选择排序:int型
void test_02_3() {
int arr[10] = { 2,1,4,7,3,8,9,0,5,6 };
print_array(arr, 10);
bubble_sort(arr, 10);
print_array(arr, 10);
}
// 测试选择排序:char 型
void test_02_4() {
char arr[10] = { 'S','s','A','b','a','C','c','D','e','d' };
print_array(arr, 10);
bubble_sort(arr, 10);
print_array(arr, 10);
}
int main02() {
test_02_1();
test_02_2();
test_02_3();
test_02_4();
return 0;
}
普通函数与函数模板的区别
- 普通函数调用时可以发生自动类型转换(隐式类型转换)
- 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
- 如果利用显示指定类型的方式,可以发生隐式类型转换
建议使用显示指定类型的方式,调用函数模板,因为可以自己确定通用类型T
#include <iostream>
#include <string>
using namespace std;
// 普通函数
void add_01(int a, int b) {
cout << a + b << endl;
}
// 函数模板
template<class T>
void add_02(T a, T b) {
cout << a + b << endl;
}
// 测试普通函数下的类型转换
void test03_1() {
int a = 1;
char b = 'A';
// 能够自动将 char 类型转为 int 类型传入进行函数调用
add_01(a, b);
}
void test03_2() {
int a = 1;
char b = 'A';
// 如果采用自动类型推导,不能自动类型转换
// add_02(a, b);
// 需要显示的指定T的类型,才能发生数据类型转换
add_02<int>(a, b);
}
int main() {
test03_1();
test03_2();
return 0;
}
普通函数与函数模板的调用优先级及规则
- 如果两者都能正常调用得到相同的目标结果,会优先调用普通函数
- 可以通过空模板参数列表来强制调用函数模板
- 模板函数可以发生重载
- 如果函数模板能够更好的匹配,优先调用函数模板
总结:既然提供了函数模板,最好就不要再提供普通函数,否则容易出现二义性
#include <iostream>
#include <string>
using namespace std;
// 普通函数
void func(int a,int b) {
cout << "调用普通函数" << endl;
}
// 模板函数
template<class T>
void func(T a,T b) {
cout << "调用模板函数" << endl;
}
// 模板函数重载
template<class T>
void func(T a,T b,T c) {
cout << "调用重载的模板函数" << endl;
}
// 普通函数与模板函数的调用优先级规则:两者都能实现,有限调用普通函数
void test04_1() {
int a = 1;
int b = 2;
func(a, b); // 调用普通函数
}
// 普通函数与模板函数的调用优先级规则:通过空模板参数列表来强制调用函数模板
void test04_2() {
int a = 1;
int b = 2;
func<>(a, b); // 调用模板函数
}
// 普通函数与模板函数的调用优先级规则:模板函数可以发生重载
void test04_3() {
int a = 1;
int b = 2;
int c = 3;
func<>(a,b); // 调用模板函数
func(a,b,c); // 调用重载的模板函数
}
// 普通函数与模板函数的调用优先级规则:如果函数模板能够更好的匹配,优先调用函数模板
void test04_4() {
char a = 1;
char b = 2;
func(a, b); // 调用模板函数
}
int main() {
test04_4();
return 0;
}
函数模板的局限性
- 模板的通用性并不是万能的
- 内置基本数据类型可以直接使用函数模板
- 自定义的数据类型或数组等复杂类型,调用通用模板时就会受到限制
如下定义了一个比较函数模板,如果传入 基本数据类型的参数,可以正常调用,但是如果传入的是一个数组或者一个类,就无法正常运行
//普通函数模板
template<class T>
bool myCompare(T& a, T& b)
{
if (a == b)
{
return true;
}
else
{
return false;
}
}
针对这种局限,C++ 提供模板的重载机制,可以为这些特定的类型提供具体化的模板
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
string name;
int age;
Person(string name, int age) {
this->name = name;
this->age = age;
}
};
// 普通函数模板
template<class T>
int my_compare(T a, T b) {
if (a == b) {
return 0;
}
else if (a > b) {
return 1;
}
else {
return -1;
}
}
// 具体化的函数模板,特殊处理Person类
template<> int my_compare(Person a, Person b) {
if (a.name == b.name && a.age == b.age) {
return 0;
}
else if (a.age > b.age) {
return 1;
}
else {
return -1;
}
}
// 内置基本数据类型可以直接使用函数模板
void test05_1() {
cout << my_compare(1, 2) << endl;
}
// 自定义数据类型或复杂数据类型不能直接使用函数模板
// 可以创建具体化的Person数据类型的模板,用于特殊处理这个类型
void test05_2() {
Person p1("zhangsan",19);
Person p2("zhangsan",19);
cout << my_compare(p1, p2) << endl; // 直接使用普通模板,运行出错
}
int main() {
test05_2();
return 0;
}
类模板
类模板:建立一个通用类,类中的成员 数据类型可以不具体制定,用一个虚拟的类型来代表。
类模板的语法
template<typename T>
紧接着编写类的声明、定义
template
关键字:声明创建模板typename
关键字:声明使用虚拟类型,可以使用class
关键字代替T
:模板中是有点额虚拟类型的名称,可以自定义
类模板的使用
编写一个类,类中成员变量的数据类型和成员函数返回值、参数类型都使用虚拟的数据类型代替,实例化类的对象时,再指定具体的类型:
#include <iostream>
#include <string>
using namespace std;
template<class T>
class Student {
private:
T pro;
public:
void setPro(T pro) {
this->pro = pro;
}
T getPro() {
return pro;
}
};
int main() {
Student<string> s1;
s1.setPro("Hello");
string pro1 = s1.getPro();
Student<int> s2;
s2.setPro(2);
int pro2 = s2.getPro();
return 0;
}
类模板与函数模板的区别
- 使用方式类似
- 函数模板的虚拟类型不能设置默认值,类模板的虚拟类型可以有默认值
- 函数模板可以使用自动类型推导方式,类模板使用时没有自动类型推导,只能显示的指定具体类型
#include <iostream>
#include <string>
using namespace std;
// 类模板
// 可以定义多个虚拟类型
// 与函数模板不同:类模板中,类型参数可以指定默认值
template<class NameType,class AgeType = int>
class Demo {
private:
NameType name;
AgeType age;
public:
Demo(NameType name, AgeType age) {
this->name = name;
this->age = age;
}
NameType getName() {
return name;
}
void setName(NameType name) {
this->name = name;
}
AgeType getAge() {
return age;
}
void setAge(AgeType age) {
this->age = age;
}
void info() {
cout << "name = " << name << ",age = " << age << endl;
}
};
int main() {
// 与函数模板不同,类模板不能使用自动类型推导方式,
// Demo<> demo1("jieke",10);
// demo1.info();
// 必须显示指定类型
Demo<char, string> demo2('A', "三岁");
demo2.info();
demo2.setName('a');
string demo2_age = demo2.getAge();
// 如果虚拟类型定义了默认值,使用时没有指定的话,就使用默认的类型
Demo<string> demo3("托尼", 18);
demo3.info();
return 0;
}
类模板中成员函数创建时机
类模板中成员函数和普通类中成员函数创建时机是有区别的:
- 普通类中的成员函数一开始就可以创建
- 类模板中的成员函数在调用时才创建
#include <iostream>
#include<string>
using namespace std;
class Acc {
public:
void show_info_a() {
cout << "Acc 类中的成员函数被调用" << endl;
}
};
class Bcc {
public:
void show_info_b() {
cout << "Bcc 类中的成员函数被调用" << endl;
}
};
template <class T>
class TestClass {
public:
T obj;
void func1() {
obj.show_info_a();
}
void func2() {
obj.show_info_b();
}
};
void test07_1() {
Acc acc;
TestClass<Acc> tc;
tc.func1(); // 可以正常调用
// 不能正常调用
// 报错:"show_info_b": 不是 "Acc" 的成员
// 说明类模板中的成员函数不是一开始就生成,而是在调用类模板时才生产的
// tc.func2();
}
void test07_2() {
Bcc bcc;
TestClass<Bcc> tc;
// 不能正常调用
// 报错:"show_info_a": 不是 "Bcc" 的成员
// 说明类模板中的成员函数不是一开始就生成,而是在调用类模板时才生产的
// tc.func1();
// 可以正常调用
tc.func2();
}
int main() {
test07_2();
return 0;
}
类模板的对象作为函数参数
类模板实例化出的对象,向函数传参的方式有三种:
- 指定传入的类型:给类模板中的虚拟参数指定具体的类型传入
- 参数模板化:在传参时,继续将类模板中的虚拟参数模板化
- 整个类模板化:将作为函数参数的对象类型进行模板化
#include<iostream>
#include<string>
using namespace std;
template<class T1,class T2>
class Foo {
public:
T1 pre1;
T2 pre2;
Foo(T1 p1, T2 p2) {
pre1 = p1;
pre2 = p2;
}
void info() {
cout << pre1 << " " << pre2 << endl;
}
};
// 类模板对象作为函数参数:显示指定对象的数据类型
void func1(Foo<int,float> foo) {
foo.info();
}
// 类模板对象作为函数参数:函数参数模板化
template<class T1,class T2>
void func2(Foo<T1,T2> foo) {
// 使用 typeid 函数获取虚拟参数的类型,name()函数获取类型名称
cout << "T1 的实际类型为:" << typeid(T1).name() << endl;
cout << "T2 的实际类型为:" << typeid(T2).name() << endl;
foo.info();
}
// 类模板对象作为函数参数:将这个对象类型进行模板化,再传递
template<class T>
void func3(T foo) {
cout << "T 的实际类型为:" << typeid(T).name() << endl;
foo.info();
}
int main() {
Foo<int, float> foo1(2, 1.25f);
func1(foo1);
Foo<string, bool> foo2("HelloC++", false);
func2(foo2);
Foo<string, string> foo3("Hello", "World");
func3(foo3);
return 0;
}
类模板继承
- 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
- 如果不指定,编译器无法给子类分配内存
- 如果想灵活指定出父类中T的类型,子类也需变为类模板
#include<iostream>
#include<string>
using namespace std;
template<class T>
class Parent {
public:
T pre;
void info() {
cout << "pre 的类型为:" << typeid(T).name() << endl;
cout << "pre 的值为:" << pre << endl;
}
};
// 类模板继承:1.子类继承时指定父类中的虚拟类型的具体类型
class Child01 : public Parent<string> {
};
// 类模板继承:2.如果子类继承时没有指定父类中的虚拟类型的具体类型,那么子类也应该定义为类模板
template<class T1,class T2>
class Child02 :public Parent<T2> {
public:
T1 c_pre;
Child02(T1 a, T2 b) {
c_pre = a;
this->pre = b;
}
void info() {
cout << "c_pre 的类型为:" << typeid(T1).name() << endl;
cout << "c_pre 的值为:" << c_pre << endl;
// 调用父类中的方法
Parent<T2>::info();
}
};
int main() {
Child01 c1;
c1.pre = "Hello";
c1.info();
Child02<int, double> c2(2,3.14);
c2.info();
return 0;
}
类模板中的函数在类外实现
#include<iostream>
#include<string>
using namespace std;
template<class T>
class Boo {
private:
T pre;
public:
// 类内声明构造函数
Boo(T p);
// 类内声明成员函数
T getPre();
};
// 类外实习构造函数
template<class T>
Boo<T>::Boo(T p) {
this->pre = p;
}
// 类外实习成员函数
template<class T>
T Boo<T>::getPre() {
return this->pre;
}
int main() {
Boo<string> boo("Hello");
string s = boo.getPre();
cout << s << endl;
return 0;
}
类模板分文件编写
类模板分文件编写存在的问题
以常规方式将类模板进行分文件编写:
① 头文件 demo.h
#pragma once
#include<iostream>
#include<string>
using namespace std;
template<class T1,class T2>
class Demo {
private:
T1 pre1;
T2 pre2;
public:
// 声明函数
Demo(T1 p1, T2 p2);
void info();
};
② 源文件 demo.cpp
#include"demo.h"
// 实现函数
template<class T1,class T2>
Demo<T1, T2>::Demo(T1 p1, T2 p2) {
this->pre1 = pre1;
this->pre2 = pre2;
}
template<class T1, class T2>
void Demo<T1, T2>::info() {
cout << "pre1的类型:" << typeid(T1).name() << endl;
cout << "pre2的类型:" << typeid(T2).name() << endl;
}
③ test.cpp 中引入头文件 demo.h
#include"demo.h"
using namespace std;
int main() {
Demo<int, string> demo(100, "Hello"); // 无法解析的外部命令
demo.info(); // 无法解析的外部命令
return 0;
}
因为类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到
类模板分文件编写解决方案
解决方式1:
直接包含 demo.cpp 源文件
#include"demo.cpp"
using namespace std;
int main() {
Demo<int, string> demo(100, "Hello"); // 正常执行
demo.info(); // 正常执行
return 0;
}
解决方案2(主流的方案):
将声明和实现写到同一个文件中,并更改后缀名为.hpp,hpp是约定的名称,并不是强制
① 头文件 demo.hpp
#pragma once
#include<iostream>
#include<string>
using namespace std;
template<class T1, class T2>
class Demo {
private:
T1 pre1;
T2 pre2;
public:
// 声明函数
Demo(T1 p1, T2 p2);
void info();
};
// 函数实现
template<class T1, class T2>
Demo<T1, T2>::Demo(T1 p1, T2 p2) {
this->pre1 = pre1;
this->pre2 = pre2;
}
template<class T1, class T2>
void Demo<T1, T2>::info() {
cout << "pre1的类型:" << typeid(T1).name() << endl;
cout << "pre2的类型:" << typeid(T2).name() << endl;
}
② test.cpp 中引入头文件 demo.hpp
#include"demo.hpp"
using namespace std;
int main() {
Demo<int, string> demo(100, "Hello"); // 正常执行
demo.info(); // 正常执行
return 0;
}
类模板做友元
① 全局函数做类模板的友元:全局函数在类内实现
#include <iostream>
#include <string>
using namespace std;
template<class T>
class Hoo {
// 全局函数做类模板的友元:类内实现全局函数(推荐:用法简单,而且编译器可以直接识别)
friend void func1(Hoo<T>& hoo) {
cout << hoo.pre << endl;
}
private:
T pre;
public:
Hoo(T p) {
this->pre = p;
}
};
int main() {
Hoo<string> hoo1("Hello");
func1(hoo1); // Hello
return 0;
}
② 全局函数做类模板的友元:类外实现全局函数(需要提前让编译器知道全局函数的存在)
#include <iostream>
#include <string>
using namespace std;
// 先声明类
template<class T> class Hoo;
// 将全局函数写在类前面,让编译器直接先找到全局函数
template<class T>
void func(Hoo<T>& hoo) {
cout << hoo.pre << endl;
}
template<class T>
class Hoo {
friend void func<>(Hoo<T>& hoo);
private:
T pre;
public:
Hoo(T p) {
this->pre = p;
}
};
int main() {
Hoo<int> hoo(100);
func(hoo);
return 0;
}
类模板案例
封装一个通用的数组类:
- 可以对内置数据类型以及自定义数据类型的数据进行存储
- 将数组中的数据存储到堆区
- 构造函数中可以传入数组的容量
- 提供对应的拷贝构造函数以及operator=防止浅拷贝问题
- 提供尾插法和尾删法对数组中的数据进行增加和删除
- 可以通过下标的方式访问数组中的元素
- 可以获取数组中当前元素个数和数组的容量
① 头文件 myarray.hpp
#pragma once
#include<iostream>
#include<string>
using namespace std;
template<class T>
class MyArray {
private:
T* paddr; // 指向堆中存储真正数据的指针
int capacity; // 数组容量
int size; // 数组元素个数
public:
// 构造函数
MyArray(int capacity) {
// 初始化数组初始容量
this->capacity = capacity;
// 初始化数组初始大小
this->size = 0;
// 初始化数组初始数据指针:以当前容量在堆中创建某个类型的数组,并且将数据指针指向该数组
this->paddr = new T[this->capacity];
}
// 析构函数
~MyArray() {
cout << this << endl;
if (this->paddr != NULL) {
delete[] this->paddr; // 释放堆中的内存
this->paddr = NULL; // 防止野指针出现
this->capacity = 0;
this->size = 0;
}
}
// 获取数组容量大小
int getCapacity() {
return this->capacity;
}
// 获取当前数组元素个数
int getSize() {
return this->size;
}
// 复制构造函数
MyArray(const MyArray& source) {
// 根据源数组初始化
this->capacity = source.capacity;
this->size = source.size;
this->paddr = new T[this->capacity];
// 将原数组中的元素进行深拷贝
for (int i = 0; i < this->size; i++){
// 如果T为对象,而且还包含指针,T类中必须需要重载 = 操作符,因为这个等号不是 构造 而是赋值,
// 普通类型可以直接= 但是指针类型需要深拷贝
this->paddr[i] = source.paddr[i];
}
}
// 重载 赋值操作符
MyArray& operator=(const MyArray& source) {
if (this->paddr != NULL) {
delete[] this->paddr;
this->capacity = 0;
this->size = 0;
}
this->capacity = source.capacity;
this->size = source.size;
this->paddr = new T[this->capacity];
for (int i = 0; i < this->size; i++) {
// 如果数组元素是对象,而且包含指针,需要重载 = 操作符,避免浅拷贝带来的问题
this->paddr[i] = source[i];
}
// 返回对象本身
return *this;
}
// 重载 [] 操作符
// 返回元素的引用
T& operator[](int index) {
// 不考虑数组越界
return this->paddr[index];
}
// 向数组尾部插入一个元素
bool add(const T& e) {
// 如果数组已满插入失败
if (this->size == this->capacity) {
return false;
}
this->paddr[size] = e;
this->size++;
}
// 删除数组最后一个元素
bool remove() {
if (this->size > 0) {
this->size--;
return true;
}
return false;
}
// 遍历数组
void show() {
for (int i = 0; i < this->size; i++)
{
cout << this->paddr[i] << " ";
}
cout << endl;
}
};
② 测试
#include"myarray.hpp"
using namespace std;
};
// 重写 左移操作符
ostream& operator<<(ostream& out, MyDog& dog) {
out << "姓名:" << *dog.name << ",年龄:" << *dog.age;
return out;
}
int main() {
MyArray<int> arr1(5);
arr1.add(1);
arr1.add(2);
arr1.add(3);
arr1.show();
MyArray<int> arr2(arr1);
arr2.add(4);
arr2.show();
MyArray<int> arr3 = arr2;
arr3.remove();
arr3.remove();
arr3.show();
cout << arr3[2] << endl;
system("pause");
return 0;
}