C++ - 模版进阶 - array

 简介

 之前对模版的进行了初步了解和使用,可查看博客:C++ 初始模板_c++模板初始化_chihiro1122的博客-CSDN博客

 其实模版除了是一类算法,或者自定义类型的 套用,还有其他功能,和其他的更高阶的使用方法。

之前在实现 各种 C++ 当中的 STL 的容器的时候用就多次用到了类,比如:套用正向迭代器模版实现的 反向迭代器的适配器;还有 queue 和 stack 容器适配器;还有仿函数的实现,都是使用了 模版来实现的:

C++ - 优先级队列(priority_queue)的介绍和模拟实现 - 反向迭代器的适配器实现_chihiro1122的博客-CSDN博客

C++ - stack 和 queue 模拟实现 -认识 deque 容器 容器适配器_chihiro1122的博客-CSDN博客  


模版进阶

typename 前缀修饰问题的解决 

在简介当中都是类模版,我们在这里回顾一下函数模版,函数模版不想类模版一样需要 显示实例化,直接传入参数就可以按照模版来自动进行替换:

//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  前缀的话,就会报错

C++ - 模版进阶 - array_第1张图片

 我们在定义模版参数的时候,参数之前的前缀可以是  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 就是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 静态数组

 array数组和 c语言  当中的 数组,在功能上和 效率上没有任何区别,就连不能用变量来初始化个数这个特性都是一样的。而且,如果使用 array 的无参数的构造函数,里面的元素也不会进行初始化,这里和 C 当中的 数组也是一样的;

array 相比于 C当中的数组,唯一的好处就是,array 可以检查越界,而且检查非常的快,他是用assert ()断言来实现的。如果是 C语言的 数组,只是读的访问,越界是检查不出来的,写可能会检查出来;而 array 容器无论是 写还是读,都会检查出来。利用的就是 operator[] 当中的 对 下标越界的检查。

除此之外,array 容器 ,相比于数组是没有任何优点的。

你可能感兴趣的:(c++,开发语言)