本节课要点:
- 容器
- 迭代器
一、容器
1. 基本概念
容器:能够存储(任意类型)对象的类(模板)称为容器。
容器是一个很纯粹的概念,它的设计目的主要是为了存储对象,对象的类型对它来说并不重要。因此,容器一般被设计成为类模板。
2. 遍历操作
我们之前为双向链表类编写的遍历函数:
void traverse(callback f) {
for (auto p = head.next; p != &tail; p = p->next)
f(p->data);
}
回调函数的类型受限,因此我们使用模板进行优化:
template <typename callback>
void traverse(callback&& f) {
for (auto p = head.next; p != &tail; p = p->next)
f(p->data);
}
如果还有其它参数,我们可以优化为:
template <typename callback, typename ...types>
void traverse(callback&& f, types&& ...args) {
for (auto p = head.next; p != &tail; p = p->next)
f(p->data);
}
为了实现完美转发,我们使用 types&&,避免类型折叠。
虽然带回调函数的遍历能够较灵活地访问容器元素,但是还是不够好。
二、数组的迭代器
容器的遍历操作往往与循环/迭代(iteration)关联。
这里我们以原生数组的遍历为例来说明迭代的一般性过程。
1. 迭代的特点
① 迭代是一个循环结构。
② 迭代的关键是设施是指针。
③ 迭代有起点,起点标记称为首迭代器。
④ 迭代有终点,终点标记称为哨兵迭代器。
⑤ 指针推进,使用 ++ 运算符完成。
⑥ 元素选取,使用 * 运算符完成。
⑦ 成员选择,使用 -> 运算符完成。
2. 数组的迭代器
//array.h
#pragma once
#include <iostream>
#include <exception>
#include <initializer_list>
#include <cassert>
template <typename value_t, size_t capacity>
class array {
public:
using value_type = value_t;
using pointer = value_type*;
using reference = value_type&;
value_type arr[capacity];
void _copy(const array& a) {
for (size_t i = 0; i < a.size(); ++i)
arr[i] = a.arr[i];
}
public:
array() noexcept = default;
array(const std::initializer_list<value_type> &l) noexcept {
assert(l.size() <= capacity);
size_t i = 0;
for (auto &v : l)
arr[i++] = v;
}
array(const array &a) noexcept {
_copy(a);
}
array(array &&a) = delete;
array &operator=(const array &a) {
assert(capacity == a.size());
_copy(a);
return *this;
}
array &operator=(array &&a) = delete;
~array() noexcept {}
reference at(size_t index) try {
if (index >= capacity)
throw std::out_of_range("out of range");
return arr[index];
} catch (std::out_of_range& e) {
std::cout << e.what() << std::endl;
exit(1);
}
reference operator[](size_t index) {
return this->at(index);
}
size_t size() const {
return capacity;
}
using iterator = pointer;
iterator begin() {
return arr;
}
iterator end() {
return arr + capacity;
}
using reverse_iterator = pointer;
reverse_iterator rbegin() {
return arr + capacity - 1;
}
reverse_iterator rend() {
return arr - 1;
}
template <typename ...types>
void emplace(iterator pos, types && ...args) {
at(pos - arr) = value_type(args...);
}
};
前面是我们已经完成的数组类内容,新增内容为:
using iterator = pointer;
iterator begin() {
return arr;
}
iterator end() {
return arr + capacity;
}
using reverse_iterator = pointer;
reverse_iterator rbegin() {
return arr + capacity - 1;
}
reverse_iterator rend() {
return arr - 1;
}
对于数组这种使用顺序存储模式的容器,原生指针就是其最简单、最使用的迭代器。因此,为这样的容器设计迭代器时,可以将其设计为原生指针的别名,我们称它为伪迭代器。
using iterator = pointer; //前向/正向迭代器
using reverse_iterator = pointer; //逆向迭代器
因为迭代器本身就是原生指针,所以无须为它重载必需的运算符函数。
设置首迭代器和哨兵迭代器:
iterator begin() {
return arr;
}
iterator end() {
return arr + capacity;
}
reverse_iterator rbegin() {
return arr + capacity - 1;
}
reverse_iterator rend() {
return arr - 1;
}
注意:哨兵是最后一个元素的下一个。
只要类拥有了迭代器,就能进行基于范围的 for 循环:
#include <iostream>
#include "array.h"
int main() {
array<int, 5> a{1, 2, 3, 4, 5};
for (auto v : a)
std::cout << v << ' ';
std::cout << std::endl;
return 0;
}
三、常规容器的迭代器
对照迭代的特点,我们的迭代操作应该有如下特点:
1. 使用迭代器的迭代的特点
① 迭代使用循环。
② 迭代器模拟原生指针,即是对原生指针的包装。
③ 设置首迭代器。
④ 设置哨兵迭代器。
⑤ 迭代器应具有的操作:
- 复制迭代器,使用 = 运算符完成。
- 比较迭代器,使用 != 运算符完成。
- 推进迭代器,使用 ++ 运算符完成。
- 返回迭代器所指元素,使用 * 运算符完成。
- 返回迭代器内部指针,使用 -> 运算符完成。
⑥ 迭代器类是容器的内部类,且是一个依赖于容器类型参数的类模板。
⑦ 为了能使用包围模板的类型参数,应在迭代器类内部定义这些参数的别名。
⑧ 为了能高效地访问容器,迭代器类一般都是包围类的友元。
2. 双向链表的迭代器
#pragma once
#include <iostream>
#include <exception>
#include <initializer_list>
#include "container.h"
template <typename value_t>
class dlist : public container<value_t> {
public:
using value_type = typename container<value_t>::value_type;
using pointer = typename container<value_t>::pointer;
using reference = typename container<value_t>::reference;
using difference_type = ptrdiff_t; //两个指针的差值,这是一个标准类型
...
friend class iterator;
class iterator {
public:
using value_type = dlist::value_type;
using reference = dlist::reference;
using difference_type = dlist::difference_type;
private:
nodeptr_t p;
dlist * cp;
public:
iterator(nodeptr_t t = nullptr, dlist * c = nullptr) : p(t), cp(c) {}
bool operator!=(const iterator& iter) {
return p != iter.p;
}
iterator& operator++() {
p = p->next;
return *this;
}
reference operator*() {
return p->data;
}
auto operator->() {
return p;
}
};
auto begin() {
return iterator(head.next, this);
}
auto end() {
return iterator(&tail, this);
}
};
可见,我们设置了一个迭代器类,它是双向链表类的友元:
friend class iterator;
为了能使用包围模板的类型参数,我们定义了这些参数的别名:
public:
using value_type = dlist::value_type;
using reference = dlist::reference;
using difference_type = dlist::difference_type;
p 是我们包装在迭代器类内的原生指针,cp 用于指向容器类对象:
private:
nodeptr_t p;
dlist * cp;
迭代器应具有的操作:
bool operator!=(const iterator& iter) {
return p != iter.p;
}
iterator& operator++() {
p = p->next;
return *this;
}
reference operator*() {
return p->data;
}
auto operator->() {
return p;
}
关于 * 运算符的重载:
考虑 * 的原始语义,*p 得到的是一个左值。如果返回值类型采用 auto,我们将得到一个右值。因此,我们需要返回的是一个引用,即 auto&。为了更便于理解,我们换成 reference。
设置首迭代器和哨兵迭代器:
auto begin() {
return iterator(head.next, this);
}
auto end() {
return iterator(&tail, this);
}
- 逆向迭代器
friend class reverse_iterator;
class reverse_iterator {
public:
using value_type = dlist::value_type;
using reference = dlist::reference;
using difference_type = dlist::difference_type;
private:
nodeptr_t p;
dlist * cp;
public:
reverse_iterator(nodeptr_t t = nullptr, dlist * c = nullptr) : p(t), cp(c) {}
bool operator!=(const reverse_iterator& iter) {
return p != iter.p;
}
reverse_iterator& operator++() {
p = p->prior;
return *this;
}
reference operator*() {
return p->data;
}
};
auto rbegin() {
return reverse_iterator(tail.prior, this);
}
auto rend() {
return reverse_iterator(&head, this);
}
- 随机存取迭代器
重载 + 运算符,我们假设 d 是一个正数:
iterator operator+(difference_type d) {
iterator iter(*this);
for (difference_type i = 0; i < d; ++i)
iter.p = iter.p->next;
return iter;
}
difference_type 是我们在前面定义的别名,用于表示两个指针之间的距离。其中,ptrdiff_t 是一个标准类型。
using difference_type = ptrdiff_t;
- 利用迭代器实现元素插入
template <typename ...types>
void emplace(iterator pos, types&& ...args) try {
if (pos != iterator()) {
auto p = nodeptr_t(pos)->prior;
_push(p, new node(p->next, p, args...))
} else
throw std::out_of_range("insert position out of range");
} catch (std::out_of_range &e) {
std::cout << e.what() << std::endl;
exit(1);
}
迭代器类构造函数的默认参数均设置为 nullptr,可以直接用于判断迭代器是否访问越界:
if (pos != iterator())
nodeptr_t 是重载的类型转换运算符,用于析取迭代器类内部的原生指针:
operator nodeptr_t() {
return p;
}