C++学习 day9

发布于 20 天前  36 次阅读


Day 9:迭代器和适配器模式

本节代码位置:

2-代码/day9-迭代器/51-字符串迭代器示例.cpp
2-代码/day9-迭代器/52-test-list-迭代器版.cpp
2-代码/day9-迭代器/53-适配器示例.cpp
2-代码/day9-迭代器/list.hpp
2-代码/day9-迭代器/list.cpp
2-代码/day9-迭代器/stack.hpp
2-代码/day9-迭代器/stack.cpp

1、迭代器模式

迭代器模式的作用:

提供一种方法,顺序访问一个聚合对象中的各个元素,
同时不暴露这个对象内部的存储结构。

简单理解:

容器负责保存数据。
迭代器负责遍历数据。
外部使用者不需要知道容器内部到底是数组、链表还是其他结构。

C++ 标准库中常见的迭代器用法:

for (auto it = 容器.begin(); it != 容器.end(); ++it)
{
    cout << *it << endl;
}

迭代器通常需要支持:

*it       访问当前元素
++it      移动到下一个元素
it != end 判断是否遍历结束

容器通常需要提供:

begin()   返回第一个元素位置的迭代器
end()     返回尾后位置的迭代器

注意:

end() 不是最后一个元素,而是最后一个元素后面的那个位置。
end() 只能用来比较,不能解引用。

2、字符串迭代器示例

51-字符串迭代器示例.cpp 中给自定义字符串 china::string 添加了迭代器。

字符串内部核心数据:

char *_data;

因为字符串本质上是一段连续字符,所以迭代器内部只需要保存一个字符指针:

class iterator
{
public:
    iterator(char *p) : p(p) {}

    char &operator*()
    {
        return *p;
    }

    iterator &operator++()
    {
        ++p;
        return *this;
    }

    bool operator!=(const iterator &rhs) const
    {
        return this->p != rhs.p;
    }

private:
    char *p;
};

begin() 返回第一个字符的位置:

iterator begin()
{
    return iterator(_data);
}

end() 返回字符串结束符 '' 的位置:

iterator end()
{
    return iterator(_data + strlen(_data));
}

使用方式:

china::string s1 = "hello world";

for (china::string::iterator it = s1.begin(); it != s1.end(); ++it)
{
    cout << *it << ' ';
}

输出效果:

h e l l o   w o r l d

重点:

字符串迭代器的本质就是 char*。
operator* 返回当前字符。
operator++ 让指针后移。
operator!= 比较两个迭代器是否指向同一位置。

3、自定义 List 迭代器

list.hpp 中的 List 是一个双向链表。

链表结点:

struct Node
{
    Node(Type data);
    Type _data;
    struct Node *_next;
    struct Node *_prev;
};

这里:

using Type = std::string;

所以当前链表保存的是字符串。

链表本身保存:

Node *_head;
Node *_tail;
int _size;

自定义链表的迭代器定义在 List 类内部:

class iterator
{
public:
    iterator();
    Type &operator*();
    iterator &operator++();
    bool operator!=(const iterator &rhs) const;

private:
    friend class List;
    Node *_p;
    iterator(Node *p);
};

为什么要把迭代器定义在 List 里面?

1. 迭代器是专门为 List 服务的。
2. 它需要知道 List 的结点类型 Node。
3. 外部使用者只需要会用 iterator,不需要知道 Node 的细节。

为什么需要 friend class List

iterator(Node *p) 是私有构造函数。
外部不能随便用 Node* 创建迭代器。
只有 List 自己能通过 begin() 和 end() 创建合法迭代器。

迭代器默认构造:

List::iterator::iterator() : _p(nullptr) {}

用结点指针构造:

List::iterator::iterator(Node *p) : _p(p) {}

解引用:

Type &List::iterator::operator*()
{
    return _p->_data;
}

前置自增:

List::iterator &List::iterator::operator++()
{
    _p = _p->_next;
    return *this;
}

不等比较:

bool List::iterator::operator!=(const iterator &rhs) const
{
    return _p != rhs._p;
}

begin() 返回头结点:

List::iterator List::begin()
{
    return iterator(_head);
}

end() 返回空指针:

List::iterator List::end()
{
    return iterator(nullptr);
}

因为链表最后一个结点的 _nextnullptr,所以遍历到 nullptr 就表示结束。

使用方式:

List list;

list.push_back("hello");
list.push_back("world");
list.push_back("iterator");
list.push_front("C++");

for (auto it = list.begin(); it != list.end(); ++it)
{
    cout << *it << endl;
}

输出顺序:

C++
hello
world
iterator

重点:

List 迭代器的本质就是 Node*。
operator* 返回当前结点的数据。
operator++ 让指针移动到下一个结点。
begin() 是 _head。
end() 是 nullptr。
空链表时 begin() == end()。

注意:

不能对 end() 解引用,因为 end() 内部是 nullptr。
链表删除结点后,指向被删除结点的迭代器会失效。

4、范围 for 的条件

范围 for 的写法:

for (auto &x : 容器)
{
    cout << x << endl;
}

想让自定义容器支持范围 for,至少要提供:

begin()
end()
迭代器的 operator*
迭代器的 operator++
迭代器的 operator!=

所以 List 提供了 begin()end()iterator 后,理论上就可以写:

for (const auto &c : list)
{
    cout << c << endl;
}

当前代码中这段是注释状态:

// for (const auto &c : list)
// {
//     cout << c << endl;
// }

如果要支持 const List 的范围 for,还需要补充 const 版本的 begin()end()


5、适配器模式

适配器模式的作用:

把一个类的接口转换成用户希望的另一个接口。

简单理解:

原来的类功能够用,但是接口名字或使用方式不符合当前需求。
这时可以再包一层,把旧接口包装成新接口。

适配器也叫包装器。

常见实现方式:

对象适配器:通过组合实现,类里面保存一个已有对象。
类适配器:通过继承实现。

本节代码使用的是对象适配器。


6、用 List 适配 Stack

栈的特点:

后进先出,LIFO。
最后放进去的数据,最先取出来。

stack.hpp 中定义了 Stack

class Stack
{
public:
    void push(Type x);
    void pop();
    Type top() const;
    bool empty() const;
    int size() const;

private:
    List _list;
};

这里 Stack 内部保存了一个 List

List _list;

也就是说:

Stack 自己不重新实现链表。
Stack 直接复用 List 的功能。
Stack 只是把 List 的接口包装成栈的接口。

接口对应关系:

Stack::push(x)   -> List::push_back(x)
Stack::pop()     -> List::pop_back()
Stack::top()     -> List::back()
Stack::empty()   -> List::empty()
Stack::size()    -> List::size()

实现:

void Stack::push(Type x)
{
    _list.push_back(x);
}

void Stack::pop()
{
    _list.pop_back();
}

Type Stack::top() const
{
    return _list.back();
}

bool Stack::empty() const
{
    return _list.empty();
}

int Stack::size() const
{
    return _list.size();
}

为了兼容课堂中 LinkStack 的叫法,还写了类型别名:

using LinkStack = Stack;

使用方式:

LinkStack s1;

s1.push("hello");
s1.push("cpp");
s1.push("world");
s1.push("!!!");

while (!s1.empty())
{
    cout << s1.top() << endl;
    s1.pop();
}

输出顺序:

!!!
world
cpp
hello

重点:

Stack 是适配器。
List 是被适配的已有类。
Stack 通过组合 List,复用链表尾插、尾删、取尾元素的能力。

7、Day9 总结

1. 迭代器用于遍历容器,同时隐藏容器内部结构。
2. begin() 返回第一个元素的位置。
3. end() 返回尾后位置,不能解引用。
4. 迭代器常用操作是 *、++、!=。
5. 字符串迭代器的本质是 char*。
6. List 迭代器的本质是 Node*。
7. 链表的 begin() 是 _head,end() 是 nullptr。
8. 空链表时 begin() == end()。
9. 自定义容器想支持范围 for,需要 begin()、end() 和基本迭代器操作。
10. 适配器模式用于把已有类的接口包装成新的接口。
11. Stack 可以通过组合 List 实现。
12. 栈的特点是后进先出。

一句话记忆:

Day9 重点:迭代器负责遍历容器,适配器负责包装接口;List 能遍历,Stack 复用 List。

"When faced with uncertainty, ask the spring breeze."