STL是一种泛型编程。面向对象关注的是编程的数据方面,而泛型编程关注的是算法。它们之间的共同点是抽象和创建可重用代码,但它们的理念绝然不同。—— 《C++ Primer Plus》
一、迭代器
1、为何使用迭代器
模板使得容器能够独立于存储的数据类型,而迭代器使算法独立于使用的容器类型。模板使得类在编译具体化时才关注具体的模板参数,而迭代器则将数据的存储方式和访问处理分离。
如果我们想在数组和链表中查找某个数值,其实现可能如下:
#include <iostream>
using namespace std;
struct Node
{
Node* next;
int item;
};
int main()
{
int arr[]{
1,2,3 };
Node* nodeStart;
Node* nodeEnd;
...
for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
{
if (arr[i] == 3)
{
cout << "find 3 at " << i << endl;
}
}
for (Node* node = nodeStart; node != nullptr; node = node->next)
{
if (node->item == 3)
{
cout << "find 3 at " << node << endl;
}
}
}
使用迭代器后,代码可以被优化为:
int main()
{
vector<int> vec{
1,2,3 };
list<int> li{
1,2,3 };
auto iterVec = find(vec.begin(), vec.end(), 3);
auto iterList = find(li.begin(), li.end(), 3);
}
find正是根据STL根据iterator所提供的搜索函数:
template <class _InIt, class _Ty>
_NODISCARD _CONSTEXPR20 _InIt find(_InIt _First, const _InIt _Last, const _Ty& _Val) {
// find first matching _Val
_Adl_verify_range(_First, _Last);
_Seek_wrapped(_First, _Find_unchecked(_Get_unwrapped(_First), _Get_unwrapped(_Last), _Val));
return _First;
}
这里vec.end() 一般指向尾指针,一个不存在的元素,也就是我们通常所说的哨兵节点。这里尾指针的存在并不是对迭代器的要求,而是对容器类的要求。
2、++的前缀和后缀版本
_Vector_iterator& operator++() noexcept {
_Mybase::operator++();
return *this;
}
_Vector_iterator operator++(int) noexcept {
_Vector_iterator _Tmp = *this;
_Mybase::operator++();
return _Tmp;
}
通过是否带一个参数int区分。不带int为前缀,带int为后缀。
3、迭代器类型
不同的算法对迭代器的要求不同。查找算法需要遍历迭代器,因此需要迭代器实现++运算符;它只需要读取数据而不需要修改数据。排序算法要求能够随机访问,以交换不同位置的元素,因此需要实现+运算符;它需要能够写数据,因此要重写=运算符。STL定义了五种迭代器:输入迭代器、输出迭代器、正向迭代器、双向迭代器和随机访问迭代器。下面我们学习下几种迭代器的特征:
输入迭代器 — 支持读取数据;通过支持++运算符实现访问其中所有元素;并不保证每次遍历容器所得到的元素顺序相同;迭代器递增之后不能保证之前的值仍可用;不能逆向访问。
输出迭代器 — 支持写入数据;其余同输入迭代器。
正向迭代器 — 支持读取和写入数据;通过支持++运算符实现访问其中所有元素;保证每次遍历容器所得到的元素顺序相同;迭代器递增之后之前的值仍可用。
双向迭代器 — 同时支持++和–操作符;其余同正向迭代器。
随机访问迭代器 — 支持随机访问(如下图),其余同双向迭代器。
这里不同的迭代器只是一种概念性描述。为了提高效率,我们应该在编程中使用要求最低的迭代器。我们看看一些不同层次的迭代器的使用:
#include <iostream>
#include <vector>
#include <list>
#include <forward_list>
using namespace std;
int main()
{
// LegacyRandomAccessIterator
vector<int> vec{
1,2,3 };
auto iterRandom = vec.begin();
iterRandom += 10;
// LegacyBidirectionalIterator
list<int> li{
1, 2, 3 };
auto iterBi = li.begin();
iterBi--;
//li += 10; invalid
// LegacyForwardIterator
forward_list <int> liFw{
1, 2, 3 };
auto iterForward = liFw.begin();
iterForward++;
//iterForward--; invalid
}
}
4、迭代器层次结构
5、concept refinement model
STL使用术语concept来描述一系列的要求。概念可以具有类似继承的关系。这就是说我们可以认为双向迭代器继承了正向迭代器的功能。但是我们不能将继承机制用于迭代器。因为迭代器可以用内置类型实现。例如,我们可以使用类实现正向迭代器,而使用指针实现双向迭代器。因此,我们使用refinement描述这种概念上的继承。概念的具体实现被称为model。因此指向某种类型的指针是一个随机访问迭代器的模型。
C++20中引入了关键字concept,其作用正对应于我们上面所提到的术语concept。
#include <iostream>
#include <concepts>
using namespace std;
class CLS_DisableCopy
{
public:
int a;
CLS_DisableCopy() {
}
CLS_DisableCopy(const CLS_DisableCopy&) = delete;
};
template<copy_constructible T>
T f(T a)
{
T b(a);
return b;
}
int main()
{
f<string>("abc");
CLS_DisableCopy obj;
obj.a = 1;
auto copy = f<CLS_DisableCopy>(obj); // invalid
}
这里的copy_constructible就是使用关键字concept定义的一种规范。不满足此规范的类不能被应用与函数模板上。现在,我们使用迭代器相关的concept重新定义我们的find和sort函数。
#include <iterator>
#include <concepts>
#include <list>
using namespace std;
template <input_iterator _TIterator, class _TValue>
_TIterator myFind(const _TIterator &start, const _TIterator &end, const _TValue &value)
{