文章目录
一. 模板的概念
- 模板就是建立通用的工具,提高代码的复用性.
- 模板不能直接使用,大部分的时候只是一个框架
- 模板的通用并不是万能的
二. 函数模板
① 函数模板介绍
- C++有一种编程思想称为
泛型编程
,主要利用的技术就是模板函数模板的作用:
建立一个通用的函数,其函数返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代表.
② 函数模板语法
template <typename T>
函数声明或者定义
template
– 声明创建模模板typename
– 表明后面的符号是一种数据类型,其中typename
可以用class
来代替T
– 声明的通用的数据类型,名称可以替换,一般为大写字母.
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include <iostream>
using namespace std;
// 函数模板
// 两个整型交换函数
void swap_int(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}
// 两个浮点型的交换函数
void swap_double(double &a, double &b)
{
double temp = a;
a = b;
b = temp;
}
// 如果还有其他的类型,就必须要继续的添加函数.
template <typename T> // 函数模板
void swap_two_number(T &t1, T &t2)
{
T temp;
temp = t1;
t1 = t2;
t2 = temp;
}
int main()
{
int a = 10;
int b = 20;
// 函数模板的使用方式
// 1. 自动类型推导
swap_two_number(a, b);
// 2. 显示指定类型
double c = 3.2;
double d = 3.4;
swap_two_number<double>(c, d);
system("pause");
return 0;
}
模板函数使用方式:
- 自动类型推导
- 指定参数模板类型
③ 函数模板的注意事项
- 自动类型推导,必须推导出一致的数据类型T,才可以使用
- 模板必须要确定出T的数据类型,才可以使用.
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include <iostream>
using namespace std;
// 1. 自动类型推导,必须推导出一致的数据类型T才可以使用
template <typename T>
void swap_number(T &t1, T &t2)
{
T temp = t1;
t1 = t2;
t2 = temp;
}
void test_01(void)
{
int a = 10;
int b = 20;
// 自动类型推导,两个类型一致,可以推测出一致的类型
swap_number(a, b);
cout << "a = " << a << " b = " << b << endl;
}
void test_02(void)
{
int a = 10;
double b = 10.5;
// 自动类型推导,两个类型不一致,不可以推测出一致的类型
//swap_number(a, b); 这里会报错,参数类型不匹配.推导不出来一致的类型
}
// 2. 模板必须要确定出T的数据类型,才可以使用
template <typename T>
void func()
{
cout << "func 调用!" << endl;
}
void test_03(void)
{
// func(); // 模板函数必须确定T的类型,才能使用,即使函数中没有这个类型参数
func<int>();// 正确
}
int main()
{
test_01();
system("pause");
return 0;
}
④ 函数模板的例子
- 利用模板函数封装一个排序的函数,可以对不同的数据类型数组进行排序
- 排序规则从大到小,排序算法为选择排序
- 分别利用char数组和int数组以及double数组进行测试
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include <iostream>
using namespace std;
// 对数组进行排序的函数
template <typename T>
void my_swap_template(T &a, T &b)
{
T temp = a;
a = b;
b = temp;
}
// 打印模板
template <typename T>
void print_arr_template(T arr[], int len)
{
for (int i = 0; i < len; i++)
{
cout << arr[i] << " ";
}
cout << endl;
}
// 排序算法
template <typename T>
void sort_template(T arr[], int len)
{
for (int i = 0; i < len; i++)
{
int max = i; // 认定最大值的下标
for (int j = i + 1; j < len; j++)
{
if (arr[max] < arr[j])
{
max = j;
}
}
if (max != i)
{
// 交换 max 和 i的元素
my_swap_template(arr[max], arr[i]); // 交换两个数
}
}
}
void test_01()
{
// 测试char数组
char charArr[] = "badcfe";
int arrLen = sizeof(charArr) / sizeof(charArr[0]);
sort_template(charArr, arrLen);
print_arr_template(charArr, arrLen);
int intArr[] = { 2,3,4,1,5,6,8,7 };
arrLen = sizeof(intArr) / sizeof(intArr[0]);
sort_template(intArr, arrLen);
print_arr_template(intArr, arrLen);
double doubleArr[] = { 1.2,3.4,5.6,0.8,9.1,3.2 };
arrLen = sizeof(doubleArr) / sizeof(doubleArr[0]);
sort_template(doubleArr, arrLen);
print_arr_template(doubleArr, arrLen);
}
int main()
{
test_01();
system("pause");
return 0;
}
三. 函数模板和普通函数的区别
- 普通函数调用可以发生隐式类型转换
- 函数模板,使用自动类型推导,不可以发生隐式类型转换.
- 函数模板 用显示指定类型,可以发生隐式类型转换.
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include <iostream>
using namespace std;
// 1.普通函数调用可以发生隐式类型转换
// 普通函数
int my_add(int a, int b)
{
return a + b;
}
// 函数模板,不能发生隐式类型转换
template <typename T>
int my_add_template(T a, T b)
{
return a + b;
}
void test_01(void)
{
int a = 10;
int b = 10;
cout << my_add(a, b) << endl;
char c = 'c';
// 普通函数的隐式类型转换 c会通过ascii码获取整数,然后和a相加
cout << my_add(a, c) << endl;
//cout << my_add_template(a, c) << endl; 报错,参数类型匹配错误
// 显示指定类型,可以发生隐式类型转换
cout << my_add_template<int>(a, c) << endl;
}
int main()
{
test_01();
system("pause");
return 0;
}
四. 普通函数和函数模板的调用规则
- 如果函数模板和普通函数都满足调用条件,优先使用普通函数.
- 可以通过空模板参数列表来强制调用函数模板
- 函数模板也可以发生重载
- 如果函数模板可以产生更好的匹配,优先调用函数模板.也就是说模板调用优先于普通的隐式类型转换的调用.
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include <iostream>
using namespace std;
// 1. 如果函数模板和普通函数都可以调用,优先调用普通函数
// 2. 如果通过空模板参数列表,强制调用函数模板
// 3. 函数模板可以发生重载
// 4. 如果函数模板可以产生更好的匹配,优先调用函数模板
void my_print(int a, int b)
{
cout << "调用普通的函数" << endl;
}
template <typename T>
void my_print(T a, T b)
{
cout << "调用模板函数" << endl;
}
template <typename T>
void my_print(T a, T b, T c)
{
cout << "调动三个参数的模板函数" << endl;
}
void test_01(void)
{
int a = 10;
int b = 20;
my_print(a, b); // 调用普通的函数,如果普通的函数,只有定义没有实现,会报错.
// 通过空模板参数列表,强制调用模板函数
my_print<>(a, b); // 调用的模板函数
// 函数模板的重载
my_print(a, b, 30);// 调用的是三个参数的模板函数
// 如果函数模板产生更好的匹配,优先调用函数模板
char c1 = 'a';
char c2 = 'b';
// 这里如果没有模板函数,会通过隐式类型转换调用普通的函数.
// 如果模板函数存在,并且符合调用,会调用函数模板
my_print(c1, c2); // 这里调用的是函数模板
}
int main()
{
test_01();
system("pause");
return 0;
}
结果:
五. 模板的局限性
- 模板的通用性不是万能的
- 有些特定的数据类型,需要具体化方式做特殊实现
- 利用具体化的模板,可以解决自定义类型的通用性
- 学习模板并不是为了写模板,而是在
STL
能够运行系统提供的模板
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include <iostream>
using namespace std;
#include <string>
class Person
{
public:
Person(string name, int age)
{
this->mName = name;
this->mAge = age;
}
public:
string mName;
int mAge;
};
template <typename T>
bool my_compare(T &a, T &b)
{
return a == b;
}
// Person类型的具体化实现,告诉编译器这是一个具体化的模板,如果是Person类型,优先调用这个重载的函数模板
template<> bool my_compare(Person &p1, Person &p2)
{
return p1.mName == p2.mName && p2.mAge == p2.mAge;
}
void test_01(void)
{
int a = 10;
int b = 20;
bool ret;
ret = my_compare(a, b);
if (ret)
{
cout << "a 和 b相等!" << endl;
}
else
{
cout << "a 和 b不相等!" << endl;
}
// 如果是自定义类型
Person p1("Tom", 10);
Person p2("Tom", 10);
ret = my_compare(p1, p2);
if (ret)
{
cout << "p1 和 p2 相等!" << endl;
}
else
{
cout << "p1 和 p2 不相等!" << endl;
}
}
int main()
{
test_01();
system("pause");
return 0;
}
六 类模板
① 类模板的作用
建立一个通用的类,类中的成员 数据类型可以不具体制定,用一个虚拟的类型来代表.
② 语法
template <typename T>
类定义
③ 例子
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include <iostream>
#include <string>
using namespace std;
// 类模板
template <class NameType ,class AgeType>
class Person
{
public:
Person(NameType name, AgeType age)
{
this->mName = name;
this->mAge = age;
}
void show_person()
{
cout << "姓名: " << this->mName << " 年龄: " << this->mAge << endl;
}
public:
NameType mName;
AgeType mAge;
};
void test_01(void)
{
// 显示的指定类型
Person<string, int> p1("孙悟空", 1111);
p1.show_person();
// 自动类型判定,如果是类模板,必须指定参数列表
// Person p2("猪八戒",20);
Person<int, int > p2(1, 2);
p2.show_person();
}
int main()
{
test_01();
system("pause");
return 0;
}
④ 类模板和函数模板的区别
- 类模板没有自动类型推导的使用方式,也就是说,类模板必须要显示的指定模板的形参类型
- 类模板在模板参数列表中可以有默认参数
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include <iostream>
using namespace std;
template <class NameType,class AgeType=int>
class Person
{
public:
Person(NameType name, AgeType age)
{
this->mName = name;
this->mAge = age;
}
void show_person()
{
cout << "姓名: " << this->mName << " 年龄: " << this->mAge << endl;
}
public:
NameType mName;
AgeType mAge;
};
// 1. 类模板没有自动类型推导使用方式
void test_01(void)
{
//Person p1("孙悟空", 1000); // 错误,无法使用自动类型推导
Person<string, int> p2("孙悟空", 1000); // 正确,只能显示指定类型
p2.show_person();
}
// 2. 类模板再模板参数列表中可以有默认的参数
void test_02(void)
{
Person<string> p3("猪八戒", 10000); // 模板可以使用默然的类型int
Person<string> p4("孙悟空", 1.2); // 这里相当于是 Person<string,int> p4("孙悟空",1.2);
p3.show_person();
p4.show_person();
}
int main()
{
test_01();
test_02();
system("pause");
return 0;
}
⑤ 类模板中成员函数的创建时机
类模板中成员函数和普通类中成员函数创建时机是有区别的:
- 普通类中的成员函数一开始就可以创建
- 类模板中的成员函数在调用的时候才创建
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include <iostream>
using namespace std;
class Person1
{
public:
void show_person1(void)
{
cout << "Person1::show_person1()调用!" << endl;
}
};
class Person2
{
public:
void show_person2(void)
{
cout << "Person2::show_person2()调用!" << endl;
}
};
template <class T>
class MyClass
{
public:
T obj;
void func1()
{
obj.show_person1();
}
void func2()
{
obj.show_person2();
}
};
void test_01(void)
{
MyClass<Person1> m;
m.func1();
//m.func2(); 编译运行的时候会报错,说明func2中的obj.show_person2()是在编译运行时才创建
// 如果这里不调用,就不会报错,也证明了编译的时候,如果没有调用obj.show_person2()是没有创建的
// 这个也很好理解,因为是模板类的成员函数,所以要在具体调用的时候才能确定是什么类型
// 确定了是什么类型,才能进行创建
// 如何调用func2呢,再声明一个模板
MyClass<Person2> m2;
m2.func2();
}
int main()
{
test_01();
system("pause");
return 0;
}
七 类模板对象做函数参数
① 介绍
- 类模板也是可以实例化对象的
- 而这个实例化的对象是可以作为函数的参数的
② 参数传入的方式
- 指定传入的类型: 直接在参数列表中显示的指定模板参数的类型
- 参数模板化: 将对象中的参数变为模板进行传递,也就是在参数列表中使用模板,将函数定义为函数模板的方式
- 整个类模板化: 将这个对象类型 模板化进行传递.也就是整个类对象,当成一个模板来进行传参.
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include <iostream>
using namespace std;
// 类模板对象做函数参数
template <class T1,class T2>
class Person
{
public:
Person(T1 name, T2 age)
{
this->mName = name;
this->mAge = age;
}
void show_person()
{
cout << "姓名: " << this->mName << " 年龄: " << this->mAge << endl;
}
public:
T1 mName;
T2 mAge;
};
// 1. 指定传入的类型,普通函数传参的方式
void print_info_01(Person<string, int> &p)
{
p.show_person();
}
// 2. 使用函数模板,参数定义为模板的形式
template <typename T1,typename T2>
void print_info_02(Person<T1,T2> & p)
{
p.show_person();
cout << "T1的类型: " << typeid(T1).name() << endl;
cout << "T2的类型: " << typeid(T2).name() << endl;
}
// 3.直接将整个类当成一个模板作为参数
template <typename T>
void print_info_03(T&p)
{
p.show_person();
cout << "T的类型: " << typeid(T).name() << endl;
}
void test_01(void)
{
Person<string,int> p1("猪八戒", 100);
print_info_01(p1);
Person<string, int> p2("孙悟空", 1000);
print_info_02(p2);
Person<string, int> p3("唐僧", 20);
print_info_03(p3);
}
int main()
{
test_01();
system("pause");
return 0;
}
结果:
总结:
- 通过类模板创建的对象,可以有三种方式向函数中进行传参
- 使用比较广泛的是第一种: 指定传入的类型
八 类模板和继承
① 类模板碰到继承的时候,需要注意一下几点:
- 当子类继承的父类是一个类模板的时候,子类在声明的时候,要指定出父类中的T类型
- 如果不指定,编译器无法给予子类分配内存
- 如果想要灵活指定出父类的T类型,子类也需要变为类模板
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include <iostream>
using namespace std;
// 类模板与继承
template<class T>
class Base
{
public:
T m;
};
// class Derived:public Base // 错误,必须要知道父类中的T类型,才能继承给子类
class Derived :public Base<int> // 指定父类的T类型是int类型
{
};
// 如果想要灵活指定父类中的T类型,子类也需要变成模板
template <class T1,class T2>
class Son :public Base<T2>
{
public:
Son()
{
cout << "T1的类型: " << typeid(T1).name() << endl;
cout << "T2的类型: " << typeid(T2).name() << endl;
cout << "Base::T的类型: " << typeid(Base<T2>::m).name() << endl;
}
T1 obj;
};
void test_01(void)
{
Derived d;
Son<int,char> s;
}
int main()
{
test_01();
system("pause");
return 0;
}
结果:
九 类模板成员函数类外实现
- 类外实现首先要指定成员函数的类作用域
- 然后还要加上模板声明以及显示的指定类模板的类型
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include <iostream>
using namespace std;
#include <string>
template <class T>
class Person
{
public:
Person(T name);
void show_person(void);
public:
T mName;
};
// 类外实现的时候腰加上模板声明,并且类域那里也要加上模板的声明
template <class T>
Person<T>::Person(T name)
{
this->mName = name;
}
template <class T>
void Person<T>::show_person(void)
{
cout << "姓名: " << this->mName << endl;
}
int main()
{
system("pause");
return 0;
}
十 类模板分文件编写
① 类模板分文件编写的时候会有什么问题
- 我们知道类模板的函数是在编译的时候调用的时候创建的
- 分文件编写也就是说头文件是文件声明,
cpp
文件是文件实现- 并且我们在使用的时候一般都是声明包含头文件,但是如果这样使用类模板,在编译的时候,头文件中并没有定义这个函数,导致这个函数会找不到.
② 解决方案
- 包含我们的
cpp文件
,直接包含源文件,只是这种做法有点不符合常规,一般没有包含源文件这种写法- 就是把分文件编写改成单文件编写,把模板类的声明和实现全部放到一个文件中,起名为
.hpp
文件,我们约定.hpp
文件一般就是类模板文件.使用的时候像包含头文件一样包含这个类模板文件就可以了
十一 类模板和友元
① 类内实现
类内实现比较简单,直接在类内声明友元然后实现即可,跟普通的全局友元函数区别不大
② 类外实现的时候
- 首先是需要让编译器知道全局函数的存在
- 需要类定义的上方进行定义声明
- 并且声明定义的时候需要制定是类模板函数
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include <iostream>
using namespace std;
#include <string>
template <class T1, class T2>
class Person; // 首先是声明Person类
template <class T1, class T2>
void show_person_02(Person<T1,T2>p)
{
// 实现的是模板函数
cout << "姓名: " << p.mName << " 年龄: " << p.mAge << endl;
}
template <class T1,class T2>
class Person
{
public:
Person(T1 name, T2 age)
{
this->mName = name;
this->mAge = age;
}
// 友元的类内实现
friend void show_person(const Person<T1,T2> p)
{
cout << "姓名: " << p.mName << " 年龄: " << p.mAge << endl;
}
// 友元的类外实现,声明的时候加上空模板参数,代表这个函数是类模板函数
friend void show_person_02<>(Person<T1,T2> p);
private:
T1 mName;
T2 mAge;
};
void test_01(void)
{
Person<string, int> p1("Tom", 10);
show_person(p1);
show_person_02(p1); // 类外实现的全局友元函数
}
int main()
{
test_01();
system("pause");
return 0;
}
十二 类模板案例
① 案例描述
实现一个通用的数组类
- 可以对内置数据类型以及自定义数据类型进行存储
- 将数组中的数据存储到堆区
- 构造函数中可以传入数组的数量
- 提供对应的拷贝构造函数以及operrator=方式浅拷贝问题
- 提供尾插法和尾删法对数组中的数据进行增加和删除
- 可以通过下标的方式访问数组中的元素
- 可以获取数组中当前元素的个数和数组的容量
② 设计的思路
③ 编码实现
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#pragma once
#include <iostream>
using namespace std;
template <class T>
class MyArray
{
public:
// 有参构造 参数 容量
MyArray(int capacity)
{
cout << "MyArray:: 有参构造调用!" << endl;
this->mCapacity = capacity;
this->mSize = 0;
this->pAddress = new T[this->mCapacity];
}
// 拷贝构造
MyArray(const MyArray &arr)
{
cout << "MyArray:: 拷贝构造调用!" << endl;
this->mCapacity = arr.mCapacity;
this->mSize = arr.mSize;
// this->pAddress = arr.pAddress 浅拷贝,错误
// 将arr中的数据全部拷贝过来,深拷贝就是把之前的内容复制过来进行赋值
for (int i = 0; i < this->mSize; i++)
{
this->pAddress[i] = arr.pAddress[i];
}
}
// operator= 防止浅拷贝的问题
MyArray &operator=(const MyArray &arr)
{
cout << "MyArray:: 的赋值运算符函数被调用!" << endl;
// 先判断原来的堆区是否有数据,如果有先释放
if (this->pAddress != NULL)
{
delete[] this->pAddress;
this->pAddress = NULL;
this->mCapacity = 0;
this->mSize = 0;
}
// 深拷贝
this->mCapacity = arr.mCapacity;
this->mSize = arr.mSize;
this->pAddress = new T[arr.mCapacity];
return *this;
}
// 尾插法
void push_back(const T &val)
{
// 判断荣浪是否等于大小
if (this->mCapacity == this->mSize)
{
return;
}
// 在数组的末尾插入数据
this->pAddress[this->mSize] = val;
this->mSize++; // 更新数组大小
}
// 尾部删除
void pop_back()
{
if (this->mSize == 0)
{
return;
}
this->mSize--;
}
// 通过下标的方式访问数组的元素
T& operator[](int index)
{
return this->pAddress[index];
}
// 返回数组的容量
int get_capacity(void)
{
return this->mCapacity;
}
// 返回数组大小
int get_size(void)
{
return this->mSize;
}
// 析构函数
~MyArray()
{
if (this->pAddress != NULL)
{
delete[] pAddress;
pAddress = NULL;
}
}
private:
T *pAddress; // 指针指向堆区开辟的真实数组
int mCapacity; // 容量
int mSize; // 数组大小
};
测试代码
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include <iostream>
#include "myarray.hpp"
using namespace std;
void print_arr(MyArray<int>& arr)
{
for (int i = 0; i < arr.get_size(); i++)
{
cout << arr[i] << " ";
}
cout << endl;
}
void test_01(void)
{
MyArray <int> arr(5);
// 尾部插入法插入数据
for (int i = 0; i < 5; i++)
{
arr.push_back(i);
}
// 打印数组
print_arr(arr);
// 从尾部删除一个数据
arr.pop_back();
print_arr(arr);
// 最后返回数组的大小和容量
cout << "数组容量: " << arr.get_capacity() << " 数组的大小: " << arr.get_size() << endl;
}
int main()
{
test_01();
system("pause");
return 0;
}
结果: