第十六章 模板与泛型编程

C++模板

模板概论

c++提供了函数模板(function template.)所谓函数模板,实际上是建立一个通用函数,其函数类型和形参类型不具体制定,用一个虚拟的类型来代表。这个通用函数就成为函数模板。凡是函数体相同的函数都可以用这个模板代替,不必定义多个函数,只需在模板中定义一次即可。在调用函数时系统会根据实参的类型来取代模板中的虚拟类型,从而实现不同函数的功能。
  • c++提供两种模板机制:函数模板和类模板
  • 类属 - 类型参数化,又称参数模板

总结:

  • 模板把函数或类要处理的数据类型参数化,表现为参数的多态性,成为类属。
  • 模板用于表达逻辑结构相同,但具体数据元素类型不同的数据对象的通用行为。

函数模板

函数模板基本使用:

  1. template 或者 template,告诉编译器,紧跟的代码里出现T不要报错
  2. mySwap(T &a,T &B)类型也需要传入,类型参数化
  3. mySwap(a,b) 自动类型推导,按照a,b的类型来推导、替换T,此时,要确保,a,b的类型是一致的,不会出现二义性。
  4. mySwap(a,b) 显式指定的类型

函数模板和普通函数区别

函数模板不允许自动类型转化,普通函数能够自动进行类型转化

普通函数和函数模板的调用规则

1. 如果出现普通函数与模板重载,优先使用普通函数调用,如果没有实现,就会报错
2. 如果像强制调用模板,那么可以使用空参列表myPrint<>(a, b);
3. 函数模板也可以发生重载
4. 如果函数模板可以产生更好的匹配,那么优先使用函数模板
点击查看代码
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;
//1.普通函数与函数模板的区别
template<class T>
T myPlus(T a, T b)
{
return a + b;
}
int myPlus2(int a, int b)
{
return a + b;
}
//2.普通函数和函数模板的调用规则
template<class T>
void myPrint(T a, T b)
{
cout << "模板函数调用的myPrint(a,b)" << endl;
}
//3.函数模板的重载
template<class T>
void myPrint(T a, T b,T c)
{
cout << "模板函数调用的myPrint(a,b,c)" << endl;
}
void myPrint(int a, int b)
{
cout << "普通函数调用的myPrint" << endl;
}
void test01()
{
int a = 10;
int b = 20;
char c = 'c';
//myPlus(a, c);//类型推导不出来 ,模板函数不可以进行类型转换
cout << myPlus2(a, c) << endl;//可以 109 a = 10,c = 99
}
void test02()
{
int a = 10;
int b = 20;
//1.如果出现普通函数与模板重载,优先使用普通函数调用,如果没有实现,就会报错
myPrint(a, b);
//2.如果像强制调用模板,那么可以使用空参列表
myPrint<>(a, b); //模板函数调用的myPrint
//3.函数模板也可以发生重载
myPrint<>(a, b);
//4.如果函数模板可以产生更好的匹配,那么优先使用函数模板
char c = 'c';
char d = 'd';
myPrint(c, d);
}
int main()
{
//test01();
test02();//普通函数调用的myPrint
system("pause");
return 0;
}

image

模板机制剖析

思考:

1. 为什么函数模板可以和普通函数放在一起?
2. c++编译器是如何实现函数模板机制的?

C/C++语言程序的编译过程

hello.cpp程序是高级c语言程序,这种程序易于被人读懂。为了在系统上运行hello.c程序,每一条c语句都必须转化为低级的机器指令。
然后将这些机器指令打包成可执行目标文件格式,并以二进制形式存储于磁盘中。
预处理(Pre-processing) -> 编译(Compiling) ->汇编(Assembling) -> 链接(Linking)

image

函数模板机制结论:

  • 编译器并不是把函数模板处理成能够处理任何类型的函数
  • 函数模板通过具体类型产生不同的函数
  • 编译器会对函数模板进行两次编译,在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。
    • 第一次编译:检查有没有语法错误
    • 第二次编译:生成类型替换后的代码,检查有没有错误

函数模板的局限性

  1. 模板不能解决所有类型
  2. 如果出现不能解决的类型,可以通过第三代具体化来解决问题
  3. 第三代具体化语法:
  4. template<> 返回值 函数名<具体类型>(参数)
  5. 返回值和函数名要与具体化之前的一致,不然会报错

假设有如下模板函数:

template<class T>
void f(T a, T b)
{
}
  • 如果代码实现时定义了赋值操作 a = b,但是T为数组,这种假设就不成立了
  • 同样,如果里面的语句为判断语句 if (a>b), 但T如果是结构体,该假设也不成立,另外如果是传入的数组,数组名为地址,因此它比较的是地址,而这也不是我们所希望的操作。
  • 总之,编写的模板函数很可能无法处理某些类型,另一方面,有时候通用化是有意义的,但C++语法不允许这样做。为了解决这种问题,可以提供模板的重载,为这些特定的类型提供具体化的模板。
class Person
{
public:
Person(string name, int age)
{
this->mName = name;
this->mAge = age;
}
string mName;
int mAge;
};
//普通交换函数
template <class T>
void mySwap(T &a, T &b)
{
T temp = a;
a = b;
b = temp;
}
//第三代具体化,显示具体化的原型和定意思以template<>开头,并通过名称来指出类型
//具体化优先于常规模板
template<>void mySwap<Person>(Person &p1, Person &p2)
{
string nameTemp;
int ageTemp;
nameTemp = p1.mName;
p1.mName = p2.mName;
p2.mName = nameTemp;
ageTemp = p1.mAge;
p1.mAge = p2.mAge;
p2.mAge = ageTemp;
}
void test()
{
Person P1("Tom", 10);
Person P2("Jerry", 20);
cout << "P1 Name = " << P1.mName << " P1 Age = " << P1.mAge << endl;
cout << "P2 Name = " << P2.mName << " P2 Age = " << P2.mAge << endl;
mySwap(P1, P2);
cout << "P1 Name = " << P1.mName << " P1 Age = " << P1.mAge << endl;
cout << "P2 Name = " << P2.mName << " P2 Age = " << P2.mAge << endl;
}
点击查看代码
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;
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;
}
return false;
}
//通过具体化,来自定义数据类型,解决上述问题
//如果两个都能匹配,优先匹配具体化的
//如果具体的匹配不到了,就去找通用的,再不行就error
//语法:template<> 返回值 函数名<具体类型>(参数)
template<> bool myCompare<Person>(Person &a, Person &b)
{
if (a.m_Age == b.m_Age)
{
return true;
}
return false;
}
void test01()
{
int a = 10;
int b = 20;
int ret = myCompare(a, b);
cout << "ret = " << ret << endl;//0
Person p1("tom",10);
Person p2("jerry",10);
//ret = myCompare(p1.m_Age, p2.m_Age);//0
//如果两个都能匹配,优先匹配具体化的
//如果具体的匹配不到了,就去找通用的,再不行就error
ret = myCompare(p1, p2);//exception
cout << "ret = " << ret << endl;//0
}
int main()
{
test01();
system("pause");
return 0;
}

类模板

类模板基本概念

类模板和函数模板的定义和使用类似,我们已经进行了介绍。有时,有两个或多个类,其功能是相同的,仅仅是数据类型不同。

类模板用于实现类所需数据的类型参数化

类模板的基本使用

写法:template<T...>紧接着是类

函数模板可以进行自动类型推导,而类模板不可以
函数模板不可以有默认数据类型,而类模板可以

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()
{
//Person P1("德玛西亚",18); // 类模板不能进行类型自动推导
Person<string, int>P1("德玛西亚", 18);
P1.showPerson();
}
点击查看代码
#define _CRT_SECURE_NO_WARNINGS
#include<string>
#include<iostream>
using namespace std;
//类模板
//类模板可以有默认类型,函数模板不可以有
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;
}
NameType m_Name;
AgeType m_Age;
};
void test01()
{
//类模板,不支持自动类型推导
//Person p("monkey",100);
//显式指定类型
Person<string, int> p("monkey", 100);
p.showPerson();
}
int main()
{
test01();
system("pause");
return 0;
}

成员函数的创建时机

点击查看代码
#define _CRT_SECURE_NO_WARNINGS
#include<string>
#include<iostream>
using namespace std;
//类模板
//类模板可以有默认类型,函数模板不可以有
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;
}
NameType m_Name;
AgeType m_Age;
};
class Person1
{
public:
void showPerson1()
{
cout << "showPerson1调用" << endl;
}
};
class Person2
{
public:
void showPerson2()
{
cout << "showPerson2调用" << endl;
}
};
template<class T>
class myClass
{
public:
T obj;
void func1()
{
obj.showPerson1();
}
void func2()
{
obj.showPerson2();
}
};
//成员函数一开始不会创建出来,而是在运行时才会创建
//如果不调用func1(),那它根本就不会去调用showPerson()
void test01()
{
//类模板,不支持自动类型推导
//Person p("monkey",100);
//显式指定类型
Person<string, int> p("monkey", 100);
p.showPerson();
}
void test02()
{
myClass<Person1> p1;
p1.func1();
p1.func2();
}
int main()
{
//test01();
test02();
system("pause");
return 0;
}

类模板做函数的参数:

1.显示指定类型
2.参数模板化
3.整体模板化

//如何查看类型
cout << "方式三:" << typeid(T).name() << endl;
//类模板
template<class NameType, class AgeType>
class Person{
public:
Person(NameType name, AgeType age){
this->mName = name;
this->mAge = age;
}
void PrintPerson(){
cout << "Name:" << this->mName << " Age:" << this->mAge << endl;
}
public:
NameType mName;
AgeType mAge;
};
//类模板做函数参数
void DoBussiness(Person<string,int>& p){
p.mAge += 20;
p.mName += "_vip";
p.PrintPerson();
}
int main(){
Person<string, int> p("John", 30);
DoBussiness(p);
system("pause");
return EXIT_SUCCESS;
}
点击查看代码
#define _CRT_SECURE_NO_WARNINGS
#include<string>
#include<iostream>
using namespace std;
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;
}
NameType m_Name;
AgeType m_Age;
};
//第一种传参方式:
//1.指定传入类型
void doWork(Person<string, int> &p)//
{
p.showPerson();
}
void test01()
{
Person <string, int>p("tom", 10);
doWork(p);
}
//2.参数模板化
template<class T1,class T2>
void doWork2(Person<T1, T2> &p)
{
//如何查看类型
cout << "方式二:" << typeid(T1).name() << endl;
cout << "方式二:" << typeid(T2).name() << endl;
p.showPerson();
}
void test02()
{
Person <string, int>p("jerry", 10);
doWork2(p);
}
//3.整体类型化
template<class T>
void doWork3(T &p)
{
//如何查看类型
cout << "方式三:" << typeid(T).name() << endl;
p.showPerson();
}
void test03()
{
Person<string, int> p("dog", 12);
doWork3(p);
}
int main()
{
test01();
test02();
test03();
system("pause");
return 0;
}

image

类模板碰到的继承问题

当类模板碰到继承:

  • 基类如果是模板类,必须让子类告诉编译器,基类中的 T 到底是什么类型,
  • 如果不告诉,那么无法分配内存,编译不过
  • 利用参数列表 class Child : public Base
点击查看代码
#define _CRT_SECURE_NO_WARNINGS
#include<string>
#include<iostream>
using namespace std;
template<class T>
class Base
{
public:
T m_A;
};
//在构造Child的时候,也会调用Base里的构造,Base里的构造有可能会对m_A初始化,
//那就要对 T 分配内存,而此时还不确定 T 的类型,因此不知道 T 的内存
//也即,child继承于base,必须告诉base中的T的数据类型,否则无法给T分配内存
//class Child : public Base;//error,应该为如下:
class Child : public Base<int>
{
};
//child也是模板类
template<class T1,class T2>
class Child2 : public Base<T2>
{
public:
Child2()
{
cout << typeid(T1).name() << endl;//int
cout << typeid(T2).name() << endl;//double
}
public:
T1 m_B;
};
void test01()
{
Child2<int, double>child;//T1 int类型 , T2是double类型 //让用户指定类型
}
int main()
{
test01();
system("pause");
return 0;
}

类模板类外实现成员函数

点击查看代码
#define _CRT_SECURE_NO_WARNINGS
#include<string>
#include<iostream>
using namespace std;
template<class T1,class T2>
class Person
{
public:
Person(T1 name, T2 age);
/*{
this->m_Name = name;//类内实现形式
this->m_Age = age;
}*/
void showPerson();
//{
// cout << "姓名:" << this->m_Name << "年龄" << this->m_Age << endl;
//}
T1 m_Name;
T2 m_Age;
};
//类外实现成员函数
template<class T1,class T2>
Person<T1, T2>::Person(T1 name,T2 age)
{
this->m_Name = name;//类内实现形式
this->m_Age = age;
}
template<class T1, class T2>
void Person<T1, T2>::showPerson()
{
cout << "姓名:" << this->m_Name << "年龄" << this->m_Age << endl;
}
void test01()
{
Person<string,int> p1("Mt", 100);
p1.showPerson();
}
int main()
{
test01();
system("pause");
return 0;
}

image

类模板头文件和源文件分离问题

问题:

  • .h 、.cpp 分别声明和实现
  • 但是由于类模板的成员函数在运行阶段才去创建,导致包含 .h 头文件不会创建函数的实现,就无法解析外部命令

解决方案:

  • 方案一:包含.cpp文件(不推荐)
  • 方案二:不要分文件编写,将声明和实现写到同一个文件中,后缀名改为.hpp(约定俗成)

Person.hpp

#pragma once
template<class T1,class T2>
class Person{
public:
Person(T1 name,T2 age);
void ShowPerson();
public:
T1 mName;
T2 mAge;
};
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age){
this->mName = name;
this->mAge = age;
}
template<class T1, class T2>
void Person<T1, T2>::ShowPerson(){
cout << "Name:" << this->mName << " Age:" << this->mAge << endl;
}

main.cpp

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
#include<string>
#include"Person.hpp"
//模板二次编译
//编译器编译源码 逐个编译单元编译的
int main(){
Person<string, int> p("Obama", 20);
p.ShowPerson();
system("pause");
return EXIT_SUCCESS;
}

**结论: ** 案例代码在qt编译器顺利通过编译并执行,但是在Linux和vs编辑器下如果只包含头文件,那么会报错链接错误,需要包含cpp文件,但是如果类模板中有友元类,那么编译失败!
**解决方案: **类模板的声明和实现放到一个文件中,我们把这个文件命名为.hpp(这个是个约定的规则,并不是标准,必须这么写).
原因:
类模板需要二次编译,在出现模板的地方编译一次,在调用模板的地方再次编译。
C++编译规则为独立编译。

Person.hpp

点击查看代码
#pragma once
#include<iostream>
using namespace std;
template<class T1,class T2>
class Person
{
public:
Person(T1 name,T2 age);
void showPerson();
T1 m_Name;
T2 m_Age;
};
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age)
{
this->m_Name = name;
this->m_Age = age;
}
template<class T1, class T2>
void Person<T1, T2>::showPerson()
{
cout << "姓名:" << this->m_Name << " 年龄 " << this->m_Age << endl;
}

main

#include<iostream>
#include<string>
#include"Person.hpp"
using namespace std;
//建议,模板不要做份文件编写,写到一个类中即可,类内进行声明和实现,最后把这个后缀名改为 .hpp
//这是约定俗称的,“.hpp”文件一般都是写模板用的
int main()
{
Person<string, int> p("pig", 18);
p.showPerson();
//如何解决?
//#include<Person.cpp>
system("pause");
return 0;
}

当模板类碰到友元函数

点击查看代码
#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>
class Person{
//1. 友元函数在类内实现
friend void PrintPerson(Person<T1, T2>& p){
cout << "Name:" << p.mName << " Age:" << p.mAge << endl;
}
//2.友元函数类外实现
//告诉编译器这个函数模板是存在
friend void PrintPerson2<>(Person<T1, T2>& p);
//3. 类模板碰到友元函数模板
template<class U1, class U2>
friend void PrintPerson(Person<U1, U2>& p);
public:
Person(T1 name, T2 age){
this->mName = name;
this->mAge = age;
}
void showPerson(){
cout << "Name:" << this->mName << " Age:" << this->mAge << endl;
}
private:
T1 mName;
T2 mAge;
};
void test01()
{
Person <string, int>p("Jerry", 20);
PrintPerson(p);
}
// 类模板碰到友元函数
//友元函数类外实现 加上<>空参数列表,告诉编译去匹配函数模板
template<class T1, class T2>
void PrintPerson2(Person<T1, T2>& p)
{
cout << "Name2:" << p.mName << " Age2:" << p.mAge << endl;
}
void test02()
{
Person <string, int>p("Jerry", 20);
PrintPerson2(p); //不写可以编译通过,写了之后,会找PrintPerson2的普通函数调用,因为写了普通函数PrintPerson2的声明
}
int main(){
//test01();
test02();
system("pause");
return EXIT_SUCCESS;
}
posted @   nullptrException  阅读(18)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示

目录导航