• C++类与对象(下)



    之前我们学习的模板能达到泛型的原因是:使用了“泛型的类型”,但是如果经过后面的“造轮子”(后面会尝试实现一下 STL的一些类模板),就会很明显发现泛型不仅仅是类型的问题,例如:“适配器”的使用(在后面双端队列里有体现),实际上就是一种泛型,对于泛型的理解我们不能仅限于类型。

    1.非类型模板

    模板除了类型模板,还有非类型模板。

    1. 类型模板:出现在模板的参数列表中,跟在class或者typname后的参数类型名称

    2. 非类型模板:使用一个常量作为类的一个非类型模板参数,在模板类/模板函数中可以将该参数作为常量来使用,且不能修改。并且,这里非类型模板参数也可以使用缺省值

    //没有非类型模板参数
    #include 
    using namespace std;
    #define NUM 10
    
    template<class T>
    class Data
    {
    public:
        //...
    private:
        T _arr[NUM];
    };
    int main()
    {
        Data<int> a1;
        //无法修改初始化大小(注意是初始化的时候修改大小)
        //只能手动调整#define的值
        //和之前的typedef的问题类似
        Data<double> a2;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    这个时候就可以使用非类型模板参数,这个参数是一个常量,更加准确来说是不可被修改的整形常量(包括布尔类型)。

    //有非类型模板参数
    #include 
    using namespace std;
    //#define NUM 10
    
    template
    class Data
    {
    public:
        //...
    private:
        T _arr[N];
    };
    int main()
    {
        Data a1;//默认初始化申请50个空间
        Data a2;//初始化时申请20个空间
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    您可能会疑惑:为什么不可以初始化先使用new开辟固定的空间,等到后续操作进行扩容操作呢?注意这里只是利用这个例子来简述语法特性,并不是实际的用途(在后续“位图”等知识中有很大的价值)。

    补充:除了使用这个常量,还可以将这个常量作为一个类的标识数字来使用。

    函数模板也可以使用这一特性。

    #include 
    using namespace std;
    
    template<class T, size_t N = 50>
    class Data
    {
    public:
        //...
    public:
        T _arr[N];
    };
    template<class T, long NUM = 50>//演示了其他整形
    void function(T& i)
    {
        i = NUM;
    }
    
    int main()
    {
        Data<int, 10> a1;
        Data<int, 100> a2;
        int i = 0;
        function<int, 200>(i);//演示了函数修改非类型模板参数
        cout << i << endl;
    }
    
    • 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

    C++ 11搞的新容器:静态数组array,其类模板就是使用了这个非类型模板参数。

    #include 
    #include 
    using namespace std;
    int main()
    {
        array<int, 10>arr;
        for (auto &i : arr)
        {
                i = 10;
        }
        for (auto i : arr)
        {
            cout << i << " ";
        }
        cout << endl;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    可惜静态数组不会进行初始化(吐槽:std::array当参数传递仍然要把数组的长度传过去,挺好玩的…),也支持范围for,并且越界检查比较严格(传统数组是抽查,但是静态数组是读写越界全面检查,避免代码崩溃)。

    嘛…感觉优势不算很大(大不了使用vector,这也可以查找越界,还可以使用列表初始化)所以推广并不高。这个容器有点为了强迫症而统一STL风格的感觉。

    类似dequelistvector的感觉(后面会讲),静态数组就是传统数组和vector之间的方案。

    2.模板特化

    通常模板可以实现和类型无关的代码,但是有一些特殊的类型可能会得到一些错误的、不符合预期的结果,因此需要进行特殊处理,这就有了“模板特化”这个概念。

    2.1.类模板特化

    2.1.1.全特化

    #include 
    using namespace std;
    template<class T1, class T2>
    class Data
    {
    public:
        Data()
        {
            cout << "Data" << endl;
        }
    private:
        T1 _d1;
        T2 _d2;
    };
    
    template<>//全特化,必须要写这句
    class Data<int, char>//这里指定了特定的类型
    {
    public:
        Data()
        {
            cout << "Data" << endl;
        }
    private:
        int _d1;
        char _d2;
    };
    void TestVector()
    {
        Data<int, int> d1;
        Data<int, char> d2;//这样就会直接调用全特化的模板,不会再去类模板构造
    }
    int main()
    {
        TestVector();
    }
    
    • 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

    2.1.2.偏特化

    除了全特化,还可以进行偏特化。在下述代码中,我们可以看到偏特化不仅只是做了一些类型的指定,也可以对类型做进一步限制。

    #include 
    using namespace std;
    template<class T1, class T2>
    class Data
    {
    public:
        Data(const T1& d1, const T2& d2) : _d1(d1), _d2(d2)
        { cout << "Data" << endl; }
    private:
        T1 _d1;
        T2 _d2;
    };
    
    //1.部分类模板参数特化
    template <class T1>
    class Data<T1, int>
    {
    public:
        Data(const T1& d1, const int& d2) : _d1(d1), _d2(d2)
        { cout << "Data" << endl; }
    private:
        T1 _d1;
        int _d2;
    };
    
    //2.1.对两个参数进行进一步限制,偏特化为指针类型
    template <typename T1, typename T2>//这里也是必须写,和全特化有些不同
    class Data <T1*, T2*>
    {
    public:
        Data(const T1& d1, const T2& d2) : _d1(d1), _d2(d2)
        { cout << "Data" << endl; }
    private:
        T1 _d1;//注意其成员不是指针,仍然是原类型
        T2 _d2;//注意其成员不是指针,仍然是原类型
    };
    
    //2.2.对两个参数进行进一步限制,偏特化为引用类型
    template <typename T1, typename T2>//这里也是必须写,和全特化有些不同
    class Data <T1&, T2&>
    {
    public:
        Data(const T1& d1, const T2& d2) : _d1(d1), _d2(d2)
        {cout << "Data" << endl; }
    private:
        T1 _d1;
        T2 _d2;
    };
    
    void test()
    {
        Data<int, double> d1(10, 20);//调用基础的类模板
        Data<int, int> d2(30, 40);//调用偏特化的类模板
        Data<int*, int*> d3(1, 2);//调用偏特化的指针版本
        Data<int&, int&> d4(3, 4);//调用偏特化的引用版本
    }
    int main()
    {
        test();
        return 0;
    }
    
    • 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

    补充:偏特化会使得特化更加强大,某些程度上来说比全特化更加常用。

    因此可以总结类模板的特化语法就是:

    //1.原类模板
    template<class T1, class T2, /*...*/, class Tn>
    class ClassName
    {/*...*/};
    
    //2.特化类模板
    template</*填入仍旧继续使用的泛型(如果都使用可以省略这里)*/>
    class ClassName</*指定特定的类型,并且写入仍旧使用的泛型,注意顺序*/>
    {/*...*/};
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2.2.函数模板特化

    #include 
    using namespace std;
    //类模板
    class Data
    {
    public:
        Data(int d) : _d(d) {}
        bool operator<(const Data& x)
        { return _d < x._d; }
    private:
        int _d;
    };
    
    //函数模板
    template<class T>
    bool Less(T left, T right)
    {
        return left < right;
    }
    
    //特化函数模板
    template<>
    bool Less<Data*>(Data* left, Data* right)
    {
        return (*left) < (*right);
    }
    /*
    template<>
    bool Less(const Data* & left, const Data* & right)//这种写法很特殊,是没有办法通过的,原本是为了使用const修饰引用变量,避免引用变量被修改,但是由于指针和const修饰的特殊性,导致const修饰了*,因此只能改成:(Data* const& left, Data* const& right)这种写法虽然奇怪,但是却是正确的。
    {
        return (*left) < (*right);
    }
    */
    
    int main()
    {
        cout << Less(1, 2) << endl;//调用了普通的函数模板
    
        Data d1(1);
        Data d2(2);
        cout << Less(d1, d2) << endl;//调用了普通的函数模板
    
        Data* p1 = &d1;
        Data* p2 = &d2;
        cout << Less(p1, p2) << endl;//调用特化后的函数模板,虽然这种调用看起来很奇怪
        return 0;
    }
    
    • 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

    注意1:区分好“匹配”和“特化”和“实例化”。

    1. 匹配:是有相匹配的类型,可以使用对应的模板
    2. 实例化:是编译器自己做的,将匹配对应的模板进行实例化
    3. 特化:特化不是全新的模板,必须依赖模板,不可以单独存在

    注意2:实际上特化更加适合类模板一些,实际上函数重载(重载)对比函数模板特化(匹配)更加简单。

    3.函数模板声明定义分离

    这一点凸显在函数的声明定义的分离上,假设有下面三个文件:

    //function.h内声明
    #pragma once
    #include 
    template<class T>
    T Add(const T& left, const T& right);
    
    int NoTemplateAdd(const int& left, const int& right);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    //function.cpp内定义
    template<class T>
    T Add(const T& left, const T& right)
    {
        return left + right;
    }
    
    int NoTemplateAdd(const int& left, const int& right)
    {
        return left + right;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    //main.cpp内包含头文件并且调用
    #include "function.h"
    int main()
    {
        std::cout << Add(1, 2);//链接错误
        std::cout << Add(1.0, 2.0);//链接错误
        std::cout << NoTemplateAdd(1, 2);//成功调用
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    可以发现函数模板没有办法声明和定义分离在两个文件中,会显示链接错误(但是普通的函数可以)。

    让我们来分析一下这里面的原因:

    1. C/C++要运行程序,就需要经历“预处理-编译-汇编-链接”
    2. 在编译阶段,会对多份源文件做各自的编译(进行词法、语法、语义分析、错误检查等)并且生成多份的汇编代码(注意头文件是不会参与编译的)这个时候在function.obj或者说function.o中,由于编译器没有看到函数的实例化,因此没有生成具体的加法函数。
    3. 而在main.obj或者main.o中,编译器看到有加法函数的调用,但是暂时不知道具体的实现,因此就暂时放进了符号表里等待后续链接
    4. 在链接阶段由于没有实例化,因此function.obj或者说function.o中没有加法函数的定义,根本就无法提供加法函数的地址在符号表里供main.obj或者main.o链接

    因此后续链接的时候就会报错,即“链接错误”。

    如果一定要分离,有两种方法:

    1. 进行显示实例化(有缺陷)

      //function.h
      #include 
      using namespace std;
      
      template <typename T>
      void MyFunction(T value);
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      //function.cpp
      #include "function.h"
      
      template <typename T>
      void MyFunction(T value)
      {
          cout << value << endl;
      }
      //显式实例化int类型的函数模板
      template void MyFunction<int>(int value);
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      //main.cpp
      #include "function.h"
      
      int main() 
      {
          //调用int版本的函数模板
          MyFunction(42);
          return 0;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
    2. 在一个翻译单元里分离,即:干脆直接将定义和声明都写在一个.hpp内,这样做是更加推荐的。

  • 相关阅读:
    【Java面试】Mysql为什么使用B+Tree作为索引结构
    css3价格标签卡片悬停特效
    基础Redis-结构与命令
    [附源码]Python计算机毕业设计Django电影院网上售票系统
    一文带你了解2023年最新央企名单、业务和管理机构(附资料)
    【Linux】进程间通信2-匿名管道2
    开发问题总结
    数据结构 C语言 2.1 线性表抽象数据类型 2.2 小议顺序表
    Java小技能:利用反射获取整个项目的枚举字典
    水果店销售技巧有哪些,水果店销售说话技巧有哪些
  • 原文地址:https://blog.csdn.net/m0_73168361/article/details/133690745