• C++中的泛型详细讲解


    1.定义

    它是一种泛化的编程方式,其实现原理为程序员编写一个函数/类的代码示例,让编译器去填补出不同的函数实现。允许您延迟编写类或方法中的编程元素的数据类型的规范,直到实际在程序中使用它的时候。换句话说,泛型允许您编写一个可以与任何数据类型一起工作的类或方法。

    2.模板

    模板是泛型编程的基础,是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。每个容器都有一个单一的定义,比如 向量,我们可以定义许多不同类型的向量,比如 vector vector 。可以使用模板来定义函数和类,我们来具体分析一下模板函数和模板类的创建和使用:

    2.1 模板函数

    我们想实现像Python一样,一个带有参数的方法,它的相同参数可以传递不同类型的值。我们通过下面的例子来了解一下:

    #include
    using namespace std;
    
    template<typename T>
    void Test(T& arg1,T& arg2) // 这是一个实现两个变量值交换的函数
    {
        T temp = arg1;
        arg1 = arg2;
        arg2 = temp;
    }
    // typename 是定义模板的关键字,可以用class来替代(注意不能用struct)
    int main()
    {
        int a = 10, b = 20;
        double c = 5.2, d = 10.5;
        Test(a, b);
        Test(c, d);
        cout << a << " " << b << endl;
        cout << c << " " << d << endl;
    }
    // 输出结果:
    20 10
    10.5 5.2
    
    // 我们交换了int类型的a与b的值,double类型c和d的值
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    如果我们将int和double同时传给Swap这个函数,那么编译器会报错,表示模板参数T不明确,那么我们需要做如下改动:

    (1)把函数传参中的引用去掉

    (2)把a强制转换成double类型,Swap((double)a, c)

    #include
    using namespace std;
    
    template<typename T>
    void Test(T arg1,T arg2) // 把“&”引用去掉
    {
        T temp = arg1;
        arg1 = arg2;
        arg2 = temp;
        cout << arg1 << " " << arg2 << endl;
    }
    
    int main()
    {
        int a = 10;
        double c = 5.2;
    
        Test((double)a, c);
        cout << "a:" << a << " c:" << c << endl;
    }
    
    // 输出结果:
    5.2 10
    a:10 c:5.2
    // a,c值没有变,是因为我们传参是值传递
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    接下来我们看一下,多个模板参数的情况:

    #include
    #include
    using namespace std;
    
    template<typename T1, typename T2>
    void Info(T1 arg1,T2 arg2)
    {
        cout << typeid(arg1).name() << endl;
        cout << typeid(arg2).name() << endl;
    }
    
    int main()
    {
        int a = 10;
        double c = 5.2;
    
        Info(a, c);
        cout << "a:" << a << " c:" << c << endl;
    }
    
    // 输出结果:
    i
    d
    a:10 c:5.2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    可以看到,实际上函数在调用这个模板的时候,已经实例化了这个函数(即替换模板参数为正确参数类型)这时候在后台处理的时候,其实Show函数已经实例化为了下面这个样子

    void Info(int arg1,double arg2)
    {
        cout << typeid(arg1).name() << endl;
        cout << typeid(arg2).name() << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.2 函数模板实例化

    上面的方式,是编译器自动帮我们实例化模板参数。在实际使用中,我们还可以自己指定实例化为什么类型

    • 利用强制类型转换
    • 使用直接指定实例化为int类型
    #include
    using namespace std;
    
    template<typename T>
    void Test(T arg1,T arg2) // 把“&”引用去掉
    {
        T temp = arg1;
        arg1 = arg2;
        arg2 = temp;
        cout << arg1 << " " << arg2 << endl;
    }
    
    int main()
    {
        int a = 10;
        double c = 5.2;
    
        Test((double)a, c);    // 强制类型转换
        Test<int>(a, c);        // 直接指定
        cout << "a:" << a << " c:" << c << endl;
    }
    /*
    使用第二种方式的时候,编译器会对另外一个不匹配的参数进行隐式类型转换。如果转换不成功,则会报错。
    
    另外注意的是,函数模板参数T同样可以用来作为返回值,但是不能通过返回值来推断参数T的类型。比如下面这个函数,我们在使用的时候就需要直接指定模板参数T,而不能写一个int* ptr=test(10)让编译器通过“返回值是int*接收的,所以函数模板参数T是int”来推断。
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    2.3 函数模板实例化

    函数模板支持给予参数缺省值,当一个参数不确定的时候,函数模板是支持给予缺省值的

    template<typename T=char>
    T* Test(int num)
    {
    	return new T[num];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    当有多个模板参数时,缺省值需要从右往左给,当然函数模板的传参也支持缺省值:

    #include
    using namespace std;
    
    template
    void Test(T arg1,T arg2=20) // 把“&”引用去掉
    {
        T temp = arg1;
        arg1 = arg2;
        arg2 = temp;
        cout << arg1 << " " << arg2 << endl;
    }
    
    int main()
    {
        int a = 10;
    
        Test(a);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    2.4 模板函数与普通函数同时存在情况

    函数在调用的时候,首先会去调用已经存在的函数。当参数和已存在的函数不匹配时,才会调用函数模板

    #include
    using namespace std;
    
    template<typename T>
    void Test(T arg1,T arg2 = 90) 
    {
        cout << "Test temp " << arg1 << " " << arg2 << endl;
    }
    
    void Test(int arg1,int arg2) 
    {
       	cout << "Test " << arg1 << " " << arg2 << endl;
    }
    
    int main()
    {
        int a = 10, b = 20;
        double c = 5.2, d = 10.5;
    
        Test(a);
        Test(a, b);
        Test(a, (int)c);    // 强转
        Test((double)a, c); // 强转
        Test<int>(a, c);    // 直接指定为int
    }
    // 输出结果:
    Test temp 10 90
    Test 10 20
    Test 10 5
    Test temp 10 5.2
    Test temp 10 5
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    2.5 函数模板不支持定义和声明分离

    函数模板的声明和定义要放在一个头文件中。在部分使用场景,会使用.hpp来表示这个头文件是包含了函数定义的(即.h和.cpp的集合体)。需要注意,这并不是一个硬性要求,你也可以直接使用.h,并将声明和定义放入其中。因为单独的.h声明会在源文件顶部展开,而此时函数模板正常推演参数,但编译器并没有找到函数的实现,即这是一个没有地址的函数。从而导致编译器找不到函数的地址,产生了符号表的链接错误。其实是有的,我们可以在模板函数定义的.cpp中对我们需要使用的函数进行显式实例化指定

    //头文件
    //声明
    template<typename T>
    void Test(T arg1, T arg2);
    //源文件
    //定义
    template<typename T>
    void Test(T arg1, T arg2)
    {
       cout << arg1 << " " << arg2 << endl;
    }
    //在源文件中显式实例化
    template
    void Test<int>(int arg1, int arg2);
    template
    void Test<double>(double arg1, double arg2);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    显式实例化需要对我们要用的所有函数进行实例化,比如你需要用double类型,只显示实例化了int类型是不行的,依旧会报错。这样感觉非常多余……!所以还是把声明和定义放在同一个文件里面清晰明了一些。

    3 类模板

    正如我们定义函数模板一样,我们也可以定义类模板。泛型类声明的一般形式如下所示:

    template <class type> class class-name {
    .
    .
    .
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里,type 是占位符类型名称,可以在类被实例化的时候进行指定。您可以使用一个逗号分隔的列表来定义多个泛型数据类型。

    下面的实例定义了类 Stack<>,并实现了泛型方法来对元素进行入栈出栈操作:

    #include 
    #include 
    #include 
    #include 
    #include 
     
    using namespace std;
     
    template <class T>
    class Stack { 
      private: 
        vector<T> elems;     // 元素 
     
      public: 
        void push(T const&);  // 入栈
        void pop();               // 出栈
        T top() const;            // 返回栈顶元素
        bool empty() const{       // 如果为空则返回真。
            return elems.empty(); 
        } 
    }; 
     
    template <class T>
    void Stack<T>::push (T const& elem) 
    { 
        // 追加传入元素的副本
        elems.push_back(elem);    
    } 
     
    template <class T>
    void Stack<T>::pop () 
    { 
        if (elems.empty()) { 
            throw out_of_range("Stack<>::pop(): empty stack"); 
        }
        // 删除最后一个元素
        elems.pop_back();         
    } 
     
    template <class T>
    T Stack<T>::top () const 
    { 
        if (elems.empty()) { 
            throw out_of_range("Stack<>::top(): empty stack"); 
        }
        // 返回最后一个元素的副本 
        return elems.back();      
    } 
     
    int main() 
    { 
        try { 
            Stack<int>         intStack;  // int 类型的栈 
            Stack<string> stringStack;    // string 类型的栈 
     
            // 操作 int 类型的栈 
            intStack.push(7); 
            cout << intStack.top() <<endl; 
     
            // 操作 string 类型的栈 
            stringStack.push("hello"); 
            cout << stringStack.top() << std::endl; 
            stringStack.pop(); 
            stringStack.pop(); 
        } 
        catch (exception const& ex) { 
            cerr << "Exception: " << ex.what() <<endl; 
            return -1;
        } 
    }
    
    // 输出结果:
    7
    hello
    Exception: Stack<>::pop(): empty stack
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
  • 相关阅读:
    nginx+tomcat(二)
    什么是DFT?FT、FS、DTFT、DFS、DFT的关系
    MiniGPT-4:看图聊天、教学、创作、搭网站......还开源了
    Microsoft Edge浏览器崩溃,错误代码: STATUS_STACK_BUFFER_OVERRUN
    猿创征文|Mybatis的注解实现复杂映射开发
    手写一个PrattParser基本运算解析器1: 编译原理概述
    Signoff Criteria --- ocv applied and results
    Pandas读取Excel文件XLRDError: Excel xlsx file; not supported
    Zone 和 Zoneset 是什么关系
    自制操作系统日志——第二十五天
  • 原文地址:https://blog.csdn.net/weixin_45805339/article/details/128107803