C++中的迭代器和指針
在前面的內容中我們簡單講過,STL主要是由三部分組成
- 容器(container),包括vector,list,set,map等
- 泛型算法(generic algorithm),用來操作這些容器,包括find(),sort(),replace()等
- 迭代器(iterator),泛型算法操作容器的工具,是連接容器和算法的粘合劑
一、迭代器(iterator)
在介紹STL之前,首先了解一下什么是迭代器。STL中的泛型算法提供了很多可作用於容器類以及數組類上的操作,這些算法與他們想要操作的元素類型無關(int,double,string等)且與容器類獨立(vector,list,array等)。很容易想到,泛型算法通過函數模板(function template)技術來達到 “與操作對象的元素類型無關” 的目的,而實現與 “容器無關” 則不直接在容器本身進行操作,而是借助一對 iterator 來標示我們要進行迭代的元素范圍。我們通過一個具體的問題來引入 iterator 的設計動機。
問題描述:
給定一個存儲整數的vector,如果vector內存在目標值value,就返回指向該值的指針;否則返回0。
首先很容易想到的一種做法是:
int* find(vector<int>& nums, const int& value){
for(int i=0; i<nums.size(); i++){
if(nums[i]==value) return &nums[i];
}
return 0;
}
接下來我們使用函數模板技術來擴充這個函數的功能,使其能夠處理不同類型的數據類型:
template <typename T>
T* find(vector<T>& nums, const T& value){
for(int i=0; i<nums.size(); i++){
if(nums[i]==value) return &nums[i];
}
return 0;
}
緊接着我們會想,函數能不能同時實現對vector和array類型的輸入進行查找,一種解決辦法是通過函數重載的技術來實現,但是如果要實現很多種類型的容器,那么便需要寫很多個重載函數。另一種更好的解決辦法是:我們便不將容器本身作為參數傳入,而是傳入需要處理的數據的開始和結束位置,這樣便對任意的輸入有了普遍性。
對於 array
數組類型的數據 int array[10]
而言,array=&arran[0]
即數組名就代表數組的開始地址,也代表數組第一個元素的地址。由於在傳遞時,無論是 array[10],array[24]
,都不會傳遞 array
的結束地址,因此需要額外傳遞一個參數 size
,或者一個結尾地址,那么程序便可以寫成:
方法一:傳遞數組的大小作為參數來標示結束為止
template <typename T>
T* find(const T* array, int size, const T& value){...}
方法二:傳遞數組的結尾指針來標示結束為止
template <typename T>
T* find(const T* begin, const T* end, const T& value){...}
上面我們已經完成了 array
類型輸入的find
函數的編寫,下面我們就來簡單看一下調用方式:
int in_array[5] = {1, 4, 5, 7, 2};
double do_array[7] = {1.5, 2.7, 3.2, 4, 7, 2, 1.7};
int* f1 = find(array, 5, 4); //采用第一種調用方式,傳入開始位置和數組大小
int* f2 = find(do_array, do_array+7, 2); //采用第二種調用方式,傳入開始位置和結束位置即[開始位置,結束位置)
那么針對 vector
類型的容器,它的存儲方式跟 array
相同,都是以一塊連續的內存存儲所有元素,因此可以采用跟 array
相同的方式來實現 find
函數。但是二者不同的是:vector 容器可以為空而 array
不能為空,因此:
vector<int> nums; find(&nums[0], &nums[0+n], value); //正確
int array[]; //無法對 array[0] 取地址
所以為了避免每次在計算 array
的首地址時,array
為空的情況,抽象出一個新的函數 begin()
,具體定義如下:
template <typename T>
inline T* begin(const vector<T>& vec){
return vec.empty() ? 0:&vec[0];
}
我們以同樣的方式封裝成 end()
函數,返回 vector
的結束地址。因此我們便有了放之四海而皆准的調用方式:
find( begin(vec), end(vec), value ); // 開始地址,結束地址,查找值
再進一步,我們可以嘗試將 find
函數應用在所有的容器類型,但是由於大部分容器(比如:list,map,set)並不是順序存儲,因此 vector
和 array
的這種指針尋址的方式並不適合其他非連續內存空間存儲的容器類型。解決這個問題的方法是,在底層指針的行為之上提供一層抽象,取代程序原本的 “指針直接操作” 方式。我們把底層指針的處理全部放在此抽象層中,將原本的指針操作根據具體的容器類型進行重載,這樣我們便可以處理標准庫所提供的的所有容器類,這便是 iterator 的創建原因。iterator 的操作方式跟指針一樣,但是 iterator 的 ++, !=, *
等運算符是根據具體的容器類型重載過得。對 list
而言,++
會按照鏈表的方式前進到下一個元素,對 vector
而言,++
會直接指向下一個內存位置。
/*************定義單鏈表的類************/
template<typename T>
class node {
public:
T value;
node *next;
node() : next(nullptr) {}
node(T val, node *p = nullptr) : value(val), next(p) {}
};
/*************封裝單鏈表***************/
template<typename T>
class my_list {
private:
node<T> *head;
node<T> *tail;
int size;
private:
//單鏈表迭代器的實現
class list_iterator {
private:
node<T> *ptr; //指向list容器中的某個元素的指針
public:
list_iterator(node<T> *p = nullptr) : ptr(p) {}
//重載++、--、*、->等基本操作
//返回引用,方便通過*it來修改對象
T &operator*() const {
return ptr->value;
}
node<T> *operator->() const {
return ptr;
}
list_iterator &operator++() {
ptr = ptr->next;
return *this;
}
list_iterator operator++(int) {
node<T> *tmp = ptr;
// this 是指向list_iterator的常量指針,因此*this就是list_iterator對象,前置++已經被重載過
++(*this);
return list_iterator(tmp);
}
bool operator==(const list_iterator &t) const {
return t.ptr == this->ptr;
}
bool operator!=(const list_iterator &t) const {
return t.ptr != this->ptr;
}
};
public:
typedef list_iterator iterator; //類型別名
my_list() {
head = nullptr;
tail = nullptr;
size = 0;
}
//從鏈表尾部插入元素
void push_back(const T &value) {
if (head == nullptr) {
head = new node<T>(value);
tail = head;
} else {
tail->next = new node<T>(value);
tail = tail->next;
}
size++;
}
//打印鏈表元素
void print(std::ostream &os = std::cout) const {
for (node<T> *ptr = head; ptr != tail->next; ptr = ptr->next)
os << ptr->value << std::endl;
}
public:
//操作迭代器的方法
//返回鏈表頭部指針
iterator begin() const {
return list_iterator(head);
}
//返回鏈表尾部指針
iterator end() const {
return list_iterator(tail->next);
}
//其它成員函數 insert/erase/emplace
};
二、容器(container):物之所置也
2.1 順序性容器
- vector 以一塊連續的內存來存放元素,對 vector 進行隨機訪問很有效率,但是由於 vector 的每個元素都被存儲在距離起始點的固定偏移位置,如果將元素插在任意位置,那么效率很低。同理,刪除任意位置的元素也缺乏效率;
- list 以雙向鏈接而非連續內存來存儲內容,因此實現 list 內部任意位置的插入和刪除操作效率很高,但是如果要對 list 進行隨機訪問,則效率很低;
- deque 與 vector 一樣都是使用連續內存來存放元素,deque 在最前端插入元素,最后端刪除元素。
2.2 關聯容器
map:被定義為一對(key-value)數值,其中的 key
通常是個字符串,扮演索引的角色,另一個數值是 value
。value
是 key
通過映射函數 f
得到的值,可以記錄 key
出現的次數等。map 對象中 key
用 first
對象來表示,value
用 second
對象來表示,即:
map<string, int>::iterator it = words.begin();
while(it != words.end()){
cout << "key:" << it->first << "\nvalue:" << it->second << endl;
}
查詢map是否存在 key 有三種方法:
/**********************方法一**********************/
string target="a";
int count = words[target]; // 查詢words中是否存在 "a"
/**********************方法二**********************/
string target="a";
map<string, int>::iterator it = words.find(target);
/**********************方法三**********************/
string target="a";
int count = words.count(target);
其中:
- 方法一:如果
words
中存在 "a",count
中就記錄了 "a" 的個數。**但是,當words
中本來就不含有 "a" 時,該方法會通過words[target]
自動添加進words
,此時words[target]=0
**,因此該方法不建議用在查詢中; - 方法二:類似於上一節中寫的
find
函數,當找到該元素時,返回指向該元素的迭代器,否則返回指向最有一個元素的后一個位置的迭代器words.end()
。所以通過判斷函數返回值是否為words.end()
便可以知道結果; - 方法三:
count
會返回某個特定項在map
內的個數。
set:set的操作方式跟map差不多,set中相當於只記錄了 key
值。
無論是 map 還是 set,在進行插入元素后會對其中的元素進行排序,因此當不需要排序時,需要定義:
unordered_map<pair<type1, type2>>my_map;
unordered_set<pair<type1, type2>>my_set;
2.3 所有容器的共通操作
==, !=
:返回true
或者false
, 判斷是否相等;empty()
:在容器為空時返回true
,否則返回false
;size()
:返回容器內的元素個數;clear()
:清空容器內的元素,但是保留容器的長度;begin()
:返回容器第一個元素的iterator
;end()
:返回容器最后一個元素的后一個位置的iterator
;insert()
:在容器的指定位置插入元素;erase()
:在容器的指定位置刪除元素;push_back()
:在容器的末端插入元素;pop_back()
:在容器的首端取出元素.......
上面列舉的都是比較常見的一部分,由於精力有限難免有錯誤和疏漏,歡迎大家在閱讀的同時對文中的不當之處進行指正、補充,不勝感激 !
三、參考內容
- 《Essential C++》中文版,侯捷譯
- https://www.cnblogs.com/wengle520/p/12492708.html