C++类与对象(下)

文章目录

  • 1.非类型模板
  • 2.模板特化
    • 2.1.类模板特化
      • 2.1.1.全特化
      • 2.1.2.偏特化
    • 2.2.函数模板特化
  • 3.函数模板声明定义分离

之前我们学习的模板能达到泛型的原因是:使用了“泛型的类型”,但是如果经过后面的“造轮子”(后面会尝试实现一下 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;
}

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

//有非类型模板参数
#include 
using namespace std;
//#define NUM 10

template
class Data
{
public:
    //...
private:
    T _arr[N];
};
int main()
{
    Data a1;//默认初始化申请50个空间
    Data a2;//初始化时申请20个空间
}

您可能会疑惑:为什么不可以初始化先使用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;
}

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;
}

可惜静态数组不会进行初始化(吐槽: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();
}

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.原类模板
template<class T1, class T2, /*...*/, class Tn>
class ClassName
{/*...*/};

//2.特化类模板
template</*填入仍旧继续使用的泛型(如果都使用可以省略这里)*/>
class ClassName</*指定特定的类型,并且写入仍旧使用的泛型,注意顺序*/>
{/*...*/};

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:区分好“匹配”和“特化”和“实例化”。

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

你可能感兴趣的:(c++,算法,数据结构)