简单说明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()的作用是翻转nextprev指针指向的空间。

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

由上面两个例子可知合并后不会重新分配空间。itaitb仍然指向原来的内存空间。只是改变了内存空间的指向关系。

注意: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()相同。

结尾

先写这么些吧。第一次写这么长的文章(可能在全网来看这一点也不长),花了两天时间T^T。希望能帮助到各位。如果有错误请批评指正,感激不尽!

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐