泛型、模板
一、泛型、模板
知乎搜索:如何通俗地理解C++的模板?
个人认为比较容易接受的回答:
模板就是建立通用的模具,大大提高复用性。
模板的特点:
- 模板不可以直接使用,它只是一个框架
- 模板的通用并不是万能的
- 根本目的是为了代码复用
- C++提供两种模板机制:函数模板和类模板
另外有趣的解释:
- 公式
- 类是实例对象的妈,负责运行期生成对象;模板是类、函数的妈,负责编译期生成类、函数。
二、函数模板
基本语法
作用:建立一个通用函数,其函数返回值类型和形参类型可以不具体指定,用一个虚拟的类型来代表。
函数模板关键字:
template
函数模板声明/定义:
template<typename T>
1、template —— 声明创建模板
2、typename —— 表明其后面的符号是一种数据类型,可以使用class代替
3、T —— 通用的数据类型,名称可以替换、通常为大写字母
使用函数模板有两种方式:自动类型推导、显示指定类型
模板的目的是为了提高复用性、将类型也参数化。
#include <iostream>
using namespace std;
// 函数模板
// 交换两个整数
void SwapIntNum(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
// 交换两个浮点数
void SwapDoubleNum(double& a, double& b) {
double temp = a;
a = b;
b = temp;
}
// 基于以上两个函数,可以看出,除了参数的数据类型不同,代码逻辑都是一样的。基于这种场景,出现了下面的函数模板:
// 注意这里先定义形参的类型
// 声明一个模板,告诉编译器后面代码中紧跟的T不要报错,T是一个通用数据类型,泛指,不具体表示某一具体类型
// 注意 <> 括号里面的typename 可以替换成class
template<typename T>
void SwapNum(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
void test_case01() {
int a = 20;
int b = 30;
SwapIntNum(a, b);
cout << "a = " << a << endl;
cout << "b = " << b << endl;
double c = 1.11;
double d = 2.34;
SwapDoubleNum(c, d);
cout << "c = " << c << endl;
cout << "d = " << d << endl;
}
void test_case02() {
int a = 20;
int b = 30;
// 1、自动类型推导:根据a、b的数据类型int自动设置T为int
SwapNum(a, b);
cout << "a = " << a << endl;
cout << "b = " << b << endl;
double c = 1.11;
double d = 2.34;
// 2、显示指定类型:指定模板中数据类型T为double型。更推荐这种写法
SwapNum<double>(c, d);
cout << "c = " << c << endl;
cout << "d = " << d << endl;
}
int main() {
test_case01();
cout << "使用模板函数 ===>>>" << endl;
test_case02();
}
输出结果:
bzl@bzl ~ o ./a.out
a = 30
b = 20
c = 2.34
d = 1.11
使用模板函数 ===>>>
a = 30
b = 20
c = 2.34
d = 1.11
bzl@bzl ~ o
注意事项:
- 自动类型推导:必须推导出一致的数据T才可以使用
- 模板必须要确定T的数据类型,才可以使用
#include <iostream>
using namespace std;
// 模板必须要确定出T的数据类型,才可以使用
template<class T>
void func() {
cout << "func 调用" << endl;
}
void test_case01() {
func(); // 错误:模板必须要确定T的数据类型才可以使用
// func<int>(); // 正确:指定了模板的数据类型为int
}
int main() {
test_case01();
return 0;
}
普通函数与函数模板的区别:
- 普通函数调用时可以发生自动类型转换(隐式类型转换)
- 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
- 如果利用显示指定类型的方式,可以发生隐式类型转换
- 建议使用显示指定类型的方式,调用函数模板。因为可以自己确定通用类型T
普通函数与函数模板调用规则:
- 如果函数模板和普通函数都可以实现,优先调用普通函数
- 可以通过空模板参数列表来强制调用函数模板
- 函数模板可以产生更好的匹配,优先调用函数模板
- 既然提供了函数模板,最好就不要提供普通函数,否则容易出现二义性
- 函数模板也可以发生重载
三、类模板
作用:建立一个通用类,类中的成员数据可以不具体指定,用一个虚拟的类型来代表
语法注释:
- template —— 声明创建模板
- typename —— 表明后面的符号是一种数据类型,可以用class代替
- T —— 通用的数据类型,名称可以替换,通常为大写字母
#include <iostream>
using namespace std;
template<class NameType, class AgeType>
class Person {
public:
Person(NameType name, AgeType age) {
// 构造函数赋初值
this->name = name;
this->age = age;
}
void ShowPersonInfo() {
cout << "name: " << this->name << " age: " << this->age << endl;
}
private:
NameType name;
AgeType age;
};
void test_case01() {
// <> 里面是模板的参数列表
Person<string, int> person_obj("孙武", 23);
person_obj.ShowPersonInfo();
}
int main(){
test_case01();
return 0;
}
输出结果:
bzl@bzl ~ o ./a.out
name: 孙武 age: 23
bzl@bzl ~ o
类模板与函数模板的区别:
- 类模板没有自动类型推导的使用方式
- 类模板在模板参数列表中可以有默认参数
#include <iostream>
using namespace std;
// 类模板在模板参数列表中可以有默认参数
template<class NameType, class AgeType = int>
class Person {
public:
Person(NameType name, AgeType age) {
// 构造函数赋初值
this->name = name;
this->age = age;
}
void ShowPersonInfo() {
cout << "name: " << this->name << " age: " << this->age << endl;
}
private:
NameType name;
AgeType age;
};
void test_case01() {
// <> 里面是模板的参数列表
Person<string, double> person_obj01("孙武1", 23.5);
person_obj01.ShowPersonInfo();
// age 使用默认参数int
Person<string> person_obj02("孙武2", 23.5);
person_obj02.ShowPersonInfo();
}
int main(){
test_case01();
return 0;
}
输出结果:
bzl@bzl ~ o ./a.out
name: 孙武1 age: 23.5
name: 孙武2 age: 23
bzl@bzl ~ o
类模板中成员函数创建时机:
- 类模板中成员函数和普通类中成员函数的创建时机是有区别的
- 普通类中的成员函数一开始就可以创建
- 类模板中的成员函数在调用时才创建
#include <iostream>
using namespace std;
class Person1 {
public:
void ShowPersonInfo() {
cout << "Person1 show" << endl;
}
};
class Person2 {
public:
void ShowPersonInfo() {
cout << "Person2 show" << endl;
}
};
template<class T>
class PersonTemplate {
public:
T obj;
// 类模板中的成员函数
void func() {
// 类模板中的成员函数在调用时才创建
obj.ShowPersonInfo();
}
};
void test_case01() {
// 根据类模板实际传参的不同,在func函数中调用不同class的成员函数 ShowPersonInfo
PersonTemplate<Person1> person_obj;
person_obj.func();
}
int main() {
test_case01();
return 0;
}
类模板对象做函数参数:
类模板实例化出的对象,向函数传参的方式,一共有三种传入方式:
1、指定传入的类型 —— 直接显示对象的数据类型
2、参数模板化 —— 将对象中的参数变为模板进行传递
3、整个类模板化 —— 将这个对象类型 模板化 进行传递
// 使用比较广泛的是第一种:指定传入类型
#include <iostream>
#include <string>
#include <typeinfo>
using namespace std;
// 类模板对象用做函数参数
template<class T1, class T2>
class Person {
private:
T1 name;
T2 age;
public:
Person(T1 name, T2 age) {
this->name = name;
this->age = age;
}
// 注意:除了构造函数析构函数外,其余的函数必须指定返回值类型
void ShowPersonInfo() {
cout << "姓名: " << this->name << " 年龄: " << this->age << endl;
}
};
// 1、指定传入类型
void PrintPerson1(Person<string, int>& p) {
p.ShowPersonInfo();
}
void test_case01() {
Person<string, int> p("孙武打的", 100);
PrintPerson1(p);
}
// 2、参数模板化
template<class T1, class T2>
void PrintPerson2(Person<T1, T2>& p){
p.ShowPersonInfo();
cout << "T1的类型为: " << typeid(T1).name() << endl;
cout << "T2的类型为: " << typeid(T2).name() << endl;
}
void test_case02() {
Person<string, int> p("八戒", 56);
PrintPerson2(p);
}
// 3、整个类模板化
template<class T>
void PrintPerson3(T& p) {
p.ShowPersonInfo();
cout << "T的类型为: " << typeid(T).name() << endl;
}
void test_case03() {
Person<string, int> p("唐山", 30);
PrintPerson3(p);
}
int main() {
test_case01();
test_case02();
test_case03();
return 0;
}
输出结果:
bzl@bzl ~ o ./a.out
姓名: 孙武打的 年龄: 100
姓名: 八戒 年龄: 56
T1的类型为: NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
T2的类型为: i
姓名: 唐山 年龄: 30
T的类型为: 6PersonINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEiE
bzl@bzl ~ o
类模板与继承:
- 当子类继承的父类是一个类模板时,子类在声明的时候,要指出父类中T的类型
- 如果不指定父类中T的类型,编译器无法给子类分配内存
- 如果想灵活指定出父类中T的类型,子类也需要变为类模板
#include <iostream>
using namespace std;
// 类模板与继承
template<class T>
class Base {
T m;
};
// 注意这里:Base在被继承的时候也需要指定模板类型
// class Son:public Base 报错:必须要知道父类中T类型,才能继承给子类,
// 因为编译器不知道给子类多少个内存空间,如果T是int类型给1个字节,
// 如果T是double型给4个字节
class Son:public Base<int> {
};
void test_case01() {
Son s1;
}
// 如果想灵活指定父类中T类型,子类也需要变类模板
template<class T1, class T2>
class Son2:public Base<T2> {
public:
Son2() {
cout << "T1的类型为: " << typeid(T1).name() << endl;
cout << "T2的类型为: " << typeid(T2).name() << endl;
}
T1 obj;
};
void test_case02() {
// T1为int,即obj为int型,T2为char型,即m为char型
Son2<int, char>S2;
}
int main() {
test_case01();
test_case02();
return 0;
}
类模板成员函数类外实现:
类模板成员函数类外实现规则:
类模板成员函数类外实现时,需要加上模板参数列表
#include <iostream>
#include <string>
using namespace std;
// 类模板成员函数类内实现
template<class T1, class T2>
class Person {
private:
T1 name;
T2 age;
public:
// 构造函数声明
Person(T1 name, T2 age);
// 普通函数声明
void ShowPerson();
};
// 构造函数类外实现
// Person<T1, T2>说明这是一个Person类模板的类外成员函数实现,
// Person::Person(T1 name, T2 age) 表示类的函数的类外实现,
// Person(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() { // 有<T1, T2>表示是类模板的成员函数类外实现, Person表示是Person作用域的ShowPerson函数。
cout << "姓名: " << this->name << " 年龄: " << this->age << endl;
}
void test_case01() {
Person<string, int> p1("张三", 18);
p1.ShowPerson();
}
int main() {
test_case01();
return 0;
}
输出结果:
zhi@ubuntu ~/ros2-gtest-gmock o ./a.out
姓名: 张三 年龄: 18
zhi@ubuntu ~/ros2-gtest-gmock o
类模板分文件编写
简介:
- 类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到。
- 解决方式:
- 直接包含.cpp源文件
- 将声明和实现写到同一个文件中,并更改后缀名为 .hpp,.hpp是约定的名称,并不是强制。
- 主流的解决方式是第二种,将类模板成员函数写到一起,并将后缀名改为 .hpp。
// 类模板没有分文件编写
#include <iostream>
#include <string>
using namespace std;
template<class T1, class T2>
class Person {
private:
T1 name;
T2 age;
public:
Person(T1 name, T2 age);
void ShowPerson();
};
// 类模板-构造函数
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 << "姓名: " << this->name << " 年龄:" << this->age << endl;
}
void test_case01() {
Person<string, int> p("Jerry", 18);
p.ShowPerson();
}
int main() {
test_case01();
return 0;
}
输出结果:
zhi@ubuntu ~/ros2-gtest-gmock o ./a.out
姓名: Jerry 年龄:18
zhi@ubuntu ~/ros2-gtest-gmock o
// 类模板分文件写:方式一
// person.h
#pragma once // 防止头文件重复包含
#include <iostream>
#include <string>
using namespace std;
template<class T1, class T2>
class Person {
private:
T1 name;
T2 age;
public:
Person(T1 name, T2 age);
void ShowPerson();
};
// person.cpp
#include "person.h"
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 << "姓名: " << this->name << " 年龄: " << this->age << endl;
}
// main.cpp
#include <iostream>
// 不能使用 #include "person.h", 如果仅包含 #include "person.h",
// 由于类模板中的成员函数并没有创建,编译器并不会去找 Person(T1 name, T2 age) 和
// void ShowPerson() 这两个函数的定义。
// 如果包含 #include "person.cpp" 就会看到 Person(T1 name, T2 age)
#include "person.cpp"
using namespace std;
void test_case01() {
Person<string, int> p("Jerry", 18);
p.ShowPerson();
}
int main() {
test_case01();
return 0;
}
输出结果:
bzl@bzl ~/cppcode/template-test o ./test-person
姓名: Jerry 年龄: 18
bzl@bzl ~/cppcode/template-test o
第二种方式:将 .h 和 .cpp 中的内容写到一起,将后缀名改为 .hpp 文件
// person.hpp 文件
#pragma once
#include <iostream>
#include <string>
using namespace std;
template<class T1, class T2>
class Person {
private:
T1 name;
T2 age;
public:
Person(T1 name, T2 age);
void ShowPerson();
};
// 构造函数 类外实现
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 << "姓名" << this->name << " 年龄: " << this->age << endl;
}
// main.cpp
#include <iostream>
#include "person.hpp"
void test_case01() {
Person<string, int> p("Jerry", 18);
p.ShowPerson();
}
int main() {
test_case01();
return 0;
}
类模板与友元:
- 全局函数类内实现 —— 直接在类内声明友元即可
- 全员函数类外实现 —— 需要提前让编译器知道全局函数的存在
- 建议全局函数做类内实现,用法简单,而且编译器可以直接识别
#include <iostream>
#include <string>
using namespace std;
// 通过全局函数打印 Person信息
// 提前声明,提前让编译器知道Person模板类存在
template<class T1, class T2>
class Person;
// 全局函数,类外实现
// 让编译器知道Person类存在后,还需要提前让编译器知道PrintPerson2全局函数存在
template<class T1, class T2>
void PrintPerson2(Person<T1, T2> p) // 全局函数,所以不需要加作用域
{
cout << "类外实现 —— 姓名:" << p.name << " 年龄:" << p.age << endl;
}
template<class T1, class T2>
class Person {
// 全局函数 类内实现
friend void PrintPerson(Person<T1, T2> p) {
cout << "类内实现 —— 姓名:" << p.name << "年龄:" << p.age << endl;
}
// 全局函数 类外实现
// 加一个空模板参数列表,表示是函数模板声明,而不是普通函数的声明
friend void PrintPerson2<>(Person<T1, T2> p);
// 只有前面提前让编译器知道PrintPerson2的存在,由于PrintPerson2里面有Person类,所以
// 还需要提前让编译器知道Person类的存在,才能声明全局函数类外的实现
public:
Person(T1 name, T2 age) {
this->name = name;
this->age = age;
}
private:
T1 name;
T2 age;
};
// 1、全局函数在类内实现
void test_case01() {
Person<string, int> p("Tom", 30);
PrintPerson(p);
}
// 2、全局函数在类外实现
void test_case02() {
Person<string, int> p("Jerry", 21);
PrintPerson2(p);
}
int main() {
test_case01();
test_case02();
return 0;
}
输出结果:
zhi@ubuntu ~/ros2-gtest-gmock o ./a.out
类内实现 —— 姓名:Tom年龄:30
类外实现 —— 姓名:Jerry 年龄:21
模板局限性:
- 模板的通用性并不是万能的
- 利用具体化的模板,可以解决自定义类型的通用化
- 学习模板并不是为了写模板,而是在STL中能够运用系统提供的模板
局限性一:
// 下面代码提供赋值操作,如果传入的a和b是一个数组就无法实现了
// 数组无法给另一个数组直接赋值
template<class T>
void MyPrint(T a, T b) {
a = b;
}
局限性二:
// 在下面代码中,如果T的数据类型传入的是像Person这样自定义数据类型,也无法正常运行。
template<class T>
void MyPrint(T a, T b) {
if (a > b) { ... };
}
模板具体实现:
#include <iostream>
#include <string>
using namespace std;
// 模板局限性
// 模板并不是万能的,有些特定数据类型,需要用具体化方式做特殊实现
class Person {
public:
string name;
int age;
Person(string name, int age) {
this->name = name;
this->age = age;
}
private:
};
// 对比两个数据是否相等的函数
template<class T>
bool MyCompare(T& a, T& b) {
if(a == b) // a == b 能判断整型、浮点型数据是否相等,但是没有办法判断Person类型与Person类型相等比较,但是可以通过 == 运算符重载,来判断Person类型的p1和Person类型的p2是否相等
{
return true;
}
else {
return false;
}
}
// 利用具体化的Person类版本实现代码,具体化优先调用
template<> bool MyCompare(Person& p1, Person& p2) // template<> 表示这是一个模板重载的版本,Person表示这是重载的person的模板
{
if(p1.name == p2.name && p1.age == p2.age) {
return true;
}
else {
return false;
}
}
void test_case01() {
int a = 10;
int b = 20;
bool ret = MyCompare(a, b);
if(ret) {
cout << "a == b" << endl;
}
else {
cout << "a != b" << endl;
}
}
void test_case02() {
Person p1("Tom", 10);
Person p2("Tom", 10);
bool ret = MyCompare(p1, p2);
if(ret) {
cout << " p1 == p2 " << endl;
}
else {
cout << " p1 != p2 " << endl;
}
}
int main() {
test_case01();
test_case02();
return 0;
}
输出结果:
zhi@ubuntu ~/ros2-gtest-gmock o ./a.out
a != b
p1 == p2