简介
之前对模版的进行了初步了解和使用,可查看博客:C++ 初始模板_c++模板初始化_chihiro1122的博客-CSDN博客
其实模版除了是一类算法,或者自定义类型的 套用,还有其他功能,和其他的更高阶的使用方法。
之前在实现 各种 C++ 当中的 STL 的容器的时候用就多次用到了类,比如:套用正向迭代器模版实现的 反向迭代器的适配器;还有 queue 和 stack 容器适配器;还有仿函数的实现,都是使用了 模版来实现的:
C++ - 优先级队列(priority_queue)的介绍和模拟实现 - 反向迭代器的适配器实现_chihiro1122的博客-CSDN博客
C++ - stack 和 queue 模拟实现 -认识 deque 容器 容器适配器_chihiro1122的博客-CSDN博客
在简介当中都是类模版,我们在这里回顾一下函数模版,函数模版不想类模版一样需要 显示实例化,直接传入参数就可以按照模版来自动进行替换:
//template
template
void Print(const Container& v)
{
typename Container::const_iterator it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it; // 因为只重载了 前置的++
}
cout << endl;
}
int main()
{
vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
for (auto ch : v)
{
cout << ch << " ";
}
cout << endl;
Print(v);
return 0;
}
上述 Print()函数不仅仅可以打印 vector 的多种参数类型,其他容器也可以打印。
但是需要注意的是,上述在取出迭代器的时候,使用的方式有些奇怪,如下所示:
typename Container::const_iterator it = v.begin();
如果前面不加 typename 前缀的话,就会报错:
我们在定义模版参数的时候,参数之前的前缀可以是 class 可以是 typename,如下所示:
template
template
但是,不管是哪一种定义方式,如果是 const 迭代器的话,在迭代器类型之前,必须用 typename前缀修饰。
原因很简单:
上述如果不加 typename 的代码:
Container::const_iterator it = v.begin();
程序运行,首先进行编译实例化。
如果要寻找 const_iterator ,编译器就会去 Container 当中去寻找,但是这里寻找就会有问题,如果我们不使用函数模版,那么就不会出现问题,因为这里的 Container 直接写成 vector
但是,如果使用模版,这里就有三种情况,一种是在 Container 当中寻找 const_iterator 这个动态成员变量;另一种是寻找 const_iterator 这个内部类(对象);那么到底这个 const_iterator 是一个成员变量,还是一个对象,还是一个类型,编译器搞不明白。而此时的 Container 不知道是什么。
如果 Container::const_iterator 这个表示的是一个类型,那么此处的语法是正确的;如果表示的是一个 对象,就不符合语法了。
所以,在此处,加一个 typename 表示此处的 Container::const_iterator 就是一个 类型。等实例化再去实例化的对象当中去寻找。
其实这里还有更好的方式来解决,用一个 auto就可以自动推导类型 ,就不用再使用之前一大长串 的类型名了。这里我们就可以体会到了 auto 的强大。
因为 auto 一定是类型,所以编译器就不会再去往 对象那一方面去想了。
但是不是所有使用 typename 的地方都可以用 auto,有些地方还是需要用到 typename 的,比如在 优先级对象当中就是用了 typename。如下图所示:
问题:有些编译器会 按需实例化,比如:当类当中的 某一个成员函数当中有编译错误,如果这个函数没有调用,那么编译器会略过这个错误;但是 按需实例化也是看编译器的,不同的编译器实例化程度不同。
模版当中不仅仅有需要类型模版参数的情况,可能还需要传入一些数值,比如下面这个例子,定义一个静态栈:
#define N 10
template
class stack
{
public:
private:
T _a[N];
size_t _size;
};
int main()
{
stack st1; // 10 个
stack st2; // 100 个
}
此时我想定义两个静态栈,但是因为是静态的,宏 N 的大小不能改变 ,那么上述代码我们只能满足 一种情况,这就和 C 当中的 宏 一样的。
所以,C++ 当中的模版参数,还可以传入值:
template
class stack
{
public:
private:
T _a[N];
size_t _size;
};
int main()
{
stack st1; // 10 个
stack st2; // 100 个
}
template
上述模版直接用 类型来当做是模版参数的类型,这个N 就是一个非类型的模版参数,解决了上述静态栈的问题。
关于非类型模版参数,需要注意的点:
模版可以实现无关类型的代码,但是一些特殊类型的代码可能会出现问题:
template
bool Less(T left, T right)
{
return left < right;
}
int main()
{
cout << Less(1, 2) << endl; // 可以比较,结果正确
int d1 = 1;
int d2 = 2;
cout << Less(d1, d2) << endl; // 可以比较,结果正确
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; // 可以比较,结果错误
return 0;
}
最后一组,传入的是指针类型,那么 模版类型 T 就是指针,指针的比较大小是比较地址的高低,但是肯定有上述的情况下,我们想要传入指针但是不想按照指针去比较,按照传入指针 解引用的值来进行比较,但是我们不可能直接修改模版函数,如果改成解引用的话,之前我们想要实现的功能就不能实现了。
所以,上述的例子就要用到模版的特化。
上述的less ()函数,特化之后如下所示:
// Less 函数的模版
template
bool Less(T left, T right)
{
return left < right;
}
// Less 模版函数的特化
template<>
bool Less(int* left, int* right)
{
return *left < *right;
}
如上下面一个 Less
注意:
上述的特化还可以写成下述函数重载的样式(和函数模版实例化出的函数进行 函数重载):
bool Less(int* left, int* right)
{
return *left < *right;
}
上述的 模版特化 和 函数重载,两种方式虽然都可以达到我们想要目的,但是,上述两种情况都只是实现了 int 类型的指针问题,不能解决多种指针的问题。
所以,聪明的你一下发现了,那么我们在实现一个模版不就行了?是这样的,看如下代码:
template
bool Less(T* left, T* right)
{
return *left < *right;
}
当,传入的T 是一个指针的时候,虽然 第一种形式的模版 和 上述这个模版都可以匹配,但是,上面这个模版更加的符合,所以,如果传入的是 某类型的指针的话,就会调用上面这个模版。
而且,如果你实现上述两个模版的同时,在想上述一样实现了某一个类型的重载函数,或者像第一次那样的 实现 模版函数的 特化,那么这两个都是现成的,编译器优先调用现成的函数。
类模板的特化和函数模版的特化是类似的 ,函数模版的特化是相当于是重新写了一个函数,类模板的特化也相当于是多写了一个类:
// 类模板
template
class Data
{
public:
Data() { cout << "Data" << endl; }
private:
T1 _d1;
T2 _d2;
};
// 类模板的全特化
template<>
class Data
{
public:
Data() { cout << "Data" << endl; }
private:
int _d1;
char _d2;
};
void TestVector()
{
Data d1;
Data d2;
}
输出:
Data
Data
当传入的模版参数是 int 和 char 的时候,调用的就是下面定义的 特化的 模版类,然后进行特殊处理,在这个当中的特殊处理,不影响 之前定义的类模板。
运用场景,在使用仿函数的时候就可以使用,当我们想对仿函数的 类模板 当中某一个类型的实例化进行特殊操作的时候,就可以使用上述类模板的特化,来对某一种类型进行特化。
像之前在 介绍 优先级队列,当中对 less 这个仿函数的介绍,当传入的是日期类指针(Date*)的结果就不对,这时候,就可以使用 特化,给 less 类模板 特化出一个 特殊处理的类。
向上述的 :
template<>
class Data
和
template<>
bool Less(int* left, int* right)
都属于是全特化。全特化就是把所以的模版参数都特化。
而偏特化,也叫做半特化,就是没有把全部的模版参数都特化。
如下代码所示:
// 类模板的全特化
template<>
class Data
{
public:
Data() { cout << "Data" << endl; }
private:
int _d1;
char _d2;
};
// 类模板的偏特化(特化部分参数)
template
class Data
{
public:
Data() { cout << "Data" << endl; }
private:
T1_d1;
char _d2;
};
向上述的偏特化,是对某一些模版参数进行特化;其实偏特化有两种方式:
// 类模板的偏特化(对一些参数进行一些限制)
template
class Data
{
public:
Data() { cout << "Data" << endl; }
private:
};
向上述不对某些参数进行特化,只是判断传入的两个模版参数是不是指针,是就调用这个 特化的类。
特化在库当中也是有运用的,库当中对为了在某一模版当中找到其中调用的模版的模版参数,即用了萃取,而萃取本质上其实就是特化实现的,只不过库当中的萃取实现很麻烦。
array数组和 c语言 当中的 数组,在功能上和 效率上没有任何区别,就连不能用变量来初始化个数这个特性都是一样的。而且,如果使用 array 的无参数的构造函数,里面的元素也不会进行初始化,这里和 C 当中的 数组也是一样的;
array 相比于 C当中的数组,唯一的好处就是,array 可以检查越界,而且检查非常的快,他是用assert ()断言来实现的。如果是 C语言的 数组,只是读的访问,越界是检查不出来的,写可能会检查出来;而 array 容器无论是 写还是读,都会检查出来。利用的就是 operator[] 当中的 对 下标越界的检查。
除此之外,array 容器 ,相比于数组是没有任何优点的。