c++ 模版进阶 + 反向迭代器的模拟实现

1. 非类型的模版参数

1.1. 用法介绍

先看下面一段代码:

#define N 10
template<class T>
class stack
{
private:
	T _a[N];
};
int main()
{
	stack<int> s1;
	stack<int> s2;
	return 0;
}

这段代码,我们有一个 栈(stack)类,这里内部成员设置的是 静态数组。我们宏定义了 N 为 10,默认数组开辟出来的大小是10。
主函数中我们用这个类创建了 s1, s2 两个对象,如果我们想要让 s1 有 10 块空间,s2 有 100块空间,我们应该怎么做?就是把 N 的值改一改,改成 100。
但是如果这个差距更大呢
s1 需要10块空间, s2 需要10000块空间,如果把 N改成10000,那么对 s1 来说损耗太大了。
所以 c++ 为了解决这种问题,有一个非类型模版参数的东西

template<class T, size_t N>
class stack
{
private:
	T _a[N];
};
int main()
{
	stack<int, 10> s1;
	stack<int, 10000> s2;
	return 0;
}

这里注意类模版那个位置,我们传入了 size_t N
这里有点类型函数传参,当传入的 10 时,N 接收到10,类中所有 N 就是10,传入10000也是同样的。
在这里插入图片描述
这样我们就能通过传入数据来改变结构,不需要改底层源代码

1.2. 注意

  1. 这里只能传入常量,不能传入变量
int main()
{
	int n;
	cin >> n;
	stack<double, n> s3;
}

这里要是传入变量n,就会直接报错
传入变量,编译器实例化的时候就不知道怎么实例化,导致出现问题
在这里插入图片描述
2. 传入的类型只能是整形
如果传入的 double, 也是会报错的
在这里插入图片描述

2. 类模版特化

2.1. 用法

模版特化,类模板之前学过,特化,就是针对某些情况,特殊化处理。
看下面一段代码:

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;
	}
};
//
int main()
{	
	Date<int, int> d1;
	Date<int, double> d2;

	return 0;
}

上面我们创建了一个 Date 类,里面写上了 析构函数,当用它创建对象的时候,就一定会输出析构函数中的 “Date<T1, T2>”
而中间那部分就是特例的写法,当传入的是类型是 <int, double> 的时候,就会创建这里的对象并输出 “Date<int, double>”
在这里插入图片描述
特化上面的 “template<>” 是必须要写上的,不可省略。
模版特化,和之前学的函数重载有点像,在传入不同类型时,走不同的函数。
但是要注意 类模板特化 和 函数重载 很不同,类模板特化在实例化之前什么都不是,实例化后才有类,而函数重载在各个函数写出来的时候就存在了。
我们也可以把传入指针类型作为一种特例

template<class Date>
class Compare
{
public:
	Compare<Date x, Date y)
	{
		return x < y;
	}
};

template<>
class Less<Date*>
{
public:
	bool operator()(Date* x, Date* y)
	{
		return *x < *y;
	}
};

当传入的参数是 Date* 的类型时,就会直接使用特例。

2.2. 全特化,偏特化

上面我们传入的模版参数数量,有两个,有一个
全特化:所有的模版参数全部都特例化
偏特化:对部分模版参数实现特例

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

这里的偏特化,当模版传入的第二个参数是 double 类型,就会自动调用偏特化的特例。
当全特化和偏特化都存在的情况下,优先使用全特化
在这里插入图片描述
第一个偏特化,第二个调用全特化,第三个没有调用特化。

2.3. 使用注意

  1. 有了类之后才能去特化,没有写类之前不能写特化
template<>
class Date<int, char>
{
public:
    Date() 
    {
        cout << "Date<int, char>" << endl;
    }
private:
    int _d1;
    int _d2;
};

前面不写类,直接写 Date 特化后的会报错
在这里插入图片描述

  1. 模版特化和函数重载
    先看下面代码:
template<class T>
bool Less<T left, T right)
{
    return left <right;
}
template<>
bool Less<Date*>(Date* left, Date* right)
{
    return *left < *right;
}

我们这里的 Less 函数,会比较两个值大小,下面的特化是当类型为 Date* 时,对该类型特殊处理
但是如果只是比较两个数的大小,我们并不会这样传参数,我们可能这样写

bool Less(const T& left, const T& right)
//...
bool Less<Date*>(const Date* & left, const Date* & right)
//..

这样写对吗?
特例中的 const,我们希望它修饰的是引用,引用的值不能改变,但是这里const 修饰的却是 Date* 这个指针。
正确的写法应该是

bool Less<Date*>(Date* const & left, Date* const & right)

这样写还是有问题,我们模版特例化出来的类型,和传入的类型不相同,那 Less 后面 “<>” 中应该填什么类型我们不能写 “Date* const”
所以这里既然是函数,我们就可以考虑函数重载

template<class T>
bool Less(const T& left, const T& right)
{
	return left < right;
}
bool Less(Date* const & left, Date* const & right)
{
	return *left <*right;
}

这里要注意的是,传入 Date* 的这个函数,和上面的模版是不构成函数重载的,模版在实例化之前什么都不是,只有实例化之后的函数才能和下面的函数称为函数重载。

3. 模版的分离编译

3.1. 什么是分离编译

分离编译:一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件连接起来形成一个可执行文件的过程
简单来理解,像以前写的 .h 文件中放声明, .cpp 文件中放定义的方式就是 分离编译模式
先看下面一段代码:
ADD.h

#include<iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right);

void func();

ADD.cpp

#include"ADD.h"
template<class T>
T Add(const T&left, const T& right)
{
	cout << "Add(const T& left, const T& right); 
	return left  + right;
}
void func()
{
	cout << "void func()" << endl;
}

test.cpp

#include"ADD.h"
int main()
{
	Add(1,2);
	func();
	return 0;
}

这里,我们把 Add函数模版的声明和定义分别放在 ADD.h 和 ADD.cpp 文件中
在这里插入图片描述
运行之后,就会出现链接错误
但是只允许 func() 不带模版的函数
在这里插入图片描述
可以正常使用
问题:
还是预处理阶段出现的问题
4个阶段

  1. 预处理,头文件展开,宏替换 …
    ADD.i test.i
  2. 编译, 检查语法生成汇编指令
    ADD.s test.s
  3. 汇编,汇编代码转化成二进制机器码
    ADD.o test.o
  4. 链接 a.out
    前面生成的文件都是单个的,链接需要合并这些文件
    如果一个函数是分离编译模式
    在编译时,每个文件中单独编译,而 Add模版函数 和 func 函数因为在 ADD.h 文件中有声明,所以这里会正常通过。
    但是在链接的时候,func 函数是可以找到他的定义的,而 Add 在其他 cpp 文件中找不到定义,所以会出现问题。
    模版函数,在实例化之前什么都没有,只有实例化后的才叫函数

3.2. 解决方法

  1. 显示实例化
template
int Add<int>(const int& left, const int& right);

既然 模版函数在 cpp 文件中找不到定义,那么我们就可以想办法在 cpp 文件中让他实例化出来
也就是上面这段代码,写入 ADD.cpp 文件中,此时程序就能正常运行
在这里插入图片描述
模版是可以分离编译的,必须要显示实例化
但是这种方法也有缺陷

Add(1.1, 2.2);

我们实例化了 int ,没实现 float或double,所以这里当传入的类型是浮点型的时候,仍然会出错
所以我们需要加上需要实例化的函数

template
double Add<double>(const double& left, const double& right);

在这里插入图片描述
此时代码可以正常运行
除了函数模版,类模版也可以这样玩
类模版的显示实例化
stack.h

template<class T>
class Stack
{
public:
	void push(const T& x);
	void pop();
private:
	T* _a = nullptr;
	int _top;
	int _capacity;
};

stack.cpp

template<class T>
void Stack<T>::push(const T& x)
{
	cout << "void Stack<T>::push(const T& x)" << endl;
}
template<class T>
void Stack<T>::pop()
{
	cout << "void Stack<T>::pop()" << endl;
}

和上面函数模版的问题一样,链接会报错
在这里插入图片描述
原因还是链接过程,声明找不到定义
所以这里还是需要显示实例化。
但是模版可以特殊点:

template
class Stack<int>;

类的显示实例化确实比函数能简单点,但是想要分离编译还是需要把每种情况都显示实例化出来,所以也不算很方便。
最好的解决方法
不使用 ADD.cpp ,也就是使用模版的时候全部写进 ADD.h 文件中,这样就省去了一堆显示实例化的麻烦。
模版支持分离编译,但是一定要显示实例化。

4. 模版小结

模版优点:

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

缺点:

  1. 模版会导致代码膨胀问题,也会导致编译时间变长
  2. 出现模版编译错误时,不易定位错误

5. 反向迭代器的模拟实现

3.1. 正向反向迭代器

前面我们模拟实现了 string, vector, list, stack, queue, priority_queue 的容器,实现了大部分的成员函数及迭代器。但是我们一直没有写反向迭代器。
前面学的 vector, string,虽然底层都很麻烦,但是使用起来很方便,就是因为容器进行了封装。
我们先简单回忆回忆正向迭代器
迭代器的封装屏蔽了底层的复杂细节,我们模拟实现的正向迭代器,vector 使用的是原生指针, list 使用的是 一个自定义类型,然后通过运算符重载实现迭代器 ++ 等操作。

list<int>::iterator it;
list_node<int>* ptr;

虽然他们的迭代器在使用方面没什么区别,但是一个是自定义类型,一个是内置类型,物理上我们理解是一样的,但是实际上不同。

*it;
++it;
*ptr;
++ptr;

上面是对自定义类型的操作,下面是对内置类型的操作,虽然两个类型完全不同,但是使用方法一致。
反向迭代器
反向迭代器与正向迭代器使用方面,没什么区别,只不过反向迭代器 ++ 就是从后向前走,和正向迭代器的 – 一样。
反向迭代器要支持倒着遍历

int main()
{
	list<int> it;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(5);

	list<int>::iterator it = lt.begin();
	while(it != lt.end())
	{
		cout << *it << "  ";
		++it;
	}
	cout << endl;
	list<int>::reverse_iterator rit = lt.rbegin();
	while(rit != lt.rend())
	{
		cout << *rit << endl;
		++rit;
	}
	return 0;
}

在这里插入图片描述

3.2. list 反向迭代器的模拟实现

list 是 带头双向循环链表,begin() 在第一个元素位置,end() 在最后一个元素后面(头结点)。
rbegin() 和 rend() 是反向迭代器的函数,他们返回就需要注意
在这里插入图片描述
rbegin() 指向最后一个元素, rend() 指向的是第一个元素前面的位置(头结点)。
我们上面简单看了正向迭代器和反向迭代器,我们发现他们没有什么太大的区别,就是遍历方向不对,所以这里我们模拟实现反向迭代器的思路是使用正向迭代器适配出反向迭代器

template<class Iterator>
class ReverseIterator
{
typedef ReverseIterator<Iterator> self;
public:
	ReverseIterator(Iterator it)
		:_it(it)
	{}
	self& operator++()
	{
		it--;
		return *this;
	}
	bool operator!=(const self& s)
	{
		return _it != s._it;
	}
private:
	Iterator _it;
}

注意,我们这里传入的是 模版,而在下面 ++, != 的操作,所有容器的反向迭代器都是这样的逻辑,因此,这段反向迭代器的代码在我们前面模拟实现过正向迭代器的容器中都可以使用。
同时,要想让对应容器能使用这里的类,我们还要在容器内加一点内容

template<class T>
class list
{
	//...
	typedef ReverseIterator<iterator> reverse_iterator;
	reverse_iterator rbegin()
	{
		return reverse_iterator(end()-1);
	}
	reverse_iterator rend()
	{
		return reverse_iterator(end());
	}
	//...
};

当我们在主函数验证我们的代码时

int main()
{
	xsz::list<int::reverse_iterator rit = lt1.rbegin();
	while(rit != lt1.rend())
	{
		cout << *rit << endl;
		++rit;
	}
	return 0;
}

在这里插入图片描述
这里显示 rit 无法解引用,这里要注意,我们是用正向迭代器适配出了反向迭代器这个类,从这个含义上说,反向迭代器是个新的类,只不过是反向迭代器的内部成员 _it 的类型是正向迭代器,而反向迭代器的类创建出来的对象和正向迭代器无关。
所以这里解引用之类的函数,还需要我们手动实现

operator*()
{
	return *_it;
}

和上面想法一样,直接调用正向迭代器的解引用,但是有个问题,我们应该返回什么类型。
因为我们传入的只是 Iterator,这个 iterator 可能是内置类型,也可以是自定义类型,也可能是 有const 修饰的类型
这里的可能性太多,回过头看我们前面模拟实现正向迭代器,是这样写的

template<class T, class Ref, class Ptr>
class __list_iterator
{
	//...
}
template<class T>
class list
{
	typedef __list_iterator<T, T&, T*> iterator;
	typename typedef __list_iterator __list_lierator<T, cosnt T&, const T*> const_iterator;
	//...
};

正向迭代器这样传的话,那我们直接原封不动的把类型传入反向迭代器即可

template<class Iterator, class Ref, class Rtr>
class ReverseIterator
{
public:
	typedef ReverseIterator<Iterator, Ref, Ptr> self;
	//...
	Ref operator*(0
	{
		return *_it;
	}
	Ptr operator->()
	{
		return _it.operator->();
	}
private:
		Iterator _it;
};

template<class T>
class list
{
	//...
	typedef ReverseIterator<Iterator, T&, T*> reverse_iterator;
	typedef ReverseIterator<const_iterator, const T&, const T*> const_reverse_iterator;
	reverse_iterator rbegin()
	{
		return reverse_iterator(end() - 1);
	}
	reverse_iterator rend()
	{
		return reverse_iterator(end());
	}
	const_reverse_iterator rbegin()const
	{
		return const_reverse_iterator(end() - 1);
	}
	const_reverse_iterator rend()const
	{
		return const_reverse_iterator rend();
	}
	//...
};

这里要注意,重载 ->, 我们不能直接写 " return _it-> " , 我们只能直接调用 _it 的运算符重载函数。
在这里插入图片描述
注意这里的关系,当传入什么类型,Reverse_iterator 的类就接收什么类型,然后进行处理。
这样的话,就不需要我们手动去改const 和 非const 的情况。
这里,我们就简单地用正向迭代器适配出了反向迭代器,比我们自己再去实现一个反向迭代器轻松很多。
需要注意:
我们必须先有正向迭代器才能去适配反向迭代器。
反向迭代器的本事还是适配器。

3.3. vector 的反向迭代器模拟实现

在这里插入图片描述
vector 的 begin(), end(), rbegin(), rend()
如果我们需要获取 rbegin() 的位置,只需要将 end()–即可
想要获取 rend() 的位置,只需要将 rbegin()-- 即可
然后我们就可以在 vector 中去写

template<class T>
class vector
{	
	//...
	typedef ReverseIterator<iterator, T&, T*> reverse_iterator;
	typedef ReverseIteratir<const_iterator, const T&, const T*> const_reverse_iterator;
	reverse_iterator rbegin()
	{
		return reverse_iterator(--end());
	}
	reverse_iterator rend()
	{
		return reverse_iterator(--begin());
	}
	const_reverse_iterator rbegin()const
	{
		return const_reverse_iterator(--end());
	}
	const_reverse_iterator rend()const
	{
		return const_reverse_iterator(--begin());
	}
	//...
};

vector 的反向迭代器简单实现后,我们试试

int main()
{
	xsz::vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);
	xsz::vector<int>::reverse_iterator rit1 = v1.rbegin();
	while(rit1 != rend())
	{
		cout << *rit1 << "  ";
		rit1++;
	}
	return 0;

但是当我们运行的时候,这里出现问题了
在这里插入图片描述
显示 --end() 这里需要左值。
解决
我们先分析为什么会产生这个错误
同样的写法,list 没有报错,vector 却报错了
我们之前实现的 vector 的迭代器是内置类型,直接使用的指针

typedef T* iterator;
iterator end()
{
	return _finish;
}

在实现 list 的时候,我们使用的是 自定义类型

typedef __list_iterator<T, T&, T*> iterator
iterator end()
{
	return iterator(_head->_next);
}

首先,先明确一点,不管函数内返回的是内置类型,还是自定义类型,他们返回的都是这个值的临时拷贝,不会把这块空间返回去。
而临时对象具有常性
“–” 的时候,对常量 – ,所以这里会出问题,显示–需要左值。
但是为什么 list 没有报错?
因为 c++ 编译器的特殊处理,自定义类型在调用 – 的时候,会去调用成员函数,而 const 会自动调用 非const 的成员函数,这种情况只是特殊处理,不合理但是编译器就是这样实现的
举例:

#include<iostream>
using namespace std;
class A
{
public:
	A(int x = 0)
		:_a(x)
	{}
	int Print()
	{
		return 0;
	}
	int& operator--()
	{
		return _a--;
	}
private:
	int _a;
};
int main()
{
	A& r = A();
	A().Print();
}

这里有一个 A类,主函数中我们用 A类创建了一个匿名对象,并用 r 引用这个匿名对象。
匿名对象具有常性,创造匿名对象后,我们就不能使用引用来处理这个匿名对象
在这里插入图片描述
我们的 Print 函数是没有 const 修饰的,按理来说,A() 这个匿名对象不能调用没有 const 修饰的 Print 函数,但是这里优化的可以调用。
同样,我们上面写了 – 的运算符重载,因为这里的优化,所以这样写不会报错

--A();

在这里插入图片描述
所以为了避免出现这种问题,我们写的时候一定要注意,如果 – 不是很必要,可以写 end() - 1 这种方式,从而减少错误的出现
在这里插入图片描述

3.4. 库中的反向迭代器

上面我们是使用了正向迭代器适配反向迭代器,但是这种实现方法并不唯一。
我们从上面可以看见,只要能实现倒着遍历的方式都可以。
库中也用了正向适配反向的写法,但是和我们写的不太一样
在这里插入图片描述
左边是我们的思路,右边是库中的
rbegin() 在 end() 的位置,rend() 在 begin() 的位置。
这种写法和我们的逻辑完全对不上。
这里我们简单分析一下库中的方法
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

我们主要看这里的代码
上面我们写解引用的时候,是直接调用的正向迭代器的解引用操作,除此之外没有做其他操作,但是这里 _Tmp 接收之后,还进行了 前置" – " 的操作。
也就是说当我们调用的时候,解引用出来的是当前位置前一个节点的值。
这样写虽然不是很好理解,但是在理解结构上能轻松点,end()对应rbegin(), begin() 对应 rend(),达到一个对称的效果。
代码实现:

template<class Iterator, class Ref, class Ptr>
class ReverseIterator
{
public:
	typedef ReverseIterator<Iterator, Ref, Ptr> self;
	ReverseIterator(Iterator it)
		:_it(it)
	{}
	self& operator++()
	{
		--_it;
		return *this;
	}
	bool operator!=(const self& s)
	{
		return _it != s._it;
	}
	Ref operator*()
	{
		Iterator cur = _it;
		return *(--cur);
	}
	Ptr operator->()
	{
		return &_it.operator->();
	}
private:
	Iterator _it;
};

在这里插入图片描述
这样就简单模拟实现了库中反向迭代器的写法

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值