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()函數,因為拷貝構造函數可以根據已有容器的大小決定一次性分配多少元素空間,就不會產生冗余容量。