模板
OVERVIEW
- 模板
- 一、函数模板
- 1.func template基本使用:
- 2.func template案例:数组排序
- 3.函数与函数模板的区别&调用规则:
- 4.func template的局限性:
- 二、类模板
- 1.类模板基本使用:
- 2.类模板与函数模板的区别:
- 3.class template中成员函数创建时机:
- 4.class template对象做函数参数:
- 5.class template与继承:
- 6.class template成员函数类外实现:
- 7.class template成员函数分文件编写:
- 8.类模板与友元:
- 三、通用数组类★
- 1.功能分析:
- 2.功能实现:
- 3.功能增加&程序修改:
本阶段主要针对C++泛型编程(利用模板实现)和STL技术做详细讲解,探讨C++更深层的使用。
模板就是通过建立通用的模具,从而提高程序复用性。模板主要可以分为函数模板与类模板。
一、函数模板
1.func template基本使用:
函数模板作用:建立一个通用函数,其函数返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代表(类型参数化)。
template<typename T>
//后紧跟着函数的定义或声明
函数模板 | 说明 |
---|---|
template | 声明创建模板 |
typename | 表明其后面的符号是一种数据类型,可以用class 替代 |
T | 通用的数据类型,名称可以替换(通常为大写的字母) |
#include<iostream>
using namespace std;
//1.声明一个函数模板告诉编译器T是一个通用的数据类型
template<typename T>
void mySwap(T &a, T &b){
T temp = a;
a = b;
b = temp;
}
void test01(){
int a = 10;
int b = 20;
//2.两种方式使用函数模板
//(1)自动类型推导
mySwap(a, b);
cout << "a = " << a << endl;
cout << "b = " << b << endl;
//(2)显示指定类型
double c = 3.14;
double d = 6.28;
mySwap<double>(c, d);
cout << "c = " << c << endl;
cout << "d = " << d << endl;
}
int main(){
test01();
system("pause");
return 0;
}
注意:
函数模板中,自动类型推导必须推导出一致的数据类型T,才可以使用
模板必须要求确定出T的数据类型,才可以使用(如下例中函数没有指出
T
的数据类型,调用时需要使用func<int>()
)template<class T> void func(){ cout << "func的调用" << endl; }
2.func template案例:数组排序
利用函数模板封装一个排序的函数,可以对不同数据类型的数组进行排序(排序规则从大到小,排序算法为选择排序)
#include<iostream>
using namespace std;
//1.mySort交换函数
template<typename T>
void mySwap(T &a, T &b){
T temp = a;
a = b;
b = temp;
}
//2.mySort选择排序函数
template<typename T>
void mySort(T arr[], int len){
for(int i = 0; i < len; ++i){
//(1)认定最大值的下标
int max = i;
//(2)遍历数组中的元素尝试更新最大值max
for(int j = i + 1; j < len; ++j){
if(arr[max] < arr[j]) max = j;
}
//(3)如果认定的最大值与遍历后的最大值不相等(最大值max是否被更新),则交换max与i下标的元素值
if(max != i) mySwap(arr[max], arr[i]);
}
}
//3.printArray数组输出函数
template<typename T>
void printArray(T arr[], int len){
for(int i = 0; i < len; ++i){
cout << arr[i] << " ";
}
cout << endl;
}
void test01(){
char charArr[] = "badcfe";
int len = sizeof(charArr) / sizeof(char);
mySort(charArr, len);
printArray(charArr, len);
}
void test02(){
int intArr[] = {7, 5, 4, 8, 10, 1, 6, 4};
int len = sizeof(intArr) / sizeof(int);
mySort(intArr, len);
printArray(intArr, len);
}
int main(){
test01();
test02();
system("pause");
return 0;
}
3.函数与函数模板的区别&调用规则:
普通函数与函数模板的区别:
- 普通函数调用时可以发生自动类型转换(隐式类型转换)
- 函数模板调用时(如果是利用自动类型推导来调用),不会发生隐式类型转换
- 函数模板调用时(如果是利用显示指定类型来调用),可以发生隐式类型转换
#include<iostream>
using namespace std;
//1.普通函数
int myAdd01(int a, int b){
return a + b;
}
//2.函数模板
template<class T>
T myAdd02(T a, T b){
return a + b;
}
void test01(){
int a = 10;
int b = 20;
char c = 'c';
//1.普通函数调用
cout << myAdd01(a, b) << endl;
cout << myAdd01(a, c) << endl;
//2.函数模板自动类型推导调用
cout << myAdd02(a, b) << endl;
//cout << myAdd02(a, c) << endl;(报错不会发生隐式类型转换)
//3.函数模板显示指定类型调用
cout << myAdd02<int>(a, b) << endl;
cout << myAdd02<int>(a, c) << endl;
}
int main(){
test01();
system("pause");
return 0;
}
总结:建议使用显示指定类型的方式调用函数模板,可以自己确定通用类型T
普通函数与函数模板的调用规则:
- 如果普通函数和函数模板都可以调用,则优先调用普通函数。
- 如果普通函数和函数模板都可以调用,通过空模板参数列表来强制调用函数模板。
- 如果函数模板可产生更好的匹配,则优先调用函数模板。
注意:函数模板也可以发生重载
#include<iostream>
using namespace std;
void myPrint(int a, int b){
cout << "调用的普通函数" << endl;
}
template<typename T>
void myPrint(T a, T b){
cout << "调用的函数模板" << endl;
}
void test01(){
cout << "test01:如果普通函数和函数模板都可以调用,则优先调用普通函数" << endl;
int a = 10;
int b = 10;
myPrint(a, b);
}
void test02(){
cout << "test02:如果普通函数和函数模板都可以调用,通过空模板参数列表来强制调用函数模板" << endl;
int a = 10;
int b = 20;
myPrint<>(a, b);
}
void test03(){
cout << "test03:如果函数模板可产生更好的匹配,则优先调用函数模板" << endl;
char c1 = 'a';
char c2 = 'b';
myPrint(c1, c2);
}
int main(){
test01();
test02();
test03();
system("pause");
return 0;
}
总结:如果提供了函数模板,就最好不要再提供普通函数,否则容易出现二义性。
4.func template的局限性:
当传入的T数据类型为数组、类(例如:Person)等数据类型时,template模板就无法使用了。
c++为了解决这种问题,使其提供的模板template可以实现重载,可以为这些特定的数据类型提供更具体的模板。
#include<iostream>
#include<string>
using namespace std;
class Person{
public:
Person(string n, int a){
this->name = n;
this->age = a;
}
string name;
int age;
};
template<typename T>
bool myCompare(T &sub1, T &sub2){
if(sub1 == sub2){
cout << "sub1 == sub2" << endl;
return true;
}else{
cout << "sub1 != sub2" << endl;
return false;
}
}
//在函数头前添加template<>具体化Person的版本实现模板函数,具体化优先调用
template<> bool myCompare(Person &sub1, Person &sub2){
if(sub1.name == sub2.name && sub1.age == sub2.age){
cout << "sub1 == sub2" << endl;
return true;
}else{
cout << "sub1 != sub2" << endl;
return false;
}
}
void test01(){
int a = 10;
int b = 20;
myCompare(a, b);
}
void test02(){
Person p1("Tom", 10);
Person p2("John", 10);
Person p3("Tom", 10);
/*
1.需要重载Person类型之间的比较运算符==
2.再提供可以接收Person数据类型的函数模板
*/
myCompare(p1, p2);
myCompare(p1, p3);
}
int main(){
test01();
test02();
system("pause");
return 0;
}
总结:
- 利用具体化的模板,可以解决自定义类型的通用化。
- 在STL中系统提供了大量的模板。
二、类模板
1.类模板基本使用:
类模板的作用,建立一个通用类,类中的成员数据类型可以不具体制定,用一个虚拟的类型来代表。
template<typename T>
//后紧跟着类定义
函数模板 | 说明 |
---|---|
template | 声明创建模板 |
typename | 表明其后面的符号是一种数据类型,可以用class 替代 |
T | 通用的数据类型,名称可以替换(通常为大写的字母) |
#include<iostream>
using namespace std;
template<class NameType, class AgeType>
class Person{
public:
Person(NameType n, AgeType a){
this->name = n;
this->age = a;
}
void showPerson(){
cout << "name:" << this->name << "\tage:" << this->age << endl;
}
NameType name;
AgeType age;
};
void test01(){
Person<string, int> p1("lch", 20);
p1.showPerson();
}
int main(){
test01();
system("pause");
return 0;
}
总结:类模板与函数模板语法非常相似,在声明模板template后面加类,称为模板类。
2.类模板与函数模板的区别:
- 类模板没有自动类型推导的使用方式(只有显示指定类型)。
- 类模板在模板参数列表中可以有默认参数(函数模板无法使用)。
#include<iostream>
using namespace std;
template<class NameType, class AgeType = int>
class Person{
public:
Person(NameType n, AgeType a){
this->name = n;
this->age = a;
}
void showPerson(){
cout << "name:" << this->name << "\tage:" << this->age << endl;
}
NameType name;
AgeType age;
};
void test01(){
//Person p1("lch", 20);//1.类模板没有自动类型推导的使用方式只有显示指定类型
Person<string, int> p1("lch", 21);
p1.showPerson();
Person<string> p2("luochenhao", 21);//2.类模板在模板参数列表中可以有默认参数
p2.showPerson();
}
int main(){
test01();
system("pause");
return 0;
}
3.class template中成员函数创建时机:
普通类中的成员函数开始就能够创建,类模板中的成员函数在被调用时才能够创建。
#include<iostream>
using namespace std;
class Cock{
public:
void showCock(){
cout << "It's a Cock, bukbukbuk!" << endl;
}
};
class Cat{
public:
void showCat(){
cout << "It's a Cat, miaomiaomiao~" << endl;
}
};
template<class T>
class Animal{
public:
T obj;
void Cock(){
obj.showCock();
}
void Cat(){
obj.showCat();
}
};
void test01(){
Animal<Cock> animal1;
animal1.Cock();
//animal1.Cat();无法调用
Animal<Cat> animal2;
//animal2.Cock();无法调用
animal2.Cat();
}
int main(){
test01();
system("pause");
return 0;
}
总结:
由于无法确定类模板T的数据类型(Cock or Cat),故类模板中的成员函数不会创建;
直到成员函数被调用时(数据类型已经确定),才能够创建成员函数。
4.class template对象做函数参数:
类模板实例化出的对象,向函数传递参数的方式有3种:
- 指定传入的类型(直接显示对象的数据类型)
- 参数模板化(将对象中的参数变为模板进行传递)
- 类模板化(将这个对象类型变为模板进行传递)
#include<iostream>
using namespace std;
template<class T1, class T2>
class Person{
public:
Person(T1 n, T2 a){
this->name = n;
this->age = a;
}
void showPerson(){
cout << "姓名:" << this->name << "\t年龄:" << this->age << endl;
}
T1 name;
T2 age;
};
//1.指定传入类型
void printPerson1(Person<string, int> &p){
p.showPerson();
}
void test01(){
Person<string, int> p("lch", 21);
printPerson1(p);
}
//2.参数模板化
template<class T1, class T2>
void printPerson2(Person<T1, T2> &p){
p.showPerson();
cout << "T1的类型为:" << typeid(T1).name() << endl;
cout << "T2的类型为:" << typeid(T2).name() << endl;
}
void test02(){
Person<string, int> p("lch", 21);
printPerson2(p);
}
//3.整个类模板化
template<class T>
void printPerson3(T &p){
p.showPerson();
cout << "T的类型为:" << typeid(T).name() << endl;
}
void test03(){
Person<string, int> p("lch", 21);
printPerson3(p);
}
int main(){
test01();
test02();
test03();
system("pause");
return 0;
}
注意:指定参数传入的类型是使用最为广泛的一种传参方式。
5.class template与继承:
当类模板遇到继承问题时,应注意:
- 当子类继承父类时一个类模板时,子类在声明时需要指定出父类中T的类型(不指定则无法给子类分配内存)。
- 如果想灵活的指定出父类中T的类型,子类也需要变成类模板。
#include<iostream>
using namespace std;
//类模板与继承
template<class T>
class Base{
public:
T m;
};
//1.class Son:public Base{};报错,必须要知道父类中T的数据类型才能继承给子类
class Son1:public Base<int>{};
//2.如果想灵活指定父类中T的类型,子类也需要变成类模板
template<class T1, class T2>
class Son2:public Base<T1>{
public:
Son2(){
cout << "T1的数据类型为:" << typeid(T1).name() << endl;
cout << "T2的数据类型为:" << typeid(T2).name() << endl;
}
T2 obj;
};
int main(){
Son1 s1;
Son2<int, char> s2;
system("pause");
return 0;
}
总结:如果父类是类模板,子类需要指定出父类中T的数据类型
6.class template成员函数类外实现:
类模板的成员函数的类内实现:
template<class T1, class T2>
class Person{
public:
Person(T1 n, T2 a){
this->name = n;
this->age = a;
}
void showPerson(){
cout << "name:" << this->name << "age:" << this->age << endl;
}
T1 name;
T2 age;
};
类模板的成员函数的类外实现:
#include<iostream>
using namespace std;
template<class T1, class T2>
class Person{
public:
Person(T1 n, T2 a);
void showPerson();
T1 name;
T2 age;
};
//1.构造函数的类外实现
template<class T1, class T2>
Person<T1, T2>::Person(T1 n, T2 a){
this->name = n;
this->age = a;
}
//2.成员函数的类外实现
template<class T1, class T2>
void Person<T1, T2>::showPerson(){
cout << "name:" << this->name << "\tage:" << this->age << endl;
}
int main(){
Person<string, int> p("lch", 21);
p.showPerson();
system("pause");
return 0;
}
注意:类模板中的成员函数类外实现时,需要加上模板参数列表,例
void Person<T1, T2>::showPerson()
。
7.class template成员函数分文件编写:
类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到:
解决方案有2种:
- 方法1:直接包含cpp源文件(使用较少)
- 方法2:将
.h
声明和.cpp
实现写到同一个文件中,并更改后缀名为hpp(hpp是约定的名称/非强制)
#ifndef PERSON_H
#define PERSON_H
#pragma once
#include <iostream>
using namespace std;
template<class T1, class T2>
class Person{
public:
Person(T1 n, T2 a);
void showPerson();
T1 name;
T2 age;
};
//1.构造函数的类外实现
template<class T1, class T2>
Person<T1, T2>::Person(T1 n, T2 a){
this->name = n;
this->age = a;
}
//2.成员函数的类外实现
template<class T1, class T2>
void Person<T1, T2>::showPerson(){
cout << "name:" << this->name << "\tage:" << this->age << endl;
}
#endif // PERSON_H
#include <iostream>
#include "Person.hpp"
using namespace std;
int main(){
Person<string, int> p("lch", 18);
p.showPerson();
system("pause");
return 0;
}
程序成功运行
8.类模板与友元:
实现类模板配合友元函数的类内和类外实现。
- 全局函数类内实现:直接在类内声明友元
- 全局函数类外实现:需要提前让编译器知道全局函数的存在(重点)
#include<iostream>
using namespace std;
//通过全局函数打印Person信息
template<class T1, class T2>
class Person{
//全局函数类内实现
friend void printPerson(Person<T1, T2> p){
cout << "name:" << p.name << "age:" << p.age << endl;
}
public:
Person(T1 n, T2 a){
this->name = n;
this->age = a;
}
private:
T1 name;
T2 age;
};
int main(){
Person<string, int> p("lch", 21);
printPerson(p);
system("pause");
return 0;
}
#include<iostream>
using namespace std;
template<class T1, class T2>
class Person;
//全局函数类外实现
template<class T1, class T2>
void printPerson(Person<T1, T2> p){
cout << "name:" << p.name << "\tage:" << p.age << endl;
}
template<class T1, class T2>
class Person{
//全局函数类外实现(加一个空模板的参数列表)
friend void printPerson<>(Person<T1, T2> p);
public:
Person(T1 n, T2 a){
this->name = n;
this->age = a;
}
private:
T1 name;
T2 age;
};
int main(){
Person<string, int> p("lch", 21);
printPerson(p);
system("pause");
return 0;
}
三、通用数组类★
1.功能分析:
- 可以对内置数据类型以及自定义数据类型的数据进行存储
- 将数组中的数据存储到堆区
- 构造函数中可以传入数组的容量。
- 提供对应的拷贝构造函数以及
operator=
防止浅拷贝问题。
2.功能实现:
#ifndef MYARRAY_H
#define MYARRAY_H
#pragma once
#include <iostream>
using namespace std;
template<class T>
class MyArray{
public:
//实现有参构造
MyArray(int capa){
cout << "MyArray有参构造调用" << endl;
this->capacity = capa;
this->size = 0;
this->pAddress = new T[this->capacity];
}
//防止浅拷贝问题需要重写拷贝构造函数
MyArray(const MyArray &arr){
cout << "MyArray拷贝构造调用" << endl;
this->capacity = arr.capacity;
this->size = arr.size;
//this->pAddress = arr.pAddress;指针不能这样直接赋值,会导致浅拷贝问题堆区内存重复释放
//(1)重新在堆区开辟一个数组空间,让指针进行维护
this->pAddress = new T[arr.capacity];
//(2)将arr中原有的数据拷贝到新开辟的空间中
for(int i = 0; i < this->size; ++i) this->pAddress[i] = arr.pAddress[i];
}
//重载operator=防止浅拷贝问题
MyArray& operator=(const MyArray &arr){
cout << "MyArray的operator=调用" << endl;
//(1)先判断原来堆区是否有数据,如果有先释放
if(this->pAddress != NULL){
delete[] this->pAddress;
this->pAddress = NULL;
this->capacity = 0;
this->size = 0;
}
//(2)深拷贝
this->capacity = arr.capacity;
this->size = arr.size;
this->pAddress = new T[arr.capacity];
for(int i = 0; i < this->size; ++i) this->pAddress[i] = arr.pAddress[i];
return *this;
}
//堆区开辟的数据需要析构函数手动释放
~MyArray(){
if(this->pAddress != NULL){
cout << "MyArray析构函数调用" << endl;
delete[] this->pAddress;//清空数组中的数据
this->pAddress = NULL;//将指针置空防止野指针
}
}
private:
T *pAddress;//指针指向堆区开辟的真实数组
int capacity;//数组容量
int size;//数组元素个数
};
#endif // MYARRAY_H
#include <iostream>
#include "MyArray.hpp"
using namespace std;
void test01(){
MyArray<int> arr1(10);//测试有参构造功能
MyArray<int> arr2(arr1);//测试拷贝构造功能
MyArray<int> arr3(100);
arr3 = arr1;//测试=运算符重载
}
int main(){
test01();
system("pause");
return 0;
}
3.功能增加&程序修改:
- 提供尾插法和尾删法对数组中的数据进行增加和删除。
- 可以通过下标的方式访问数组中的元素(重载数组中的
[]
实现访问操作)。 - 可以获取数组中当前元素个数和数组容量。
#ifndef MYARRAY_H
#define MYARRAY_H
#pragma once
#include <iostream>
using namespace std;
template<class T>
class MyArray{
public:
//1.实现有参构造
MyArray(int capa){
this->capacity = capa;
this->size = 0;
this->pAddress = new T[this->capacity];
}
//2.防止浅拷贝问题需要重写拷贝构造函数
MyArray(const MyArray &arr){
this->capacity = arr.capacity;
this->size = arr.size;
//this->pAddress = arr.pAddress;指针不能这样直接赋值,会导致浅拷贝问题堆区内存重复释放
//(1)重新在堆区开辟一个数组空间,让指针进行维护
this->pAddress = new T[arr.capacity];
//(2)将arr中原有的数据拷贝到新开辟的空间中
for(int i = 0; i < this->size; ++i) this->pAddress[i] = arr.pAddress[i];
}
//3.重载operator=防止浅拷贝问题
MyArray& operator=(const MyArray &arr){
//(1)先判断原来堆区是否有数据,如果有先释放
if(this->pAddress != NULL){
delete[] this->pAddress;
this->pAddress = NULL;
this->capacity = 0;
this->size = 0;
}
//(2)深拷贝
this->capacity = arr.capacity;
this->size = arr.size;
this->pAddress = new T[arr.capacity];
for(int i = 0; i < this->size; ++i) this->pAddress[i] = arr.pAddress[i];
return *this;
}
//4.尾插法实现数据增加
void Push_Back(const T &val){
//(1)判断容量已经满
if(this->capacity == this->size) return;
//(2)进行数据插入
this->pAddress[this->size] = val;
this->size++;
}
//5.尾删法实现数据删除
void Pop_Back(){
//(1)判断容量已经空
if(this->size == 0) return;
//(2)进行数据删除
this->size--;
}
//6.用户通过下标的方式访问数组中的元素(如果函数调用想作为等号的左值存在arr[0]=100;需要返回T&)
T& operator[](int index){
return this->pAddress[index];
}
//7.返回数组的容量
int getCapacity(){
return this->capacity;
}
//8.返回数组的个数
int getSize(){
return this->size;
}
//9.堆区开辟的数据需要析构函数手动释放
~MyArray(){
if(this->pAddress != NULL){
delete[] this->pAddress;//清空数组中的数据
this->pAddress = NULL;//将指针置空防止野指针
}
}
private:
T *pAddress;//指针指向堆区开辟的真实数组
int capacity;//数组容量
int size;//数组元素个数
};
#endif // MYARRAY_H
#include <iostream>
#include "MyArray.hpp"
using namespace std;
void printArray(MyArray<int> &arr){
cout << ">>>数组内容打印如下:" << endl;
for(int i = 0; i < arr.getSize(); ++i){
cout << arr[i] << " ";
}
cout << endl;
}
void test01(){
//arr1数组测试
MyArray<int> arr1(10);
for(int i = 0; i < 7; ++i) arr1.Push_Back(i);
printArray(arr1);
cout << "数组的容量为:" << arr1.getCapacity() << endl;
cout << "数组中的元素个数为:" << arr1.getSize() << endl;
//arr2数组测试
MyArray<int> arr2(arr1);
arr2.Pop_Back();
printArray(arr2);
cout << "数组的容量为:" << arr2.getCapacity() << endl;
cout << "数组中的元素个数为:" << arr2.getSize() << endl;
}
class Person{
public:
Person(){};
Person(string n, int a){
this->name = n;
this->age = a;
}
string name;
int age;
};
void printPersonArray(MyArray<Person> &arr){
cout << ">>>数组内容打印如下:" << endl;
for(int i = 0; i < arr.getSize(); ++i){
cout << "姓名:" << arr[i].name << "\t年龄:" << arr[i].age << endl;
}
}
void test02(){
MyArray<Person> arr(10);
Person p1("lch", 21);
Person p2("cxk", 18);
Person p3("kfc", 28);
Person p4("gkd", 21);
arr.Push_Back(p1);
arr.Push_Back(p2);
arr.Push_Back(p3);
arr.Push_Back(p4);
printPersonArray(arr);
cout << "数组的容量为:" << arr.getCapacity() << endl;
cout << "数组中的元素个数为:" << arr.getSize() << endl;
}
int main(){
test01();
test02();
system("pause");
return 0;
}