C++/C学习笔记(十)
——迭代器
1.迭代器
(1)迭代器的本质
循环结构有两种控制方式:标志控制和计数控制。迭代器可以把这两种标志控制的循环统一为一种控制方法:迭代器控制,每一次迭代操作中对迭代器的修改就等价于修改标志或计数器。
在STL中,容器的迭代器被作为容器元素对象或者I/O流中的对象的位置指示器,因此可以把它理解为面向对象的指针——一种泛型指针或通用指针,不依赖于元素的真实类型。
迭代器的概念如图所示:
set<int>::iterator iter;
↓ →→ ++(迭代)
{ 1,2,3,4,5,6,7,8,9,0 □ }
↑ ↑
begin() end()、
可见,容器迭代器的作用类似于数据库中的游标(cursor),它屏蔽了底层存储空间的不连续性,在上层使容器元素维持一种“逻辑连续”的假象。不可把迭代器与void*和“基类指针”这样的通用指针混淆。
指针代表真正的内存地址,即对象在内存中的存储位置;而迭代器则代表元素在容器中的相对位置。
STL把迭代器划分为5个类别(Category),这5类迭代器分别具有不同的能力,表现为支持不同的运算符,它们都是类模版,因此具有通用性。
标准迭代器
迭代器种类 | 提供的操作 | 特征说明 |
trivial迭代器 | X x; X(); *x; *x=t; X->m | 只是一个概念,用以区分所有迭代器的定义。 |
输入迭代器 Input Iterator | *i; (void)i++; ++i; *i++; 还包含trivial 迭代器的所有操作 | 提供只读操作,即可读取它所指向的元素的值,但不可改变元素的值; 如果i==j,并不意味++i==++j; |
输出迭代器 Output Iterator | X x; X(); X(x); X y(x); X y=x; *x=t; ++x; (void)x++; *x++=t; | 提供只写操作,即可改变它所指向的元素的值;但不可读取该元素的值
|
前进迭代器 Forward Iterator |
++i; i++;
| 只能向前访问下一个元素,不能反向访问前一个元素 典型:slist |
双向迭代器 (Bidirectional Iterator) | ++i; i++; i--; --i; 还包含前进迭代器中所有操作 | 它是对前进迭代器的扩充,提供双向访问 典型:list(双向链表),set/map |
随机访问迭代器 (Random Access Iterator) | i+=n; i+n或n+i; i-=n; i-n; i-j; i[n]; i[n]=t; 还包含双向迭代器中的所有操作 | 能访问前面或后面第n个元素,即可以随机访问任何一个元素 典型:vector的迭代器(它就是原始指针),deque |
(2)迭代器失效及其危险性
迭代器失效是指当前容器底层存储发生变动时,原来指向容器中某个或某些元素的迭代器由于元素的存储位置发生了改变而不再指向它们,从而成为无效的迭代器。使用无效的迭代器就像使用无效的野指针一样危险。
可能引起容器存储变动的操作:reserve()、resize()、push_back()、pop_back()、insert()、erase()、clear()等容器方法和一些泛型算法如sort()、copy()、replace()、remove()、unique(),以及集合操作(并、交、差)算法等。如下例:
/***************************************************************/
#include<iostream>
#include<vector>
using namespace std;
void main()
{
vector<int>ages; // 未预留空间
ages.push_back(2); //引起内存重分配
vector<int>::const_iterator p=ages.begin();
for(int i=0;i<10;i++)
{
ages.push_back(5); //会引起若干次内存重分配操作
}
cout<<"The first age:"<<*p<<endl; //p已经失效,危险!
}
/***************************************************************/
解决迭代器失效问题:(1)在调用上述操作后重新获取迭代器;(2)在修改容器钱为其预留足够的空闲空间可以避免存储空间重分配。
上例可改为:
/***************************************************************/
void main()
{
//...
vector<int>::const_iterator p=ages.begin();
for(int i=0;i<10;i++)
{
ages.push_back(5); //会引起若干次内存重分配操作
}
p=ages.begin(); //重新获取迭代器
cout<<"The first age:"<<*p<<endl; //OK
}
/***************************************************************/
顺序容器vector和string都可以用reserve()和resize()来预留空间或调整它们的大小:reserve()用来保留(扩充)容量,它并不改变容器的有效元素个数;resize()则调整容器大小(size,有效元素的个数),而且有时候也会增大容器的容量。
接下来要搞清楚“容量”和“容器”及“有效元素”的概念。
容量是为了减少那些使用连续空间(线性空间)存储元素的容器在增加元素时重新分配内存次数的一种机制,即当增加元素且剩余空闲空间不足时,按照一定比例多分配出一些空闲空间以备将来再增加元素时使用,以提高插入操作的性能。一个具有多余容量的std::vector<T>的典型内存映像如下图所示。
图中迭代器start和finish之间的元素就是容器的有效元素,而start和end_of_storage之间的空间就是该容器的总容量,容量是包含有效元素空间在内的。Finish和end_of_storage之间的空闲时间就是冗余容量,冗余容量不属于容器。
reserve使用详解:
reserve()原型:void reserve(size_type n);其中n就是用户请求保留的总容量的大小(在不重新分配内存情况下可容纳元素的个数)。Reserve()可按以下实现:
如果n大于容器现有的容量(即capacity()),则需要在自由内存区为整个容器重新分配一块新的更大的连续空间,其大小为n*sizeof(T),然后将容器内所有有效元素从旧位置全部拷贝到新位置(调用拷贝构造函数),最后释放旧位置的所有存储空间并调整容器对象的元素位置指示器(就是让那3个指针指向新内存区的相应位置)。也就是说,如果请求容量比原有容量大的话,结果是容器的冗余容量加大(即end_of_storage指针的相对位置发生了变化),而容器本身的有效元素不会发生任何变化,即容器的大小并没有改变。
如果n小于或等于现有容量,则什么也不做。
除了调用容器的某些方法可以改变容器的大小外,在容器外部没有任何方法可以做到这一点。因此如果想使用迭代器在冗余容量的空间上通过赋值来给容器增加元素的话,结果一定会让你失望的。例如:
/***************************************************************/
std::list<int> li;
std::vector<int> vi;
for(int c=0;c<10,c++)
li.push_back(c);
vi.reserve(li.size());//预留空间,但是并没有改变容器的大小,
//预留空间未初始化
std::copy(li.begin(),li.end(),vi.begin());//拷贝赋值
std::copy(vi.begin(),vi.end(),std::ostream_iterator<int>(std::cerr,"\t"));
/***************************************************************/
这段程序显然是错误的。虽然vi.reserve()为vector预留了内存,但是改变的只是容器的容量。同时在copy算法中对容器元素赋值也不会改变容器的大小,因此拷贝后容器的size()仍然为0,虽然list的元素已经被拷贝到了为vector预留的空间上。结果可想而知:没有输出任何东西!Vector在拷贝前后的状态变化可用下图来说明:
resize使用详解:
resize()调整容器的大小(size),有时也会扩大容器的容量。不管容器当前包含多少个有效元素,也不管容器的冗余容量是多少,它都将容器的有效元素个数调整为用户指定的个数。resize()原型如下:
void resize(size_type n, const T&c=T());
其中n就是最后要保持的元素个数,如果需要新增元素的话,c则是新增元素的默认初始值。resize()实现策略:
如果n大于容器当前的大小(即size()),则在容器的末尾插入(追加)n-size()个初始值为c的元素,如果不指定初始值,则用元素类型的默认构造函数来初始化每一个新元素。
如果n小于容器当前的大小,则从容器的末尾删除size()-n个元素,但不释放元素本身的内存空间,因此容量不变。
否则,什么也不做。
则上面的例子可以改为:
/***************************************************************/
std::list<int> li;
std::vector<int> vi;
for(int c=0;c<10,c++)
li.push_back(c);
vi.resize(li.size());//调整容器大小
std::copy(li.begin(),li.end(),vi.begin());//拷贝赋值
std::copy(vi.begin(),vi.end(),std::ostream_iterator<int>(std::cerr,"\t"));
/***************************************************************/
这样就能输出正确的结果了,如下图所示:
显然,使用reserve()和resize()都不能缩减容器的容量。一个解决的办法就是使用容器的拷贝构造函数和swap()函数,因为拷贝构造函数可以根据已有容器的大小决定一次性分配多少元素空间,就不会产生冗余容量。