【C++从0到王者】第二十站:模板进阶

这篇具有很好参考价值的文章主要介绍了【C++从0到王者】第二十站:模板进阶。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


前言

在前面我们使用模板主要是为了解决两类问题。一类是解决类里面某个数据类型,可以使用模板。 第二类就不单单是控制某种数据类型,而是控制某种逻辑,比如我们的适配器模式:传一个正向迭代器,可以适配出反向迭代器。传一个普通的容器,可以适配出栈、队列、优先级队列等。这样的好处就是我们的栈不是死的。并不单单只是一个链式栈、或者顺序栈等等,或者传一个类型过去,这个类型可以仿造函数,即仿函数,一般这个类也就是一个普通的类,只不过其重载了()运算符,导致其生成的对象可以像函数一样进行调用。它可以控制sort的升序或降序,堆的大小堆

一、typename 和 class的一些区别

typename和class在绝大多数场景下都是没有区别的,但是在一些场景下还是存在一些区别的。
如下代码所示:

在我们想要写一个打印vector里面的数据的时候,我们会写出如下代码。

#include<iostream>
#include<vector>
#include<list>
using namespace std;

void Print(const vector<int>& v)
{
	vector<int>::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
}
int main()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
	Print(v);
	return 0;
}

确实上面方法挺好用的,但是我们这个Print是否可以利用模板往泛型去写呢?答案当然是可以的,于是我们可能就会写出这样的代码,结果当我们运行的时候,报错了。

template<class Container>
void Print(const Container& v)
{
	Container::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
}

它的报错是这样的,提示说要在Container前加上typename
【C++从0到王者】第二十站:模板进阶,【C++】,c++,开发语言,list,c语言,数据结构
于是我们按照它的错误信息进行改成,代码就通过了

template<class Container>
void Print(const Container& v)
{
	typename Container::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
}

【C++从0到王者】第二十站:模板进阶,【C++】,c++,开发语言,list,c语言,数据结构

那么为什么必须要加上typename呢?

这是因为编译器在编译的时候从上往下,在编译到这里的时候这里还没有Container实例化,那么此时编译器就区分不清楚Container是什么类型,之前是vector<int>的时候它以及被实例化出来了,所以不会报错。vector的话编译器就很清楚在vector里面找到这个迭代器类型即可。而现在Container没有实例化,那么此时就会出问题,就有两种可能性:一种可能就是这里是一个静态成员变量,一种就是类里面进行typedef出来的类型。也就是说,这里到底是类型还是静态成员变量是无法区分的。所以编译器要求加上typename告诉它这里是一个类型,说明这里是合乎语法的。等模板实例化以后再去找

在我们之前优先级队列里面其实也用到了typename
【C++从0到王者】第二十站:模板进阶,【C++】,c++,开发语言,list,c语言,数据结构

二、非类型模板参数

1.非类型模板参数介绍

有时候,我们需要一些不是类型的模板参数

比如在我们想要写一个静态栈的时候,我们之前需要将N进行一个宏定义,然后再类里面之间使用,现在我们可以使用非类型模板参数,直接传递一个N过去,从而修改N的容量

template<class T,size_t N>
class Stack
{
private:
	T _a[N];
	int _top;
};
int main()
{
	Stack<int, 10> st1;
	Stack<int, 100> st2;

	return 0;
}

要注意这里的N是一个常量,不可以被修改的。否则报错。

非类型模板参数必须满足以下两点

  1. 必须是常量
  2. 必须是整型

2.array容器

【C++从0到王者】第二十站:模板进阶,【C++】,c++,开发语言,list,c语言,数据结构

数组是固定大小的序列容器:它们按照严格的线性顺序保存特定数量的元素。

在内部,数组不保留它所包含的元素以外的任何数据(甚至不保留它的大小,这是一个模板参数,在编译时固定)。就存储大小而言,它与使用该语言的括号语法([])声明的普通数组一样有效。这个类只是给它添加了一层成员函数和全局函数,这样数组就可以用作标准容器。

与其他标准容器不同,数组具有固定的大小,并且不通过分配器管理其元素的分配:它们是封装固定大小的元素数组的聚合类型。因此,它们不能动态地展开或收缩(有关可以展开的类似容器,请参阅vector)。

大小为零的数组是有效的,但它们不应该被解引用(成员front、back和data)。

与标准库中的其他容器不同,交换两个数组容器是一个线性操作,涉及单独交换范围内的所有元素,这通常是一个效率相当低的操作。另一方面,这允许两个容器中的元素的迭代器保持它们原来的容器关联。

数组容器的另一个独特特性是它们可以被视为元组对象:头重载get函数以访问数组的元素,就像它是一个元组一样,以及专门的tuple_size和tuple_element类型。

如上是关于这个容器的介绍,它就是采用了非类型模板参数,它支持的操作有下面这些
【C++从0到王者】第二十站:模板进阶,【C++】,c++,开发语言,list,c语言,数据结构

其实本质就是多加了一层函数。

array和普通的数组本质上没有太大区别,要说唯一的区别就是,对于越界的检查更加严格了。对越界读写都有检查,而普通数组不能检查越界读,少部分越界写可以检查。

三、模板的特化

1.函数模板的特化

有时候我们会遇到这样的场景

template<class T>
bool Less(T a, T b)
{
	return a < b;
}
int main()
{
	int a = 2;
	int b = 1;
	cout << Less(a, b) << endl;
	cout << Less(&a, &b) << endl;
	return 0;
}

我们期望说,比较的时候即便是指针,也能比较里面的值,但是此时我们这里比较的是两个指针的大小
【C++从0到王者】第二十站:模板进阶,【C++】,c++,开发语言,list,c语言,数据结构

为了达到我们的期望,我们可以有多种方法进行处理
如下面就是使用了模板的特化

当遇到int*类型的时候,就走的是特化

template<class T>
bool Less(T a, T b)
{
	return a < b;
}
template<>
bool Less<int*>(int* a, int* b)
{
	return *a < *b;
}
int main()
{
	int a = 2;
	int b = 1;
	cout << Less(a, b) << endl;
	cout << Less(&a, &b) << endl;
	return 0;
}

但是实际上,这样写特化不如直接就是一个函数重载更加来的方便
【C++从0到王者】第二十站:模板进阶,【C++】,c++,开发语言,list,c语言,数据结构

函数函数调用是有现成的就用现成的,没有现成的才用模板。

但是像下面这种情况就必须使用模板的特化了
【C++从0到王者】第二十站:模板进阶,【C++】,c++,开发语言,list,c语言,数据结构

即我们有时候还需要特化其他类型。就必须使用模板的特化来的更加方便

2.类模板的特化

1.全特化

如下所示,就是对Date类的特化。它的步骤也是一样的,需要对某种类型进行特殊处理。于是我们就写一个template<> ,然后比之前的Date多一个类型。这样我们就可以对某一类型特殊处理了

template<class T1, class T2>
class Date
{
public:
	Date()
	{
		cout << "Date<T1, T2>" << endl;
	}
private:
	T1 _d1;
	T2 _d2;
};


//对上面的类进行特化
template<>
class Date<int, double>
{
public:
	Date()
	{
		cout << "Date<int, double>" << endl;
	}
private:
	int _d1;
	double _d2;
};
int main()
{
	Date<int, int> d1;
	Date<int, double> d2;

	return 0;
}

运行结果如下所示

【C++从0到王者】第二十站:模板进阶,【C++】,c++,开发语言,list,c语言,数据结构

有了类了特化,这样我们之前的优先级队列就可以更加完善了。我们之前优先级队列的时候我们本身期望传入指针的时候而言按照指向的内容去比较。之前我们是直接替换了比较类,现在我们可以使用类的特化对前面进行加以修改

【C++从0到王者】第二十站:模板进阶,【C++】,c++,开发语言,list,c语言,数据结构

这样一来我们就可以进行正常比较了。(注意我们这里使用了域作用限定符,不然我们的就命名冲突了,会出事的)
【C++从0到王者】第二十站:模板进阶,【C++】,c++,开发语言,list,c语言,数据结构

像以上这些特化必须得有原模板以后才可以进行特化,向上面这种特化,将原来全部的模板参数给特化,这种特化也被称之为全特化

2.偏特化(半特化)

顾名思义,偏特化就是只特化一部分模板参数

//偏特化
template<class T1>
class Date<T1, double>
{
public:
	Date()
	{
		cout << "Date<T1, double>" << endl;
	}
private:
	T1 _d1;
	double _d2;
};

如上代码所示,我们还是对前面的Date类进行特化,这次我们只特化一个参数,那么此时称之为半特化或偏特化
【C++从0到王者】第二十站:模板进阶,【C++】,c++,开发语言,list,c语言,数据结构

上面的偏特化的作用就是部分特化。这是偏特化的一种形式

偏特化其实有两种形式:

  1. 对模板参数做类表的一部分参数特化,即部分特化
  2. 参数的更进一步限制,即偏特化不仅仅指特化部分参数,而是针对模板参数的更进一步的条件限制所设计出来的一个特化版本

针对第二点,如下就是第二种特化形式

template<class T1, class T2>
class Date<T1*, T2*>
{
public:
	Date()
	{
		cout << "Date<T1*, T2*>" << endl;
	}
};

【C++从0到王者】第二十站:模板进阶,【C++】,c++,开发语言,list,c语言,数据结构

有了偏特化的第二种形式的思想的,我们可以将前面优先级队列中的仿函数再次修改,只要是指针类型的,都进行特化

	template<class T>
	class less
	{
	public:
		bool operator()(const T& x, const T& y)
		{
			return x < y;
		}
	};
	template<class T>
	class less<T*>
	{
	public:
		bool operator()(const T* x, const T* y)
		{
			return *x < *y;
		}
	};


	template<class T>
	class greater
	{
	public:
		bool operator()(const T& x, const T& y)
		{
			return x > y;
		}
	};
	template<class T>
	class greater<T*>
	{
	public:
		bool operator()(const T* x, const T* y)
		{
			return *x > *y;
		}
	};

【C++从0到王者】第二十站:模板进阶,【C++】,c++,开发语言,list,c语言,数据结构

除了对指针类型的限制,还可以是对引用的限制,引用和指针混在一起的特化,以下是演示

template<class T1, class T2>
class Date<T1&, T2&>
{
public:
	Date()
	{
		cout << "Date<T1&, T2&>" << endl;
	}
};
template<class T1, class T2>
class Date<T1&, T2*>
{
public:
	Date()
	{
		cout << "Date<T1&, T2*>" << endl;
	}
};
template<class T1, class T2>
class Date<T1*&, T2*>
{
public:
	Date()
	{
		cout << "Date<T1*&, T2*>" << endl;
	}
};

【C++从0到王者】第二十站:模板进阶,【C++】,c++,开发语言,list,c语言,数据结构

三、模板的分离编译

我们之前在C语言的时候特别喜欢声明和定义分类。在C++中,当我们试着分离的时候
【C++从0到王者】第二十站:模板进阶,【C++】,c++,开发语言,list,c语言,数据结构
编译器报错了报的是一个链接错误
【C++从0到王者】第二十站:模板进阶,【C++】,c++,开发语言,list,c语言,数据结构

但是如果调用size等接口的话,模板又正常了。不报错误。

可而得知,是类成员函数的声明和定义分离时出现的链接错误。即没有找到这个函数的地址。

这种错误就类似于我们定义了一个类,这个类是如下进行定义的,一个类声明了两个函数,但是只实现了一个函数。另外一个函数没有被实现。
【C++从0到王者】第二十站:模板进阶,【C++】,c++,开发语言,list,c语言,数据结构

于是此时我们的func2函数在调用的时候就会报错,且错误类型还是一样的。链接错误,即找不到地址。
【C++从0到王者】第二十站:模板进阶,【C++】,c++,开发语言,list,c语言,数据结构

这里其实就涉及到我们的编译链接过程了。在test.c文件中,对于stack类,它的其他成员函数在编译的时候就已经找到地址了。而push和pop都只有声明,在编译阶段都是没有地址的。

在编译阶段虽然他们没有地址,但是由于有声明,相当于一种承诺。所以自然不会报错

编译阶段只看声明, 声明是一种承诺,所以编译检查声明函数参数返回可以对上,等着链接的时候,拿着修饰后的函数去其他文件符号表查找

到了链接阶段我们此时的现象是

  1. func1链接查到了
  2. func2链接没有查到。因为func2没有定义
  3. push链接查不到,但是我们的push定义了

那么为什么会出现第三中情况的,我们究其原因,是因为他们是分别编译的。stack.o文件就没有生成地址,因为压根就不知道这个T是什么类型的,就没办法去生成地址。没法实例化
【C++从0到王者】第二十站:模板进阶,【C++】,c++,开发语言,list,c语言,数据结构

那么如何解决呢?其实我们可以显式实例化,即我们直接在函数是实现中,写一个template,注意不要带尖括号,然后class stack<int>即可
【C++从0到王者】第二十站:模板进阶,【C++】,c++,开发语言,list,c语言,数据结构

但是这里还是存在一些问题的,因为治标不治本,如果我们在主函数中又用一个double类型的,那么又要添加一个显式实例化。

namespace Sim 
{
	template<class T ,class Container>
	void stack<T,Container>::push(const T& val)
	{
		_con.push_back(val);
	}
	template<class T, class Container>
	void stack<T,Container>::pop()
	{
		_con.pop_back();
	}

	template
	class stack<int>;

	template
	class stack<double>;
}

模板的声明和定义如果通过分文件的方式,显然是不太合适的。我们如果要将其分类,可以在同一个文件内进行分离。

这样是由于test文件是知道模板要实例化为什么类型的,所以就不用进行显式实例化了

namespace Sim
{
	template<class T, class Container = deque<T>>
	class stack
	{
	public:
		void push(const T& val);
		void pop();
		const T& top()
		{
			return _con.back();
		}
		size_t size()
		{
			return _con.size();
		}

		bool empty()
		{
			return _con.empty();
		}
	private:
		Container _con;
	};
	template<class T, class Container>
	void stack<T, Container>::push(const T& val)
	{
		_con.push_back(val);
	}
	template<class T, class Container>
	void stack<T, Container>::pop()
	{
		_con.pop_back();
	}

};

即便是stl库里面,也是这样做的,小函数定义在类里面,大函数定义在类外面,但是声明和定义分离是放在同一个文件的。

有时候我们会看见这些模板的库的后缀是.hpp,意思就是声明和定义放在一个文件中,这只是一个名字的暗示。

四、总结

【优点】

  1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
  2. 增强了代码的灵活性

【缺陷】

  1. 模板会导致代码膨胀问题,也会导致编译时间变长
  2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误

好了本期内容就到这里了
如果对你有帮助的话,不要忘记点赞加收藏哦!!!
有任何关于文章的问题,可以直接私信我或者评论区留言哦!!!
文章来源地址https://www.toymoban.com/news/detail-643176.html

到了这里,关于【C++从0到王者】第二十站:模板进阶的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 学C的第二十六天【指针的进阶(二)】

    ========================================================================= 相关代码gitee自取 :C语言学习日记: 加油努力 (gitee.com)  ========================================================================= 接上期 : 学C的第二十五天【指针的进阶(一)】_高高的胖子的博客-CSDN博客  ================================

    2024年02月13日
    浏览(76)
  • 网络安全进阶学习第二十一课——XML介绍

    XML(eXtensible Markup Language),可扩展标记语言,是一种标记语言,使用简单标记描述数据;(另一种常见的标记语言是HTML) XML是一种非常灵活的语言, 没有固定的标签,所有标签都可以自定义 ; 通常 XML被用于信息的传递和记录 ,因此,xml经常被用于充当配置文件。如果把

    2024年02月06日
    浏览(48)
  • 网络安全进阶学习第二十课——CTF之文件操作与隐写

    ------ 当文件没有文件扩展名,或者具有文件扩展名但无法正常打开时,可以根据识别到的文件类型进行修改文件扩展名,从而使文件能够正常打开。 使用场景:不知道后缀名,无法打开文件。 格式: file myheart 这里就识别到是一个PCAP的流量包 ------ 通过WinHex程序可以查看文件

    2024年02月07日
    浏览(43)
  • C语言第二十四弹---指针(八)

    ✨ 个人主页:   熬夜学编程的小林 💗 系列专栏:   【C语言详解】   【数据结构详解】 指针 1、数组和指针笔试题解析  1.1、字符数组 1.1.1、代码1: 1.1.2、代码2: 1.1.3、代码3: 1.1.4、代码4: 1.1.5、代码5: 1.1.6、代码6: 总结    1.1.1、代码1: char arr[] = {\\\'a\\\',\\\'b\\\',\\\'c\\\',\\\'d\\\',\\\'e

    2024年02月20日
    浏览(76)
  • C语言第二十七弹---内存函数

    ✨ 个人主页:  熬夜学编程的小林 💗 系列专栏:  【C语言详解】 【数据结构详解】 内存函数 1、memcpy 使用和模拟实现 2、memmove 使用和模拟实现 3、memset 函数的使用 4、memcmp 函数的使用 总结 前面两弹讲解了字符函数和字符串函数,但是在我们实际运用中不仅仅只有这些

    2024年02月19日
    浏览(38)
  • 【从零开始学习C++ | 第二十一篇】C++新增特性 (上)

    目录  前言: 委托构造函数: 类内初始化: 空指针: 枚举类: 总结:         C++的学习难度大,内容繁多。因此我们要及时掌握C++的各种特性,因此我们更新本篇文章,向大家介绍C++的新增特性。 委托构造函数是指一 个类的构造函数调用另一个类的构造函数,以减少代

    2024年02月13日
    浏览(69)
  • 【从零开始学习C++ | 第二十二篇】C++新增特性(下)

    目录 前言: 类型推导: constexpr: 初始化列表: 基于范围的for循环: 智能指针之unique ptr Lambda表达式: 总结:         本文我们将继续介绍   C++ 11 新增十大特性的剩余六个,如果没有看过介绍前四个特性的小伙伴的可以点进我C++的专栏就可以看到。 类型推导(

    2024年02月14日
    浏览(63)
  • 盖子的c++小课堂——第二十一讲:map

    时隔一周,我又来更新了^_^,今天都第二十一讲了,前三个板块马上就结束了,也就是小课堂(1)马上结束了, 敬请期待“盖子的c++小课堂(2)” ,嘿嘿~~ 数据容器——一对一映射 每个人都有对应一个身高 每个string对应一个double 每个阿拉伯数字都有对应一个拼写 每个i

    2024年02月15日
    浏览(37)
  • C语言第二十六弹---字符串函数(下)

    ✨ 个人主页:   熬夜学编程的小林 💗 系列专栏:   【C语言详解】   【数据结构详解】 目录 1、strncat 函数的使用 2、strncmp 函数的使用 3、strstr 函数的使用和模拟实现 4、strtok 函数的使用 5、strerror 函数的使用 6、perror 函数的使用 总结 • Appends the first num characters of sourc

    2024年02月20日
    浏览(73)
  • 【C++进阶】模板进阶

    👦个人主页:@Weraphael ✍🏻作者简介:目前学习C++和算法 ✈️专栏:C++航路 🐋 希望大家多多支持,咱一起进步!😁 如果文章对你有帮助的话 欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨ 初阶模板地址:点击跳转 在以前博客我们说过,定义模板参数可以用 typename ,

    2024年02月10日
    浏览(30)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包