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);
}
因为链表最后一个结点的 _next 是 nullptr,所以遍历到 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。






Comments | NOTHING