课堂笔记| 第九章:容器和迭代器

本节课要点:

  • 容器
  • 迭代器

一、容器

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;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值