简单说明C++ STL list中的迭代器(iterator)
这篇文章主要讨论在使用list方法后迭代器的活动和一些注意点。
先说结论:
assign()
,resize()
,erase()
和remove()
方法可能会对iterator造成影响,其他方法均不改变iterator原本指向的空间。
详细见下文
0. 头文件和迭代器
如果想在C++中使用list容器,需要有头文件<list>
迭代器的类型声明是list<Type T>::iterator
,当然也可以auto
#include <iostream>
#include <list>
using namespace std;
list<int> a {1, 3, 5};
list<int> b {2, 4};//构造函数
list<int>::iterator ita = a.begin();
auto itb = b.begin();//声明一下我们要讨论的迭代器ita和itb,分别作用于链表a和链表b
这里auto
会自动判断变量类型,所以上面代码中声明迭代器的两种方法其实是等价的。
下面我们列举几个list方法,看看这个期间iterator的变化。
1.1 begin()和end()
ita = a.begin();
cout << *ita << endl; //输出1
ita = a.end();
cout << *ita << endl; //报错
a.begin()
会使ita
指向第一个元素,而a.end()
会使ita
指向最后一个元素的下一个位置。所以第二个 cout
会报错。
如果想输出最后一个元素,可以进行如下操作
ita = a.end();
ita--;
cout << *ita << endl; // 输出5
list的迭代器是支持++
和--
操作的(前后缀都支持)
但是不支持+
和-
操作,因为链表不能进行随机访问操作,只能O(n)
查询。
所以想输出第5个元素,这么做是不行的:
ita = a.begin();
ita = ita + 4; // 报错
cout << *ita << endl;
得这么整:
ita = a.begin();
for (int i = 0; i < 4; ++i)
++ita;
cout << *ita << endl;
1.2 rbegin()和rend() – 本小节为二次更新内容
这里的r
代表reverse
,意思为:反过来的。
所以a.rbegin()
指向链表最后一个元素,a.rend()
指向链表第一个元素。
注意!!!!
在begin()
和end()
这套体系中,begin()
指向第一个元素,end()
指向最后一个元素的下一个位置
,即指向NULL。
然而在rbegin()
和rend()
这套体系中,rbegin()
指向最后一个元素,并非NULL!rend()
不指向第一个元素的前一个位置,而是恰好指向第一个元素的位置。
另外,在遍历链表的一个循环中,begin() end()
体系与rbegin() rend()
体系不能混用,即以下做法是错误的:
for (auto it = a.begin(); it != rbegin(); ++it)
cout << *it << endl;
想输出最后一个元素,还有下面的一种方法。
2. front()和back()
cout << a.front() << endl; // 输出1
cout << a.back() << endl; // 输出5
这里a.front()
和a.back()
直接返回第一个元素和最后一个元素。这个例子与迭代器无关。
3. push_back()和push_front()
push_back()
的作用是在链表末尾加一个元素push_front()
的作用是在链表开头加一个元素
ita = a.begin();
a.push_front(999);
cout << *ita << endl; // 输出1
ita
原来指向存有1
的内存空间,在push_front()
后,ita
仍然指向原来的内存块,所以输出*ita
仍会输出1
。
但是如果执行以下代码:
cout << ita == a.begin() << endl; // false
会得到false
。因为此时的开头不是原来的1
了,而是999
。
for (ita = a.begin(); ita != a.end(); ++ita)
cout << *ita << endl;
// 输出结果为:
// 999
// 1
// 3
// 5
itb = b.end();
b.pushback(itb, 888);
cout << *itb << endl; // 会报错
for (itb = b.begin(); itb != b.end(); ++itb)
cout << *itb << endl;
// 输出结果为:
// 2
// 4
// 888
即使在push_back()
后itb
仍然指向一个不存有元素的空间(没变),所以那一行cout
会报错。
4. resize()
resize()
的作用是把链表长度改成指定的大小resize()
有两种使用方法,一个是扩大,还有一个是缩小
//扩大
ita = a.end();
a.resize(5, 2); // 把链表长度改为 5 ,额外多出来的部分都置为 2
// 可以省略第二个参数,那么多出来的默认置为 0
cout << *ita << endl; // 会报错
如果在resize()
之前输出*ita
,那么肯定会报错,但是为什么扩大之后也报错呢?
因为扩大后新申请的空间不是原来*ita
指向的空间,想想链表的最后一项的next
不是NULL
嘛!
缩小就更不用说了,就看缩到什么程度了。如果缩小后iterator仍然在链表长度范围内,那么没事儿;但是如果超出范围了,那就报错了。
所以resize()
后一定要检查一下。避免出现错误。
5. pop_back()和pop_front()
pop_back()
的作用是删除最后一个元素pop_front()
的作用是删除第一个元素
ita = a.begin();
a.pop_front();
cout << *ita << endl; // 报错
这个应该很明显了。但是我想输出第一个元素,我能不能这么做呢?
ita = a.begin();
a.pop_front();
ita++;
cout << *ita << endl;
运行结果:在这一步会报错。
ita++; // Assertion Failed!
因为在pop_front()
之后,第一个元素的内存被free
(或delete
)掉了。所以ita++
,也就是p=p->next
是没法实现的。所以会报错。
所以想实现pop_front()
后输出第一个元素:
方法一:可以先移动一位
ita = a.begin();
++ita;
a.pop_front();
cout << *ita << endl;
方法二:用begin()
定位
a.pop_front();
ita = a.begin();
cout<< *ita << endl;
方法三:用front()
直接输出
a.pop_front();
cout << a.front() << endl;
pop_back()
也同理啦!同样没法实现以下操作:
ita = a.end();
ita--;
a.pop_back();
ita--; // 报错
cout << *ita << endl;
6. assign()
assign()
的可以认为是resize()
的升级版,可以改变长度的同时重置所有元素。
用法一:assign(n, val)
ita = a.end();
ita--; // 指向最后一个元素
a.assign(2, 10); // 长度变短
cout << *ita << endl; // 报错
长度变短之后,原来指向的空间没了,所以报错
ita = a.end();
a.assign(5, 10); // 变长
cout << *ita << endl; // 报错
与上面几个例子同理,这也同样行不通。
ita = a.begin();
a.assign(5, 10);
cout << *ita << endl; // 输出10
扩大的过程中如果原来的内存空间存在,那么就不会新分配空间,只会改变里面的值,所以输出的不是1
而是10
。缩小同理。
用法二:assign(l.begin(), l.end())
这里两个参数是两个迭代器,不用非得从头到尾,但是要合法。
itb = b.begin();
itb++;
b.assign(a.begin(), a.end());
cout << *itb << endl; // 输出 3
可以看出这里输出的是更改后的值
itb = b.begin();
itb++;
b.assign(a.begin(), a.end());
itb++;
cout << *itb << endl; // 输出 5
变长之后itb++
也是没问题的。但是不能在assign()
之前itb++
,那样会越界报错。
这里猜测assign()
的原理是值赋值+如果扩大则开辟新空间。
7. swap()
swap()
的作用是交换两个链表
ita = a.begin();
a.swap(b);
cout << *ita << endl; // 输出 1
可以看出ita
指向的内存空间还是原来的1
,猜测swap()
做的事情是交换两个头指针。
itb = b.end();
b.swap(a);
cout << *itb << endl; // 会报错
这个问题这篇文章里提到过很多次了,不赘述了。
想输出修改后的值,那就重新定位就可以了。比如
ita = a.begin();
a.swap(b);
ita = a.begin(); // 重新定位
cout << *ita << endl; // 输出 2
8. reverse()
reverse()
的作用是翻转链表,原来的1,2,3变成3,2,1。
ita = a.begin();
a.reverse();
cout << *ita << endl; // 输出 1
猜测reverse()
的作用是翻转next
和prev
指针指向的空间。
9. merge()
merge()
的作用是合并两个链表。不会去重。
使用merge()
的前提是两个链表已经按同样的顺序排好序(以’<'的关系排好序)
不会去重的意思是,比如
链表a = { 1, 2, 3, 5, 5}
链表b = { 4, 5, 5}merge()
后的链表就是 { 1, 2, 3, 4, 5a, 5a, 5b, 5b}
其中5a代表这个5来自链表a,5b代表这个5来自链表b,说明merge()是稳定排序。
下面的例子还用{1, 3, 5}
和{2, 4}
ita = a.begin();
a.merge(b);
cout << *ita << endl; // 输出 1
itb = b.begin();
b.merge(a);
cout << *itb << endl; // 输出 2
由上面两个例子可知合并后不会重新分配空间。ita
和itb
仍然指向原来的内存空间。只是改变了内存空间的指向关系。
注意:a.merge(b)
后b
链表会变为空,所以指向b
的指针会失效。
10. insert()
a.insert(ita, val)
后,会在ita
的位置之前插入val
。
ita = a.begin();
a.insert(ita, 10);
cout << *ita << endl; // 输出 1
for (ita = a.begin(); ita != a.end(); ++ita)
cout << *ita << endl;
// 输出:
// 10
// 1
// 3
// 5
这个感觉还是比较好想的。ita
指向的内存空间不变。
11. erase()
erase(ita)
的作用是删除ita
指向的内存空间的元素。
ita = a.begin();
++ita; // 指向第二个元素
a.erase(ita);
cout << *ita << endl; // 报错
for (ita = a.begin(); ita != a.end(); ++ita)
cout << *ita << endl;
// 输出:
// 1
// 5
由此可见当erase()
后,ita
既不会指向前一个元素,也不会指向下一个元素。
同样,下面的操作也不合法。
ita = a.begin();
++ita;
a.erase(ita);
ita++;
cout << *ita << endl; //试图输出最后一个元素
当然会报错,会在倒数第二行的ita++
处报错。理由在上面也阐述过。
12. remove()
remove(val)
的作用是删除所有值为val
的结点。
如果链表a
为{1,1,2,3}
,那么执行remove(1)
后,链表a
变为{2,3}
下面例子中a
仍为{1,3,5}
ita = a.begin();
ita++;
a.remove(1);
cout << *ita << endl; // 输出 3
a.remove(3);
cout << *ita << endl; // 报错
报错理由与erase()
相同。