泛型、模板

一、泛型、模板

知乎搜索:如何通俗地理解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

注意事项:

  1. 自动类型推导:必须推导出一致的数据T才可以使用
  2. 模板必须要确定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;
}

普通函数与函数模板的区别:

  1. 普通函数调用时可以发生自动类型转换(隐式类型转换)
  2. 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
  3. 如果利用显示指定类型的方式,可以发生隐式类型转换
  4. 建议使用显示指定类型的方式,调用函数模板。因为可以自己确定通用类型T

普通函数与函数模板调用规则:

  1. 如果函数模板和普通函数都可以实现,优先调用普通函数
  2. 可以通过空模板参数列表来强制调用函数模板
  3. 函数模板可以产生更好的匹配,优先调用函数模板
  4. 既然提供了函数模板,最好就不要提供普通函数,否则容易出现二义性
  5. 函数模板也可以发生重载

三、类模板

作用:建立一个通用类,类中的成员数据可以不具体指定,用一个虚拟的类型来代表

语法注释:

  1. template —— 声明创建模板
  2. typename —— 表明后面的符号是一种数据类型,可以用class代替
  3. 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 

类模板与函数模板的区别:

  1. 类模板没有自动类型推导的使用方式
  2. 类模板在模板参数列表中可以有默认参数
#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 

类模板中成员函数创建时机:

  1. 类模板中成员函数和普通类中成员函数的创建时机是有区别的
  2. 普通类中的成员函数一开始就可以创建
  3. 类模板中的成员函数在调用时才创建
#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. 类模板实例化出的对象,向函数传参的方式,一共有三种传入方式:

    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

类模板与继承:

  1. 当子类继承的父类是一个类模板时,子类在声明的时候,要指出父类中T的类型
  2. 如果不指定父类中T的类型,编译器无法给子类分配内存
  3. 如果想灵活指定出父类中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;
}

类模板成员函数类外实现:

  1. 类模板成员函数类外实现规则:

    类模板成员函数类外实现时,需要加上模板参数列表

#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

类模板分文件编写

简介:

  1. 类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到。
  2. 解决方式:
    1. 直接包含.cpp源文件
    2. 将声明和实现写到同一个文件中,并更改后缀名为 .hpp,.hpp是约定的名称,并不是强制。
  3. 主流的解决方式是第二种,将类模板成员函数写到一起,并将后缀名改为 .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;
}

类模板与友元:

  1. 全局函数类内实现 —— 直接在类内声明友元即可
  2. 全员函数类外实现 —— 需要提前让编译器知道全局函数的存在
  3. 建议全局函数做类内实现,用法简单,而且编译器可以直接识别
#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

模板局限性:

  1. 模板的通用性并不是万能的
  2. 利用具体化的模板,可以解决自定义类型的通用化
  3. 学习模板并不是为了写模板,而是在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 
posted @ 2021-10-19 19:55  砚台是黑的  阅读(87)  评论(0编辑  收藏  举报