C++核心编程 day08 C++模板
1. 模板概述
C++中提供了函数模板。函数模板就是建立一个通用的函数,其函数类型和形参的类型都是不具体的,而是使用一个虚拟的类型来代表。这个通用的函数就是函数模板。凡是函数体相同的函数都可以使用这个模板来进行代替,只需要在模板中进行一次定义就行。在调用的时候编译器会根据实际参数的类型来取代函数模板中的虚拟类型,从而实现了不同函数的功能。C++中提供了两种模板机制,一种是函数模板,另外一种是类模板。
2. 函数模板
函数模板的定义是在一个通用的函数前加上template <typename T, ...>
或者template <class T, ...>
。其中在这里面的class
和typename
是一样的,没有任何区别。有一种写法就是函数模板的时候用typename
,而在类模板中则使用class
,当然这个写法仅供参考,更多的会写成class
之类的。这里的T
是一个虚拟的通用数据类型,会告诉编译器下面紧跟的函数或者类中出现了T
不要报错。函数模板不能单独使用,必须指定出T
才能使用。
函数模板会进行自动数据类型推导,但是必须推导出一致的数据类型才能正常使用模板,如果推导不出来,则无法调用。除此之外函数模板需要调用的话也可以显式指定类型。下面给出一个函数模板的示例代码。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
void mySwapInt(int &a, int &b)
{
int tmp = a;
a = b;
b = tmp;
}
void mySwapDouble(double &a, double &b)
{
double tmp = a;
a = b;
b = tmp;
}
// 利用模板实现通用交换函数
template<typename T> // T代表一个通用的数据类型,告诉编译器如果下面紧跟着的函数或者类中出现T不要报错
void mySwap(T &a, T &b)
{
T tmp = a;
a = b;
b = tmp;
}
// 模板不能单独使用,必须指定出T才可以使用
template<typename T>
void mySwap2()
{
}
int main()
{
int a = 10;
int b = 20;
//mySwapInt(a, b);
char ch = 'c';
//1.自动类型推导,必须推导出一致的T数据类型才可能正常使用模板
//mySwap(a, ch); // 推导不出一致的T,因此无法调用
cout << "a = " << a << endl;
cout << "b = " << b << endl;
double c = 3.14;
double d = 1.11;
//mySwapDouble(c, d);
//2.显式指定类型
mySwap2<int>();
mySwap<double>(c, d);
cout << "c = " << c << endl;
cout << "d = " << d << endl;
system("pause");
return 0;
}
既然函数模板是一个通用的模板,那么接下来我们写一个通用的排序函数实现对char
类型和int
类型的数组进行排序。要求排序为降序排序,算法使用选择排序法,代码如下。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
template<typename T>
void mySwap(T &a, T &b)
{
T tmp = a;
a = b;
b = tmp;
}
//需求 通过一个通用排序函数实现对char和int数组的排序,排序顺序从大到小,算法选择排序法
template<typename T> // typename和class一样,这里也可以写成class T
void mySelectSort(T arr[], int len)
{
for (int i = 0; i < len; i++)
{
int max = i;
for (int j = i + 1; j < len; j++)
{
if (arr[j] > arr[max])
{
max = j;
}
}
// 判断 算出的max和开始认定的i是否一致,如果不同交换数据
if (max != i)
{
mySwap(arr[max], arr[i]);
}
}
}
template<typename T>
void printArray(T arr[], int len)
{
for (int i = 0; i < len; i++)
{
cout << arr[i] << " ";
}
cout << endl;
}
int main()
{
char s1[] = "helloworld";
int len = strlen(s1);
mySelectSort(s1, len);
printArray(s1, len);
int arr[] = { 2, 6, 8, -3, 2, -8, 4, 2, 78, 2, -9, 0, 2 };
len = sizeof arr / sizeof arr[0];
mySelectSort(arr, len);
printArray(arr, len);
system("pause");
return 0;
}
3. 函数模板与普通函数区别以及调用规则
那么函数模板与普通函数有什么区别呢?区别主要是函数模板不允许自动类型转换,而普通函数是支持普通类型转换的。
此外,如果普通函数和函数模板在一起,有以下的调用规则。
- C++编译器会优先考虑普通函数。
- 如果想要强行调用函数模板,可以使用空模板实参列表来进行限定。
- 函数模板也可以进行重载。
- 如果函数模板可以产生一个更好的匹配,那么会调用函数模板。
相关的示例代码如下:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
//1.函数模板和普通函数的区别
template<typename T>
T myAdd(T a, T b)
{
return a + b;
}
int myAdd2(int a, int b)
{
return a + b;
}
void test01()
{
int a = 10;
int b = 20;
char c = 'c';
//myAdd(a, c); //如果使用类型自动推导,是不可以发生隐式类型转换的
cout << myAdd2(a, c) << endl; //普通函数可以隐式类型转换
}
//2.函数模板和普通函数的调用规则
template<typename T>
void myPrint(T a, T b)
{
cout << "函数模板调用" << endl;
}
/*
函数模板是通过具体类型产生不同的函数 --- 通过函数模板产生的函数称为模板函数
void myPrint(int a, int b)
{
cout << "函数模板调用" << endl;
}
void myPrint(double a, double b)
{
cout << "函数模板调用" << endl;
}
*/
template<typename T>
void myPrint(T a, T b, T c)
{
cout << "函数模板(T a, T b, T c)调用" << endl;
}
void myPrint(int a, int b)
{
cout << "普通函数调用" << endl;
}
void test02()
{
//1.如果函数模板和普通函数都可以调用,那么优先调用普通函数
int a = 10;
int b = 20;
myPrint(a, b);
//2.如果想强制调用函数模板,可以使用空模板参数列表
myPrint<>(a, b);
//3.函数模板也可以发生函数重载
myPrint(a, b, 10);
//4.如果函数模板能产生更好的匹配,那么优先使用函数模板
char c = 'c';
char d = 'd';
myPrint(c, d);
}
int main()
{
test01();
test02();
system("pause");
return 0;
}
在程序运行中,编译器并不是把函数模板处理成能够处理任何类型数据的函数。函数模板是要通过具体类型产生不同的数据。编译器会对函数模板进行两次编译,在声明的地方对模板代码本身进行编译,检查是否有语法错误。在调用的时候会对替换后的参数进行编译。
4. 模板的局限性
函数模板也有局限性,也就是这个通用的函数模板其实也并不通用。比如我们可以写一个比较两个变量的通用函数模板,但是该模板却不能处理自定义类型的数据的比较。那么怎么解决这个局限性呢?我们可以使用具体化技术去对自定义数据类型提供特殊模板。在使用具体化技术的时候需要在特殊模板前添加template<>
。下面给出一个例子。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
#include <string>
class Person
{
public:
Person(string name, int age)
{
this->name = name;
this->age = age;
}
string name;
int age;
};
// 显示两个变量对比函数
template<class T>
bool myCompare(T &a, T &b)
{
if (a == b)
{
return true;
}
return false;
}
//利用具体化技术,实现对自定义数据类型提供特殊模板
template<> bool myCompare(Person &a, Person &b)
{
if (a.name == b.name && a.age == b.age)
{
return true;
}
return false;
}
int main()
{
int a = 10;
int b = 10;
bool ret = myCompare(a, b);
if (ret)
{
cout << "a == b" << endl;
}
else
{
cout << "a != b" << endl;
}
Person p1("Tom", 20);
Person p2("Tom", 20);
ret = myCompare(p1, p2);
if (ret)
{
cout << "p1 == p2" << endl;
}
else
{
cout << "p1 != p2" << endl;
}
system("pause");
return 0;
}
5. 类模板
类模板和函数模板的定义和使用类似。有时候我们有两个或者多个类,这些类的功能是相同的,但是就只是数据类型不一样。类模板用于实现类所需数据的类型参数化。与函数模板不一样的是,类模板可以为类型参数提供默认参数,同时类模板不允许自动类型推导,只能使用显式指定类型。下面给出一个类模板定义的代码。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
#include <string>
template<class NAMETYPE, class AGETYPE = int> //类模板中可以有默认参数
class Person
{
public:
Person(NAMETYPE name, AGETYPE age)
{
this->name = name;
this->age = age;
}
void showPerson()
{
cout << "name: " << this->name << ", age: " << this->age << endl;
}
NAMETYPE name;
AGETYPE age;
};
int main()
{
//类模板和函数模板区别:
//1.类模板中不可以使用自动类型推导,只能使用显式指定类型
//2.类模板中可以有默认参数
Person<string> p("唐三藏", 88);
p.showPerson();
system("pause");
return 0;
}
类模板中的成员函数也并不是一开始就创建的,而是在运行阶段确定出来了数据类型T
之后才会去创建。如下面代码进行验证。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Person1
{
public:
void showPerson1()
{
cout << "Peson1 show 调用" << endl;
}
};
class Person2
{
public:
void showPerson2()
{
cout << "Person2 show 调用" << endl;
}
};
//类模板中的成员函数,并不是一开始就创建的,而是在运行阶段确定出T的数据类型才去创建
template<class T>
class MyClass
{
public:
void func1()
{
obj.showPersons1();
}
void func2()
{
obj.showPerson2();
}
T obj;
};
int main()
{
MyClass<Person2> p;
//p.func1();
p.func2();
system("pause");
return 0;
}
我们在编写代码的时候MyClass
中的成员函数并不会报错,并且在运行的时候只要Person1
类没有去调用Person2
的成员函数该代码就不会出错。如果不是运行阶段的时候进行创建,那么在编译的时候就会不通过,因为在MyClass
中的obj
调用了其他类的成员函数。
如果我们需要将一个模板类的对象作为一个函数参数又应该怎么传参呢?一共有三个方法,分别是指定传入类型、参数模板化、整个类模板化,示例代码如下。
#define _CRT_SECURE_NO_WARINGS
#include <iostream>
using namespace std;
#include <string>
template<class T1, class T2> //类模板中可以有默认参数
class Person
{
public:
Person(T1 name, T2 age)
{
this->name = name;
this->age = age;
}
void showPerson()
{
cout << "name: " << this->name << ", age: " << this->age << endl;
}
T1 name;
T2 age;
};
//1.指定传入类型
void doWork(Person<string, int> &p)
{
p.showPerson();
}
void test01()
{
Person<string, int> p("孙悟空", 999);
doWork(p);
}
//2.参数模板化
template<typename T1, typename T2>
void doWork2(Person<T1, T2> &p)
{
cout << "T1的数据类型为" << typeid(T1).name() << endl;
cout << "T2的数据类型为" << typeid(T2).name() << endl;
p.showPerson();
}
void test02()
{
Person<string, int> p("猪八戒", 788);
doWork2(p);
}
//3.整个类模板化
template<class T>
void doWork3(T &p)
{
cout << "T的数据类型为" << typeid(T).name() << endl;
p.showPerson();
}
void test03()
{
Person<string, int> p("沙悟净", 679);
doWork3(p);
}
int main()
{
test01();
test02();
test03();
system("pause");
return 0;
}
当一个类继承一个类模板的时候,如果没有指定父类的数据类型,编译器不会给子类分配内存,示例代码如下。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
template<class T>
class Base
{
public:
T m_a;
};
//必须要指定出父类中的T数据类型才能给子类分配内存
class Son : public Base<int>
{
};
template<class T>
class Base2
{
public:
T m_a;
};
template<class T1, class T2>
class Son2 : public Base2<T2>
{
public:
Son2()
{
cout << typeid(T1).name() << endl;
cout << typeid(T2).name() << endl;
}
T1 m_b;
};
int main()
{
Son2<int, double> s;
system("pause");
return 0;
}
在前面我们学习类的编写和定义的时候,我们是将类的定义和实现进行分离的。类模板也可以将成员函数的实现写在类外,示例代码如下。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
#include <string>
template<class T1, class T2>
class Person
{
public:
Person(T1 name, T2 age);
void showPerson();
T1 name;
T2 age;
};
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age)
{
this->name = name;
this->age = age;
}
template<class T1, class T2>
void Person<T1, T2>::showPerson()
{
cout << "name: " << this->name << ", age: " << this->age << endl;
}
int main()
{
Person<string, int> p("孙悟空", 999);
p.showPerson();
system("pause");
return 0;
}
同样如上面的代码,如果要实现成员函数份文件编写,我们可以将他们拆分为一个是.h
的头文件,一个是.cpp
的C++实现文件。但是我们并不建议这样写。因为这样写的话我们需要使用的时候就不能去include
头文件,而是需要去包含.cpp
的文件。原因是类模板需要二次编译,在出现模板的地方编译一次,在调用模板的地方还需要再次编译,所以这样会一直看不见类模板的实现部分。我们的解决方案是将类模板的声明和实现放在一个文件中,这个文件我们也约定为.hpp
结尾。
如果类模板中有友元的存在,则会有点比较麻烦。当类中有友元函数,如果是在类内进行友元实现,一切都很正常。假如现在我们要在类内进行声明,类外进行实现,由于我们的函数是一个模板类的函数而不是一个实际的函数,所以需要对友元函数的函数名后面添加一个模板类型无参列表,并且还需要在类的前面进行声明以及对模板类进行声明,不然就会出错,友元与模板类的示例代码如下。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
#include <string>
template<class T1, class T2>
class Person;
template<class T1, class T2>
void printPerson2(Person<T1, T2> &p);
template<class T1, class T2>
void printPerson3(Person<T1, T2> &p)
{
cout << "name: " << p.name << ", age: " << p.age << endl;
}
template<class T1, class T2>
class Person
{
//1. 友元函数类内实现
friend void printPerson1(Person<T1, T2> &p)
{
cout << "name: " << p.name << ", age: " << p.age << endl;
}
//2. 友元函数类外实现
friend void printPerson2<>(Person<T1, T2> &p);
friend void printPerson3<>(Person<T1, T2> &p);
public:
Person(T1 name, T2 age);
private:
T1 name;
T2 age;
};
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age)
{
this->name = name;
this->age = age;
}
template<class T1, class T2>
void printPerson2(Person<T1, T2> &p)
{
cout << "name: " << p.name << ", age: " << p.age << endl;
}
int main()
{
Person<string, int> p("迪杰斯特拉", 666);
printPerson1(p);
printPerson2(p);
printPerson3(p);
system("pause");
return 0;
}
6. 数组模板类的实现
在数组模板类中,我们需要能实现以下功能,分别是无参构造、有参构造、拷贝构造、析构、获取数组大小、获取数组容量、在尾部插入一个元素、重载赋值符号以及中括号等,示例代码如下。
MyArray.hpp
#ifndef __MYARRAY_HPP__
#define __MYARRAY_HPP__
template<class T>
class MyArray
{
public:
//构造函数
MyArray();
MyArray(int capacity);
MyArray(const MyArray &myArray);
//析构函数
~MyArray();
//运算符重载
MyArray &operator=(MyArray &myArray);
T &operator[](const int &index);
int getCapacity();
int getSize();
void pushBack(const T &val);
private:
T *pAddress; //指向堆区数组指针
int capacity; //数组容量
int size; //数组大小
};
template<class T>
MyArray<T>::MyArray()
{
this->capacity = 100;
this->size = 0;
this->pAddress = new T[this->capacity];
}
template<class T>
MyArray<T>::MyArray(int capacity)
{
this->capacity = capacity;
this->size = 0;
this->pAddress = new T[this->capacity];
}
template<class T>
MyArray<T>::MyArray(const MyArray &myArray)
{
this->capacity = myArray.capacity;
this->size = myArray.size;
this->pAddress = new T[this->size];
memcpy(this->pAddress, myArray.pAddress, sizeof(T) * this->size);
}
//析构函数
template<class T>
MyArray<T>::~MyArray()
{
if (this->pAddress != NULL)
{
delete[] this->pAddress;
this->pAddress = NULL;
}
}
//运算符重载
template<class T>
MyArray<T> &MyArray<T>::operator=(MyArray &myArray)
{
this->capacity = myArray.capacity;
this->size = myArray.size;
this->pAddress = new T[this->capacity];
memcpy(this->pAddress, myArray.pAddress, sizeof(T)* this->size);
}
template<class T>
T &MyArray<T>::operator[](const int &index)
{
return this->pAddress[index];
}
template<class T>
int MyArray<T>::getCapacity()
{
return this->capacity;
}
template<class T>
int MyArray<T>::getSize()
{
return this->size;
}
template<class T>
void MyArray<T>::pushBack(const T &val)
{
if (this->size == this->capacity)
{
return;
}
this->pAddress[this->size++] = val;
}
#endif
测试代码如下。
main.cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
#include <string>
#include <ctime>
#include "MyArray.hpp"
class Person
{
public:
Person(){};
Person(string name, int age)
{
this->name = name;
this->age = age;
}
string name;
int age;
};
ostream &operator<<(ostream &cout, Person &p)
{
cout << "(" << p.name << "," << p.age << ")";
return cout;
}
void myPrintInt(MyArray<int> &myArray)
{
for (int i = 0; i < myArray.getSize(); i++)
{
cout << myArray[i] << " ";
}
cout << endl;
}
void test01()
{
srand((unsigned int)time(NULL));
MyArray<int> myIntArray(20);
for (int i = 0; i < 10; i++)
{
myIntArray.pushBack(rand() % 100);
}
myPrintInt(myIntArray);
}
void myPrintPerson(MyArray<Person> &myPersonArray)
{
for (int i = 0; i < myPersonArray.getSize(); i++)
{
cout << myPersonArray[i] << " ";
}
cout << endl;
}
void test02()
{
MyArray<Person> myPersonArray;
Person p1("孙悟空", 99);
Person p2("猪八戒", 88);
Person p3("沙悟净", 77);
Person p4("唐玄奘", 66);
Person p5("白骨精", 55);
Person p6("哪吒", 44);
Person p7("白龙马", 33);
Person p8("李靖", 22);
myPersonArray.pushBack(p1);
myPersonArray.pushBack(p2);
myPersonArray.pushBack(p3);
myPersonArray.pushBack(p4);
myPersonArray.pushBack(p5);
myPersonArray.pushBack(p6);
myPersonArray.pushBack(p7);
myPersonArray.pushBack(p8);
myPrintPerson(myPersonArray);
cout << "数组的容量为: " << myPersonArray.getCapacity() << endl;
cout << "数组的大小为: " << myPersonArray.getSize() << endl;
}
int main()
{
test01();
test02();
system("pause");
return 0;
}