一、函数模板
1、概述
(1)函数模板是一系列相关函数的模型或样板,这些函数的源代码形式相同,只是所针对的数据类型不同 ,对于函数模板,数据类型本身成了它的参数,因而是一种参数化类型的函数。
(2)声明一个函数模板的格式:
template< <模板形参表> > <函数声明>
①模板形参表由一个或多个模板形参组成,各模板形参之间用逗号隔开。
②每个模板形参具有以下几种形式:
typename <参数名>
class <参数名> //前两种是等价的,typename和class可以互换,它们声明的是虚拟类型参数
<类型修饰> <参数名> //声明的参数称为常规参数
(3)定义好函数模板后,编译系统将依据每一次对函数模板调用时实际所使用的数据类型生成适当的调用代码,并生成相应的函数版本。编译系统生成函数模板的某个具体函数版本的过程称为函数模板的实例化,每一个实例就是一个函数定义。
cpp
#include<iostream>
using namespace std;
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;
}
template<typename T> //声明一个模板,告诉编译器后面代码中紧跟着的T不要报错,T是一个通用数据类型
void mySwap(T &a, T &b)
{
T temp = a;
a = b;
b = temp;
}
int main() {
int a = 10;
int b = 20;
//swapInt(a, b);
mySwap(a, b); //自动类型推导
cout << "a = " << a << endl;
cout << "b = " << b << endl;
double c = 1.1;
double d = 2.2;
//swapDouble(c, d);
mySwap<double>(c, d); //显示指定类型
cout << "c = " << c << endl;
cout << "d = " << d << endl;
system("pause");
return 0;
}
(4)在调用一个模板函数时,编译系统需要足够的信息来判别每个虚拟类型参数所对应的实际类型 ,可以从两个不同的渠道获得这样的信息------模板实参表或模板函数实参表,模板实参的信息优先于函数实参的信息。
<被调用函数名><模板实参表>(<模板函数实参表>)
//模板实参表对应函数模板中的模板形参表
//模板函数实参表对应函数模板中函数声明的函数形参表
①编译系统可以根据实参的数据类型推导出模板形参 ,这就是自动类型推导,编译系统 必须推导出一致的数据类型T (T是通用的数据类型,名称可以替换,通常为大写字母)才可以使用模板函数,换句话说,模板必须要确定出T的数据类型,才可以使用。
②如果从模板函数实参表获得的信息已经能够判定其中部分或全部虚拟类型参数所对应的实际参数,而且它们又正好是参数表中最后的若干参数,则模板实参表中的那几个参数可以省略。如果模板实参表中的实参都被省略了,则连空表<>也可以省略。
③对于某个模板实参,如果从模板函数的实参表中无法获得同样的信息,就不能省略,或者虽然能够获得同样的信息,但在它后面还有其它不能省略的实参,则其自身还是不能省略。
cpp
#include<iostream>
using namespace std;
template<class T> //typename可以替换成class,二者无差别
void mySwap(T &a, T &b)
{
T temp = a;
a = b;
b = temp;
}
template<class T> //typename可以替换成class,二者无差别
void func()
{
cout << "调用func" << endl;
}
int main() {
int a = 10;
int b = 20;
mySwap(a, b); //自动类型推导,必须推导出一致的数据类型T才可以使用
cout << "a = " << a << endl;
cout << "b = " << b << endl;
char c = 'c';
//mySwap(c, d); 推不出一致的数据类型T
//func(); 模板必须要确定出T的数据类型才可以使用
func<int>();
system("pause");
return 0;
}
2、普通函数与函数模板区别
(1)普通函数调用时可以发生自动类型转换(隐式类型转换)。
(2)函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换;如果利用显示指定类型的方式,可以发生隐式类型转换。(建议使用显示指定类型的方式调用函数模板,因为这样可以自己确定通用类型T)
cpp
#include<iostream>
using namespace std;
//普通函数
int myAdd01(int a, int b)
{
return a + b;
}
//函数模板
template<class T>
T myAdd02(T a, T b)
{
return a + b;
}
//使用函数模板时,如果用自动类型推导,不会发生自动类型转换,即隐式类型转换
void test01()
{
int a = 10;
int b = 20;
char c = 'c';
cout << myAdd01(a, c) << endl; //正确,将char类型的'c'隐式转换为int类型 'c' 对应 ASCII码 99
//cout << myAdd02(a, c) << endl; //报错,使用自动类型推导时,不会发生隐式类型转换
cout << myAdd02<int>(a, c) << endl; //正确,如果用显示指定类型,可以发生隐式类型转换
cout << myAdd02(a, (int)c) << endl; //直接对c进行强制转换也是可以的
}
int main() {
test01();
system("pause");
return 0;
}
3、普通函数与函数模板的调用规则
(1)如果函数模板和普通函数都可以实现,优先调用普通函数。
(2)可以通过空模板参数列表来强制调用函数模板。
(3)函数模板也可以发生重载。
(4)如果函数模板可以产生更好的匹配,优先调用函数模板。
cpp
#include<iostream>
using namespace std;
//普通函数与函数模板调用规则
void myPrint(int a, int b)
{
cout << "调用的普通函数" << endl;
}
template<typename T>
void myPrint(T a, T b)
{
cout << "调用的模板" << endl;
}
template<typename T>
void myPrint(T a, T b, T c)
{
cout << "调用重载的模板" << endl;
}
void test01()
{
//1、如果函数模板和普通函数都可以实现,优先调用普通函数
//注意:如果告诉编译器,普通函数是有的,但只是声明没有实现,或者不在当前文件内实现,就会报错找不到
int a = 10;
int b = 20;
myPrint(a, b); //调用普通函数
//2、可以通过空模板参数列表来强制调用函数模板
myPrint<>(a, b); //调用函数模板
//3、函数模板也可以发生重载
int c = 30;
myPrint(a, b, c); //调用重载的函数模板
//4、 如果函数模板可以产生更好的匹配,优先调用函数模板
char c1 = 'a';
char c2 = 'b';
myPrint(c1, c2); //调用函数模板
}
int main() {
test01();
system("pause");
return 0;
}
4、模板的局限性
(1)模板的通用性并不是万能的,例如:
template<class T>
void f(T a, T b)
{
a = b;
}
在上述代码中提供的赋值操作,如果传入的a和b是一个数组,就无法实现了。
(2)再例如:
template<class T>
void f(T a, T b)
{
if(a > b) { ... }
}
在上述代码中,如果T的数据类型传入的是像Person这样的自定义数据类型,也无法正常运行,C++为了解决这种问题提供了模板的重载,程序员可以为这些特定的类型提供具体化的模板。
cpp
#include<iostream>
using namespace std;
#include <string>
class Person
{
public:
Person(string name, int age)
{
this->m_Name = name;
this->m_Age = age;
}
string m_Name;
int m_Age;
};
//普通函数模板
template<class T>
bool myCompare(T& a, T& b)
{
if (a == b)
{
return true;
}
else
{
return false;
}
}
//具体化,显示具体化的原型和定意思以template<>开头,并通过名称来指出类型
//具体化优先于常规模板
template<> bool myCompare(Person &p1, Person &p2)
{
if (p1.m_Name == p2.m_Name && p1.m_Age == p2.m_Age)
{
return true;
}
else
{
return false;
}
}
void test01()
{
int a = 10;
int b = 20;
//内置数据类型可以直接使用通用的函数模板
bool ret = myCompare(a, b);
if (ret)
{
cout << "a == b " << endl;
}
else
{
cout << "a != b " << endl;
}
}
void test02()
{
Person p1("Tom", 10);
Person p2("Tom", 10);
//自定义数据类型,不会调用普通的函数模板
//可以创建具体化的Person数据类型的模板,用于特殊处理这个类型
bool ret = myCompare(p1, p2);
if (ret)
{
cout << "p1 == p2 " << endl;
}
else
{
cout << "p1 != p2 " << endl;
}
}
int main() {
test01();
test02();
system("pause");
return 0;
}
5、举例
cpp
#include<iostream>
using namespace std;
template<class T>
void mySwap(T &a, T &b)
{
T temp = a;
a = b;
b = temp;
}
template<class T>
void mySort(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]) //认定的最大值比遍历出的数值要小,说明j下标的元素才是真正的最大值
{
max = j; //更新最大值下标
}
}
if (max != i)
{
mySwap(arr[max], arr[i]);
}
}
}
template<class T>
void myPrintf(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(charArr[0]);
mySort(charArr, len);
myPrintf(charArr, len);
}
void test02()
{
int intArr[] = { 1,2,3,4,5,6 };
int len = sizeof(intArr) / sizeof(intArr[0]);
mySort(intArr, len);
myPrintf(intArr, len);
}
int main() {
test01();
test02();
system("pause");
return 0;
}
二、类模板
1、概述
(1)类模板就是一系列相关类的模型或样板,这些类的成员组成相同,成员函数的源代码形式相同,不同的只是所针对的类型(成员的类型以及成员函数的参数和返回值的类型)。
(2)声明一个类模板的格式:
template< <模板形参表> > <类声明>
①模板形参表由一个或多个模板形参组成,各模板形参之间用逗号隔开。
②与一般的类声明的不同之处在于,这里的类声明要用模板形参表中声明的虚拟类型参数来修饰它的某些成员。
(3)在模板外定义的成员函数是一个函数模板(成员函数可在类模板内定义),其定义格式为:
template<模板形参表>
<返回类型> <类名>< <模板形参表> >::<函数名>(<函数形参表>)
{
<函数体>
}
cpp
#include<iostream>
using namespace std;
#include<string>
template<class T1,class T2>
class Person
{
public:
T1 m_Name;
T2 m_Age;
Person(T1 name, T2 age);
void showPerson();
};
template<class T1, class T2>
Person<T1,T2>::Person(T1 name, T2 age)
{
this->m_Age = age;
this->m_Name = name;
}
template<class T1, class T2>
void Person<T1, T2>::showPerson()
{
cout << "姓名:" << this->m_Name << " 年龄: " << this->m_Age << endl;
}
void test01()
{
Person<string, int>P("张三", 10);
P.showPerson();
}
int main() {
test01();
system("pause");
return 0;
}
(4)用类模板定义对象的格式为:
<类名>< <模板形参表> > <对象名>
<类名>< <模板形参表> > <对象名>(<构造函数实参表>)
cpp
#include<iostream>
using namespace std;
#include <string>
//类模板
template<class NameType, class AgeType>
class Person
{
public:
Person(NameType name, AgeType age)
{
this->mName = name;
this->mAge = age;
}
void showPerson()
{
cout << "name: " << this->mName << " age: " << this->mAge << endl;
}
public:
NameType mName;
AgeType mAge;
};
void test01()
{
// 指定NameType 为string类型,AgeType 为 int类型
Person<string, int>P1("孙悟空", 999);
P1.showPerson();
}
int main() {
test01();
system("pause");
return 0;
}
(5)在定义类模板时,可以为模板形参表声明的最后若干个参数设置默认值,而这些有默认值的参数中,最后的若干个对应实参可以在定义对象时省略。
2、类模板与函数模板区别
(1)类模板没有自动类型推导的使用方式,只能用显示指定类型方式。
(2)类模板在模板参数列表中可以有默认参数。
cpp
#include<iostream>
using namespace std;
#include <string>
template<class NameType, class AgeType>
class Person
{
public:
Person(NameType name, AgeType age)
{
this->m_Name = name;
this->m_Age = age;
}
void showPerson()
{
cout << "name: " << this->m_Name << " age: " << this->m_Age << endl;
}
public:
NameType m_Name;
AgeType m_Age;
};
template<class NameType, class AgeType = int>
class Pig
{
public:
Pig(NameType name, AgeType age)
{
this->m_Name = name;
this->m_Age = age;
}
void showPerson()
{
cout << "name: " << this->m_Name << " age: " << this->m_Age << endl;
}
public:
NameType m_Name;
AgeType m_Age;
};
void test01()
{
//Person P1("孙悟空", 999); 类模板没有自动类型推导使用方式
Person<string, int>p("孙悟空", 1000);
p.showPerson();
Pig<string>p2("猪八戒", 2000); //类模板在模板参数列表中可以有默认参数
p2.showPerson();
}
int main() {
test01();
system("pause");
return 0;
}
3、类模板中成员函数创建时机
类模板中成员函数和普通类中成员函数创建时机是有区别的,普通类中的成员函数一开始就可以创建,类模板中的成员函数在调用时才创建。
cpp
#include<iostream>
using namespace std;
class Person1
{
public:
void showPerson1()
{
cout << "Person1 show " << endl;
}
};
class Person2
{
public:
void showPerson2()
{
cout << "Person2 show " << endl;
}
};
template<class T>
class MyClass
{
public:
T obj;
void func1()
{
obj.showPerson1();
}
void func2()
{
obj.showPerson2();
}
};
void test01()
{
MyClass<Person1> t;
t.func1();
//t.func2();
MyClass<Person2> d;
//d.func1();
d.func2();
}
int main() {
test01();
system("pause");
return 0;
}
4、类模板对象做函数参数
(1)通过类模板创建的对象,可以有三种方式向函数中进行传参:
①指定传入的类型------直接显示对象的数据类型(常用)。
②参数模板化------将对象中的参数变为模板进行传递。
③整个类模板化------将这个对象类型模板化进行传递。
(2)举例:
cpp
#include<iostream>
using namespace std;
#include<string>
template<class T1,class T2>
class Person
{
public:
T1 m_Name;
T2 m_Age;
Person(T1 name, T2 age)
{
this->m_Age = age;
this->m_Name = name;
}
void showPerson()
{
cout << "姓名: " << this->m_Name << " 年龄: " << this->m_Age << endl;
}
};
void printPerson1(Person<string, int>&p) //指定传入类型
{
p.showPerson();
}
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;
}
template<class T> //整个类模板化
void printPerson3(T &p)
{
p.showPerson();
cout << "T 的数据类型为: " << typeid(T).name() << endl;
}
void test01()
{
Person<string, int> p("孙悟空", 100);
printPerson1(p);
}
void test02()
{
Person<string, int> p("猪八戒", 90);
printPerson2(p);
}
void test03()
{
Person<string, int> p("唐僧", 900);
printPerson3(p);
}
int main() {
test01();
test02();
test03();
system("pause");
return 0;
}
5、类模板与继承
(1)当类模板碰到继承时,需要注意以下几点:
①当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型,如果不指定,编译器无法给子类分配内存。
②如果想灵活指定出父类中T的类型,子类也需变为类模板。
(2)举例:
cpp
#include<iostream>
using namespace std;
template<class T>
class Base
{
public:
T m;
};
class Son :public Base<int> //要知道父类中T的类型才能继承
{
};
template<class T1,class T2> //要想灵活指定父类中T的类型,子类也需要变为类模板
class Son2 :public Base<T2>
{
public:
T1 obj;
Son2()
{
cout << "T1的类型为:" << typeid(T1).name() << endl;
cout << "T2的类型为:" << typeid(T2).name() << endl;
}
};
void test01()
{
Son s1;
Son2<int, char>s2;
}
int main() {
test01();
system("pause");
return 0;
}
6、类模板分文件编写
(1)类模板中成员函数创建时机是在调用阶段,这将导致分文件在编写时无法链接,为了解决这个问题,++++可以在分文件中直接包含.cpp源文件++++ ,++++或者将声明和实现写到同一个文件中,并更改后缀名为.hpp++++(hpp是约定的名称,并不是强制,仅仅只是主流)。
(2)举例:
①main.cpp:
cpp
#include<iostream>
using namespace std;
#include<string>
#include"person.hpp"
void test01()
{
Person<string, int>P("张三", 10);
P.showPerson();
}
int main() {
test01();
system("pause");
return 0;
}
②person.hpp:
cpp
#pragma once
#include<iostream>
using namespace std;
template<class T1, class T2>
class Person
{
public:
T1 m_Name;
T2 m_Age;
Person(T1 name, T2 age);
void showPerson();
};
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age)
{
this->m_Age = age;
this->m_Name = name;
}
template<class T1, class T2>
void Person<T1, T2>::showPerson()
{
cout << "姓名:" << this->m_Name << " 年龄: " << this->m_Age << endl;
}
③person.cpp:
cpp
/*#include"person.h"
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age)
{
this->m_Age = age;
this->m_Name = name;
}
template<class T1, class T2>
void Person<T1, T2>::showPerson()
{
cout << "姓名:" << this->m_Name << " 年龄: " << this->m_Age << endl;
}*/
7、类模板与友元
(1)全局函数类内实现------直接在类内声明友元即可。(建议全局函数做类内实现,用法简单,而且编译器可以直接识别)
(2)全局函数类外实现------需要提前让编译器知道全局函数的存在。
cpp
#include<iostream>
using namespace std;
#include<string>
template<class T1, class T2> //提前让编译器知道Person类的存在,printPerson2函数要用
class Person;
template<class T1, class T2>
void printPerson2(Person<T1, T2> p) //提前让编译器知道printPerson2函数的存在
{
cout << "(类外实现)姓名: " << p.m_Name << " 年龄: " << p.m_Age << endl;
}
template<class T1, class T2>
class Person
{
friend void printPerson(Person<T1, T2> p) //全局函数类内实现
{
cout << "(类内实现)姓名: " << p.m_Name << " 年龄: " << p.m_Age << endl;
}
friend void printPerson2<>(Person<T1, T2> p); //全局函数类外实现(要加空模板参数列表,要让编译器提前知道它存在)
private:
T1 m_Name;
T2 m_Age;
public:
Person(T1 name, T2 age)
{
this->m_Age = age;
this->m_Name = name;
}
};
void test01()
{
Person<string, int>p("Tom", 20);
printPerson(p);
printPerson2(p);
}
int main() {
test01();
system("pause");
return 0;
}
三、终极案例
1、案例要求
(1)可以对内置数据类型以及自定义数据类型的数据进行存储。
(2)将数组中的数据存储到堆区。
(3)构造函数中可以传入数组的容量。
(4)提供对应的拷贝构造函数以及operator=防止浅拷贝问题。
(5)提供尾插法和尾删法对数组中的数据进行增加和删除。
(6)可以通过下标的方式访问数组中的元素。
(7)可以获取数组中当前元素个数和数组的容量。
2、代码
(1)main.cpp:
cpp
#include<iostream>
using namespace std;
#include<string>
#include"myArray.hpp"
void printArray(myArray <int> & arr)
{
for (int i = 0; i < arr.getSize(); i++)
{
cout << arr[i] << endl;
}
}
void test01()
{
myArray<int> arr1(10);
for (int i = 0; i < 5; i++)
{
arr1.Push_Back(i);
}
cout << "arr1的打印输出为: " << endl;
printArray(arr1);
cout << "arr1的容量为: " << arr1.getCapacity() << endl;
cout << "arr1的大小为: " << arr1.getSize() << endl;
myArray<int> arr2(arr1);
cout << "arr2的打印输出为: " << endl;
printArray(arr2);
arr2.Pop_Back();
cout << "arr2尾删后的打印输出为: " << endl;
printArray(arr2);
cout << "arr2的容量为: " << arr2.getCapacity() << endl;
cout << "arr2的大小为: " << arr2.getSize() << endl;
myArray<int> arr3(10);
arr3 = arr1;
cout << "arr3的打印输出为: " << endl;
printArray(arr3);
cout << "arr3的容量为: " << arr3.getCapacity() << endl;
cout << "arr3的大小为: " << arr3.getSize() << endl;
}
class Person
{
public:
string m_Name;
int m_Age;
Person() {};
Person(string name, int age)
{
m_Name = name;
m_Age = age;
}
};
void printPersonArray(myArray<Person>& arr)
{
for (int i = 0; i < arr.getSize(); i++)
{
cout << "姓名: " << arr[i].m_Name
<< " \t年龄: " << arr[i].m_Age << endl;
}
}
void test02()
{
myArray<Person> arr(10);
Person p1("孙悟空", 999);
Person p2("猪八戒", 666);
Person p3("Tom", 23);
Person p4("Mike", 12);
Person p5("沙僧", 300);
arr.Push_Back(p1);
arr.Push_Back(p2);
arr.Push_Back(p3);
arr.Push_Back(p4);
arr.Push_Back(p5);
cout << "arr的打印输出为: " << endl;
printPersonArray(arr);
cout << "arr的容量为: " << arr.getCapacity() << endl;
cout << "arr的大小为: " << arr.getSize() << endl;
}
int main() {
test01();
test02();
system("pause");
return 0;
}
(2)myArray.hpp:
cpp
#pragma once
#include<iostream>
using namespace std;
template<class T>
class myArray
{
public:
T * pAddress; //指向一个堆空间,这个空间存储真正的数据
myArray(int capacity) //有参构造,容量为参数
{
//cout << "有参构造函数调用" << endl;
this->m_Capacity = capacity;
this->m_Size = 0;
this->pAddress = new T[this->m_Capacity];
}
~myArray() //析构函数
{
//cout << "析构函数调用" << endl;
if (this->pAddress != NULL)
{
delete[]this->pAddress;
this->pAddress = NULL;
}
}
myArray(const myArray& arr) //拷贝构造
{
//cout << "拷贝构造函数调用" << endl;
this->m_Capacity = arr.m_Capacity;
this->pAddress = new T[arr.m_Capacity];
this->m_Size = arr.m_Size;
for (int i = 0; i < this->m_Capacity; i++)
{
pAddress[i] = arr.pAddress[i];
}
}
myArray& operator=(const myArray& arr)
{
//cout << "operator=调用" << endl;
if (this->pAddress != NULL) //先判断原来堆区是否有数据,有就先释放
{
delete[]this->pAddress;
this->pAddress = NULL;
this->m_Capacity = 0;
this->m_Size = 0;
}
this->m_Capacity = arr.m_Capacity;
this->pAddress = new T[arr.m_Capacity];
this->m_Size = arr.m_Size;
for (int i = 0; i < this->m_Capacity; i++)
{
pAddress[i] = arr.pAddress[i];
}
return *this;
}
void Push_Back(const T & val)
{
if (m_Capacity == m_Size) //判断容量是否充足
{
cout << "已经满了!" << endl;
return;
}
pAddress[m_Size] = val; //在数组末尾插入数据
m_Size++;
}
void Pop_Back()
{
if (m_Size == 0) //判断还有没有内容
{
cout << "没东西删了!" << endl;
return;
}
m_Size--; //让用户访问不到最后一个元素即可(逻辑删除)
}
T& operator[](int index)
{
return pAddress[index];
}
int getCapacity()
{
return m_Capacity;
}
int getSize()
{
return m_Size;
}
private:
int m_Capacity; //容量
int m_Size; //大小
};