zoukankan      html  css  js  c++  java
  • 泛型、模板

    一、泛型、模板

    知乎搜索:如何通俗地理解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 
    
  • 相关阅读:
    ZT 二叉树先序,中序,后序遍历非递归实现
    二叉树的遍历(一)
    Z :彻底了解指针数组,数组指针以及函数指针 [复
    ZT 复杂的函数指针例子分析 2008
    D:hunting2014小题目字符串倒序
    本周实验的PSP0过程文档
    构建之法阅读笔记02
    第二周学习进度
    实现自动生成30道四则运算题目(2)
    实现自动生成30道四则运算题目
  • 原文地址:https://www.cnblogs.com/huaibin/p/15426185.html
Copyright © 2011-2022 走看看