《C++ Primer 5th》笔记(9 / 19):顺序容器
文章目錄
- 順序容器概述
- 確定使用哪種順序容器
- 容器庫概覽
- 迭代器
- 迭代器范圍
- 使用左閉合范圍蘊含的編程假定
- 容器類型成員
- begin和end成員
- 容器定義和初始化
- 將一個容器初始化為另一個容器的拷貝
- 列表初始化
- 與順序容器大小相關(guān)的構(gòu)造函數(shù)
- 標準庫array具有固定大小
- 賦值和swap
- 使用assign(僅順序容器)
- 使用swap
- 容器大小操作
- 關(guān)系運算符
- 容器的關(guān)系運算符使用元素的關(guān)系運算符完成比較
- 順序容器操作
- 向順序容器添加元素
- 使用push_back
- 關(guān)鍵概念:容器元素是拷貝
- 使用push_front
- 在容器中的特定位置添加元素
- 插入范圍內(nèi)元素
- 使用insert的返回值
- 使用emplace 操作
- 訪問元素
- 訪問成員函數(shù)返回的是引用
- 下標操作和安全的隨機訪問
- 刪除元素
- pop_front和pop_back成員函數(shù)
- 從容器內(nèi)部刪除一個元素
- 刪除多個元素
- 特殊的forward_list操作
- 改變?nèi)萜鞔笮?/li>
- 容器操作可能使迭代器失效
- 建議:管理迭代器
- 編寫改變?nèi)萜鞯难h(huán)程序
- 不要保存end返回的迭代器
- vector對象是如何增長
- 管理容量的成員函數(shù)
- capacity和size
- 額外的string操作
- 構(gòu)造string的其他方法
- substr操作
- 改變string的其他方法
- append和replace函數(shù)
- 改變string 的多種重載函數(shù)
- string搜索操作
- 指定在哪里開始搜索
- 逆向搜索
- compare函數(shù)
- 數(shù)值轉(zhuǎn)換
- 容器適配器
- 概述
- 定義一個適配器
- 棧適配器
- 隊列適配器
- 小結(jié)
一個容器就是一些特定類型對象的集合。順序容器(sequential container)為程序員提供了控制元素存儲和訪問順序的能力。這種順序不依賴于元素的值,而是與元素加入容器時的位置相對應(yīng)。與之相對的,將在第11章介紹的有序和無序關(guān)聯(lián)容器,則根據(jù)關(guān)鍵字的值來存儲元素。
標準庫還提供了三種容器適配器,分別為容器操作定義了不同的接口,來與容器類型適配。我們將在本章末尾介紹適配器。
順序容器概述
下表列出了標準庫中的順序容器,所有順序容器都提供了快速順序訪問元素的能力。但是,這些容器在以下方面都有不同的性能折中:
- 向容器添加或從容器中刪除元素的代價
- 非順序訪問容器中元素的代價
| vector | 可變大小數(shù)組。支持快速隨機訪問。在尾部之外的位置插入或刪除元素可能很慢 |
| deque | 雙端隊列。支持快速隨機訪問。在頭尾位置插入/刪除速度很快 |
| list | 雙向鏈表。只支持雙向順序訪問。在 list中任何位置進行插入/刪除操作速度都很快 |
| forward_list | 單向鏈表。只支持單向順序訪問。在鏈表任何位置進行插入/刪除操作速度都很快 |
| array | 固定大小數(shù)組。支持快速隨機訪問。不能添加或刪除元素 |
| string | 與vector相似的容器,但專門用于保存字符。隨機訪問快。在尾部插入/刪除速度快 |
除了固定大小的array外,其他容器都提供高效、靈活的內(nèi)存管理。我們可以添加和刪除元素,擴張和收縮容器的大小。容器保存元素的策略對容器操作的效率有著固有的,有時是重大的影響。在某些情況下,存儲策略還會影響特定容器是否支持特定操作。
例如,string和vector將元素保存在連續(xù)的內(nèi)存空間中。由于元素是連續(xù)存儲的,由元素的下標來計算其地址是非常快速的。但是,在這兩種容器的中間位置添加或刪除元素就會非常耗時:在一次插入或刪除操作后,需要移動插入/刪除位置之后的所有元素,來保持連續(xù)存儲。而且,添加一個元素有時可能還需要分配額外的存儲空間。在這種情況下,每個元素都必須移動到新的存儲空間中。(增刪慢,讀快)
list和forward_list兩個容器的設(shè)計目的是令容器任何位置的添加和刪除操作都很快速。作為代價,這兩個容器不支持元素的隨機訪問:為了訪問一個元素,我們只能遍歷整個容器。而且,與vector、deque和 array相比,這兩個容器的額外內(nèi)存開銷也很大(增刪快,需要額外的內(nèi)存,存指針)。
deque是一個更為復(fù)雜的數(shù)據(jù)結(jié)構(gòu)。與string和 ector類似,deque支持快速的隨機訪問。與string和vector一樣,在deque的中間位置添加或刪除元素的代價(可能)很高。但是,在 deque 的兩端添加或刪除元素都是很快的,與 list或forward_list添加刪除元素的速度相當。
forward_list和array是新C++標準增加的類型。與內(nèi)置數(shù)組相比,array是一種更安全、更容易使用的數(shù)組類型。與內(nèi)置數(shù)組類似,array對象的大小是固定的。因此,array不支持添加和刪除元素以及改變?nèi)萜鞔笮〉牟僮鳌orward_list的設(shè)計目標是達到與最好的手寫的單向鏈表數(shù)據(jù)結(jié)構(gòu)相當?shù)男阅堋R虼?#xff0c;forward_list沒有size操作,因為保存或計算其大小就會比手寫鏈表多出額外的開銷。對其他容器而言,size保證是一個快速的常量時間的操作。
Note:新標準庫的容器比舊版本快得多,原因我們將在第13章解釋。新標準庫容器的性能幾乎肯定與最精心優(yōu)化過的同類數(shù)據(jù)結(jié)構(gòu)一樣好(通常會更好)。現(xiàn)代C++程序應(yīng)該使用標準庫容器,而不是更原始的數(shù)據(jù)結(jié)構(gòu),如內(nèi)置數(shù)組。
確定使用哪種順序容器
Tip:通常,使用vector是最好的選擇,除非你有很好的理由選擇其他容器。
以下是一些選擇容器的基本原則:
- 除非你有很好的理由選擇其他容器,否則應(yīng)使用vector。
- 如果你的程序有很多小的元素,且空間的額外開銷很重要,則不要使用list或forward_list。
- 如果程序要求隨機訪問元素,應(yīng)使用vector或deque。
- 如果程序要求在容器的中間插入或刪除元素,應(yīng)使用list或forward_list。如果程序需要在頭尾位置插入或刪除元素,但不會在中間位置進行插入或刪除操作,則使用deque。(鏈表)
- 如果程序只有在讀取輸入時才需要在容器中間位置插入元素,隨后需要隨機訪問元素,則
- 首先,確定是否真的需要在容器中間位置添加元素。當處理輸入數(shù)據(jù)時,通常可以很容易地向vector追加數(shù)據(jù),然后再調(diào)用標準庫的sort函數(shù)(第10章內(nèi)容)來重排容器中的元素,從而避免在中間位置添加元素。
- 如果必須在中間位置插入元素,考慮在輸入階段使用list,一旦輸入完成,將list中的內(nèi)容拷貝到一個vector中。
如果程序既需要隨機訪問元素,又需要在容器中間位置插入元素,那該怎么辦?答案取決于在list或forward_list 中訪問元素與vector或deque中插入/刪除元素的相對性能。一般來說,應(yīng)用中占主導(dǎo)地位的操作(執(zhí)行的訪問操作更多還是插入/刪除更多)決定了容器類型的選擇。在此情況下,對兩種容器分別測試應(yīng)用的性能可能就是必要的了。
Best Praetices:如果你不確定應(yīng)該使用哪種容器,那么可以在程序中只使用vector和list公共的操作;使用迭代器,不使用下標操作,避免隨機訪問。這樣,在必要時選擇使用vector或list都很方便。
容器庫概覽
容器類型上的操作形成了一種層次:
- 某些操作是所有容器類型都提供的(參見下表)。
- 另外一些操作僅針對順序容器(本章內(nèi)容)、關(guān)聯(lián)容器(第11章)或無序容器(第11章)。
- 還有一些操作只適用于一小部分容器。
| 類型別名 | |
| iterator | 此容器類型的迭代器類型 |
| const_iterator | 可以讀取元素,但不能修改元素的迭代器類型 |
| size_type | 無符號整數(shù)類型,足夠保存此種容器類型最大可能容器的大小 |
| difference_type | 帶符號整數(shù)類型,足夠保存兩個迭代器之間的距離 |
| value_type | 元素類型 |
| reference | 元素的左值類型;與value_type&含義相同 |
| const_reference | 元素的const左值類型(即,const value_type&) |
| 構(gòu)造函數(shù) | |
| C c; | 默認構(gòu)造函數(shù),構(gòu)造空容器(array) |
| C c1(c2); | 構(gòu)造c2的拷貝c1 |
| C c(b,e); | 構(gòu)造c,將迭代器b和e指定的范圍內(nèi)的元素拷貝到c(array不支持) |
| C c{a, b, c…}; | 列表初始化c |
| 賦值與swap | |
| cl = c2 | 將c1中的元素替換為c2中元素 |
| c1 = {a, b, c…} | 將c1中的元素替換為列表中元素(不適用于array) |
| a.swap(b) | 交換a和b的元素 |
| swap(a, b) | 與a.swap(b)等價 |
| 大小 | |
| c.size() | c中元素的數(shù)目(不支持forward_list) |
| c.max_size() | c可保存的最大元素數(shù)目 |
| c.empty() | 若c中存儲了元素,返回false,否則返回true |
| 添加/刪除元素(不適用于array) 注:在不同容器中,這些操作的接口都不同 | |
| c.insert(args) | 將args中的元素拷貝進c |
| c.emplace(inits) | 使用inits構(gòu)造c中的一個元素 |
| c.erase(args) | 刪除args指定的元素 |
| c.clear() | 刪除c中的所有元素,返回void |
| 關(guān)系運算符 | |
| ==,!= | 所有容器都支持相等(不等)運算符 |
| <,<=,>,>= | 關(guān)系運算符(無序關(guān)聯(lián)容器不支持) |
| 獲取迭代器 | |
| c.begin(),c.end() | 返回指向c的首元素和尾元素之后位置的迭代器 |
| c.cbegin(),c.cend() | 返回const_iterator |
| 反向容器的額外成員(不支持forward_list) | |
| reverse_iterator | 按逆序?qū)ぶ吩氐牡?/td> |
| const_reverse_iterator | 不能修改元素的逆序迭代器 |
| c.rbegin(),c.rend() | 返回指向c的尾元素和首元素之前位置的迭代器 |
| c.crbegin(),c.crend() | 返回const_reverse_iterator |
在本節(jié)中,我們將介紹對所有容器都適用的操作。本章剩余部分將聚焦于僅適用于順序容器的操作。關(guān)聯(lián)容器特有的操作將在第11章介紹。
一般來說,每個容器都定義在一個頭文件中,文件名與類型名相同。即,deque定義在頭文件deque 中,list定義在頭文件list中,以此類推。容器均定義為模板類。例如對vector,我們必須提供額外信息來生成特定的容器類型。對大多數(shù),但不是所有容器,我們還需要額外提供元素類型信息:
list<Sales_data> //保存sales_data對象的list deque<double> //保存double的deque對容器可以保存的元素類型的限制
順序容器幾乎可以保存任意類型的元素。特別是,我們可以定義一個容器,其元素的類型是另一個容器。這種容器的定義與任何其他容器類型完全一樣:在尖括號中指定元素類型(此種情況下,是另一種容器類型):
vector<vector<string>> lines; //vector的vector此處lines是一個vector,其元素類型是string的vector。
Note:較舊的編譯器可能需要在兩個尖括號之間鍵入空格,例如,vector<vector<string> >。
雖然我們可以在容器中保存幾乎任何類型,但某些容器操作對元素類型有其自己的特殊要求。我們可以為不支持特定操作需求的類型定義容器,但這種情況下就只能使用那些沒有特殊要求的容器操作了。
例如,順序容器構(gòu)造函數(shù)的一個版本接受容器大小參數(shù),它使用了元素類型的默認構(gòu)造函數(shù)。但某些類沒有默認構(gòu)造函數(shù)。我們可以定義一個保存這種類型對象的容器,但我們在構(gòu)造這種容器時不能只傳遞給它一個元素數(shù)目參數(shù):
// assume noDefault is a type without a default constructor vector<noDefault> v1(10, init); // ok: element initializer supplied vector<noDefault> v2(10); // error: must supply an element initializer迭代器
與容器一樣,迭代器有著公共的接口:如果一個迭代器提供某個操作,那么所有提供相同操作的迭代器對這個操作的實現(xiàn)方式都是相同的。
例如,標準容器類型上的所有迭代器都允許我們訪問容器中的元素,而所有迭代器都是通過解引用運算符來實現(xiàn)這個操作的。類似的,標準庫容器的所有迭代器都定義了遞增運算符,從當前元素移動到下一個元素。
下表關(guān)于容器迭代器支持的所有操作,其中有一個例外不符合公共接口特點——forward_list迭代器不支持遞減運算符(–)。(forward_list是單向鏈表)
| *iter | 返回迭代器iter所指元素的引用 |
| iter->mem | 解引用iter并獲取該元素的名為mem的成員,等價于(*iter).mem |
| ++iter | 令iter指示容器中的下一個元素 |
| –iter | 令iter指示容器中的上一個元素 |
| iter1 == iter2 | 判斷兩個迭代器是否相等, 如果兩個迭代器指示的是同一個元素 或者它們是同一個容器的尾后迭代器,則相等;反之,不相等 |
| iter1 != iter2 | 判斷兩個迭代器是否不相等 |
下表關(guān)于迭代器支持的算術(shù)運算,這些運算只能應(yīng)用于string、vector、deque和 array的迭代器。我們不能將它們用于其他任何容器類型的迭代器。
| iter + n | 迭代器加上一個整數(shù)值仍得一個迭代器, 迭代器指示的新位置與原來相比向前移動了若干個元素。 結(jié)果迭代器或者指示容器內(nèi)的一個元素,或者指示容器尾元素的下一位置 |
| iter - n | 迭代器減去一個整數(shù)值仍得一個迭代器, 迭代器指示的新位置與原來相比向后移動了若干個元素。 結(jié)果迭代器或者指示容器內(nèi)的一個元素,或者指示容器尾元素的下一位置 |
| iter1 += n | 迭代器加法的復(fù)合賦值語句,將iter1加n的結(jié)果賦給iter1 |
| iter1 -= n | 迭代器減法的復(fù)合賦值語句,將iter1減n的結(jié)果賦給iter1 |
| iter1 - iter2 | 兩個迭代器相減的結(jié)果是它們之間的距離,也就是說, 將運算符右側(cè)的迭代器向前移動差值個元素后將得到左側(cè)的迭代器。 參與運算的兩個迭代器必須指向的是同一個容器中的元素或者尾元素的下一位置 |
| >、>=、<、<= | 迭代器的關(guān)系運算符,如果某迭代器指向的容器位置在另一個迭代器所指位置之前, 則說前者小于后者。參與運算的兩個迭代器必須指向的是同一個容器中的元素或者尾元素的下一位置 |
迭代器范圍
Note:迭代器范圍的概念是標準庫的基礎(chǔ)。
一個迭代器范圍(iterator range)由一對迭代器表示,兩個迭代器分別指向同一個容器中的元素或者是尾元素之后的位置(one past the last element)。這兩個迭代器通常被稱為begin和end,或者是first和last(可能有些誤導(dǎo)),它們標記了容器中元素的一個范圍。
雖然第二個迭代器常常被稱為last,但這種叫法有些誤導(dǎo),因為第二個迭代器從來都不會指向范圍中的最后一個元素,而是指向尾元素之后的位置。迭代器范圍中的元素包含first所表示的元素以及從first開始直至last(但不包含last)之間的所有元素。
這種元素范圍被稱為左閉合區(qū)間(left-inclusive interval),其標準數(shù)學(xué)描述為[begin,end)。
表示范圍自begin開始,于end之前結(jié)束。迭代器begin和end必須指向相同的容器。end可以與begin指向相同的位置,但不能指向begin之前的位置。
對構(gòu)成范圍的迭代器的要求
如果同時滿足如下條件,兩個迭代器begin和end構(gòu)成一個迭代器范圍:
- 它們指向同一個容器中的元素,或者是容器最后一個元素之后的位置,
- 我們可以通過反復(fù)遞增begin來到達end。換句話說,end不在begin之前。
WARNING:編譯器不會強制這些要求。確保程序符合這些約定是程序員的責任。
使用左閉合范圍蘊含的編程假定
標準庫使用左閉合范圍是因為這種范圍有三種方便的性質(zhì)。假定 begin和end構(gòu)成一個合法的迭代器范圍,則
-
如果begin與end相等,則范圍為空。
-
如果begin與end不等,則范圍至少包含一個元素,且 begin指向該范圍中的第一個元素。
-
我們可以對begin遞增若干次,使得begin==end
這些性質(zhì)意味著我們可以像下面的代碼一樣用一個循環(huán)來處理一個元素范圍,而這是安全的:
while (begin != end){*begin = val;//正確:范圍非空,因此begin指向一個元素++begin;//移動迭代器,獲取下一個元素 }容器類型成員
每個容器都定義了多個類型,如在容器庫概覽的表格所示。在第3章,已經(jīng)使用過其中三種:
除了已經(jīng)使用過的迭代器類型,大多數(shù)容器還提供反向迭代器。簡單地說,反向迭代器就是一種反向遍歷容器的迭代器,與正向迭代器相比,各種操作的含義也都發(fā)生了顛倒。例如,對一個反向迭代器執(zhí)行++操作,會得到上一個元素。我們將在第10章介紹更多關(guān)于反向迭代器的內(nèi)容。
剩下的就是類型別名了,通過類型別名,我們可以在不了解容器中元素類型的情況下使用它。如果需要元素類型,可以使用容器的value_type。如果需要元素類型的一個引用,可以使用reference或const_reference。這些元素相關(guān)的類型別名在泛型編程中非常有用,我們將在16章中介紹相關(guān)內(nèi)容。
為了使用這些類型,我們必須顯式使用其類名:
// iter是通過list<string>定義的一個迭代器類型 list<string>::iterator iter;// count是通過vector<int>定義的一個difference_type類型 vector<int>::difference_type count;//帶符號整數(shù)類型,足夠保存兩個迭代器之間的距離這些聲明語句使用了作用域運算符來說明我們希望使用list<string>類的iterator成員及vector<int>類定義的difference_type。
begin和end成員
begin和end操作生成指向容器中第一個元素和尾元素之后位置的迭代器。這兩個迭代器最常見的用途是形成一個包含容器中所有元素的迭代器范圍。
如在容器庫概覽的表格所示,begin和end有多個版本:帶r的版本返回反向迭代器(見第10章);以c開頭的版本則返回const迭代器:
list<string> a = {"Milton", "Shakespeare", "Austen"}; auto it1 = a.begin(); // list<string>::iterator auto it2 = a.rbegin(); // list<string>::reverse_iterator auto it3 = a.cbegin(); // list<string>::const_iterator auto it4 = a.crbegin();// list<string>::const_reverse_iterator不以c開頭的函數(shù)都是被重載過的。
也就是說,實際上有兩個名為begin的成員。一個是const成員,返回容器的const_iterator類型。另一個是非常量成員,返回容器的iterator類型。rbegin、end和rend的情況類似。
當我們對一個非常量對象調(diào)用這些成員時,得到的是返回 iterator 的版本。只有在對一個const對象調(diào)用這些函數(shù)時,才會得到一個const版本。與const指針和引用類似,可以將一個普通的iterator轉(zhuǎn)換為對應(yīng)的const_iterator,但反之不行。
以c開頭的版本是C++新標準引入的,用以支持auto與begin和end函數(shù)結(jié)合使用。過去,沒有其他選擇,只能顯式聲明希望使用哪種類型的迭代器:
// type is explicitly specified list<string>::iterator it5 = a.begin(); list<string>::const_iterator it6 = a.begin(); // iterator or const_iterator depending on a's type of a auto it7 = a.begin(); // const_iterator only if a is const auto it8 = a.cbegin(); // it8 is const_iterator當auto與begin或end結(jié)合使用時,獲得的迭代器類型依賴于容器類型,與我們想要如何使用迭代器毫不相干。但以c開頭的版本還是可以獲得const_iterator的,而不管容器的類型是什么。
Best Practies:當不需要寫訪問時,應(yīng)使用 cbegin和cend。
容器定義和初始化
每個容器類型都定義了一個默認構(gòu)造函數(shù)。除array之外,其他容器的默認構(gòu)造函數(shù)都會創(chuàng)建一個指定類型的空容器,且都可以接受指定容器大小和元素初始值的參數(shù)。
| C c; | 默認構(gòu)造函數(shù)。如果c是一個array,則c中元素按默認方式初始化;否則c為空 |
| C c1(c2) | c1初始化為c2的拷貝。c1和c2必須是相同類型(即,它們必須是相同的容器類型,且保存的是相同的元素類型;對于array類型,兩者還必須具有相同大小) |
| C c1=c2 | 同上條 |
| C c{a,b,c…} | c初始化為初始化列表中元素的拷貝。列表中元素的類型必須與c的元素類型相容。對于array類型,列表中元素數(shù)目必須等于或小于array的大小,任何遺漏的元素都進行值初始化(參見第3章) |
| C c={a,b,c…} | 同上條 |
| C c(b, e) | c初始化為迭代器b和e指定范圍中的元素的拷貝。范圍中元素的類型必須與c的元素類型相容(array不適用) |
| 只有順序容器(不包括array)的構(gòu)造函數(shù)才能接受大小參數(shù) | |
| C seq(n) | seq包含n個元素,這些元素進行了值初始化;此構(gòu)造函數(shù)是explicit的。(string不適用) |
| C seq(n,t) | seq包含n個初始化為值t的元素 |
將一個容器初始化為另一個容器的拷貝
將一個新容器創(chuàng)建為另一個容器的拷貝的方法有兩種:可以直接拷貝整個容器,或者
(array除外)拷貝由一個迭代器對指定的元素范圍。
為了創(chuàng)建一個容器為另一個容器的拷貝,兩個容器的類型及其元素類型必須匹配。不過,當傳遞迭代器參數(shù)來拷貝一個范圍時,就不要求容器類型是相同的了。而且,新容器和原容器中的元素類型也可以不同,只要能將要拷貝的元素轉(zhuǎn)換為要初始化的容器的元素類型即可。
// each container has three elements, initialized from the given initializers list<string> authors = {"Milton", "Shakespeare", "Austen"}; vector<const char*> articles = {"a", "an", "the"}; list<string> list2(authors); // ok: types match deque<string> authList(authors); // error: container types don't match vector<string> words(articles); // error: element types must match // ok: converts const char* elements to string forward_list<string> words(articles.begin(), articles.end());Note:當將一個容器初始化為另一個容器的拷貝時,兩個容器的容器類型和元素類型都必須相同。
接受兩個迭代器參數(shù)的構(gòu)造函數(shù)用這兩個迭代器表示我們想要拷貝的一個元素范圍。與以往一樣,兩個迭代器分別標記想要拷貝的第一個元素和尾元素之后的位置。新容器的大小與范圍中元素的數(shù)目相同。新容器中的每個元素都用范圍中對應(yīng)元素的值進行初始化。
由于兩個迭代器表示一個范圍,因此可以使用這種構(gòu)造函數(shù)來拷貝一個容器中的子序列。例如,我們可以編寫如下代碼:
//拷貝元素,直到(但不包括)it指向的元素 deque<string> authList(authors.begin(), it);//假定迭代器it表示authors中的一個元素列表初始化
在新標準中,我們可以對一個容器進行列表初始化
//每個容器有三個元素,用給定的初始化器進行初始化 list<string> authors = {"Milton","Shakespeare","Austen"}; vector<const char*> articles = { "a", "an" ,"the" } ;當這樣做時,我們就顯式地指定了容器中每個元素的值。對于除array之外的容器類型,初始化列表還隱含地指定了容器的大小:容器將包含與初始值一樣多的元素。
與順序容器大小相關(guān)的構(gòu)造函數(shù)
除了與關(guān)聯(lián)容器相同的構(gòu)造函數(shù)外,順序容器(array除外)還提供另一個構(gòu)造函數(shù),它接受一個容器大小和一個(可選的)元素初始值。如果我們不提供元素初始值,則標準庫會創(chuàng)建一個值初始化器:
vector<int> ivec(10, -1); // ten int elements, each initialized to -1 list<string> svec(10, "hi!"); // ten strings; each element is "hi!" forward_list<int> ivec(10); // ten elements, each initialized to 0 deque<string> svec(10); // ten elements, each an empty string如果元素類型是內(nèi)置類型或者是具有默認構(gòu)造函數(shù)的類類型,可以只為構(gòu)造函數(shù)提供一個容器大小參數(shù)。如果元素類型沒有默認構(gòu)造函數(shù),除了大小參數(shù)外,還必須指定一個顯式的元素初始值。
Note:只有順序容器的構(gòu)造函數(shù)才接受大小參數(shù),關(guān)聯(lián)容器并不支持。
標準庫array具有固定大小
與內(nèi)置數(shù)組一樣,標準庫array的大小也是類型的一部分。當定義一個array時,除了指定元素類型,還要指定容器大小:
array<int, 42> //類型為:保存42個int的數(shù)組 array<string, 10> //類型為:保存10個string的數(shù)組為了使用array類型,我們必須同時指定元素類型和大小:
array<int, 10>::size_type i;//數(shù)組類型包括元素類型和大小 array<int>::size_type j;//錯誤:array<int>不是一個類型由于大小是array類型的一部分,array不支持普通的容器構(gòu)造函數(shù)。這些構(gòu)造函數(shù)都會確定容器的大小,要么隱式地,要么顯式地。而允許用戶向一個 array構(gòu)造函數(shù)傳遞大小參數(shù),最好情況下也是多余的,而且容易出錯。
array大小固定的特性也影響了它所定義的構(gòu)造函數(shù)的行為。與其他容器不同,一個默認構(gòu)造的array是非空的:它包含了與其大小一樣多的元素。這些元素都被默認初始化,就像一個內(nèi)置數(shù)組中的元素那樣。
如果我們對array進行列表初始化,初始值的數(shù)目必須等于或小于array的大小。如果初始值數(shù)目小于array的大小,則它們被用來初始化array中靠前的元素,所有剩余元素都會進行值初始化。在這兩種情況下,如果元素類型是一個類類型,那么該類必須有一個默認構(gòu)造函數(shù),以使值初始化能夠進行:
array<int, 10> ial;//10個默認初始化的int array<int, 10> ia2 = {0,1,2,3,4,5,6,7,8,9};//列表初始化 array<int,10> ia3 ={42}; //ia3 [0]為42,剩余元素為0值得注意的是,雖然我們不能對內(nèi)置數(shù)組類型進行拷貝或?qū)ο筚x值操作,但array并無此限制:
int digs[10] = {0,1,2,3,4,5,6,7,8,9}; int cpy[10] = digs;//錯誤:內(nèi)置數(shù)組不支持拷貝或賦值 array<int,10> digits = {0,1,2,3,4,5,6,7,8,9 }; array<int,10> copy = digits; //正確:只要數(shù)組類型匹配即合法與其他容器一樣,array也要求初始值的類型必須與要創(chuàng)建的容器類型相同。此外,array還要求元素類型和大小也都一樣,因為大小是array類型的一部分。
賦值和swap
| c1=c2 | 將c1中的元素替換為c2中元素的拷貝。c1和c2必須具有相同的類型 |
| c={a,b,c…} | 將c1中元素替換為初始化列表中元素的拷貝(array不適用) |
| swap(c1, c2) | 交換c1和c2中的元素。c1和c2必須具有相同的類型。swap通常比從c2向c1拷貝元素快得多 |
| cl.swap(c2) | 同上條 |
| assign操作不適用于關(guān)聯(lián)容器和array | |
| seq.assign(b, e) | 將seq中的元素替換為迭代器b和e所表示的范圍中的元素。迭代器b和e不能指向seq中的元素 |
| seq.assign(il) | 將seq中的元素替換為初始化列表il中的元素 |
| seq.assign(n, t) | 將seq中的元素替換為n個值為t的元素 |
WARNING:賦值相關(guān)運算會導(dǎo)致指向左邊容器內(nèi)部的迭代器、引用和指針失效。而swap操作將容器內(nèi)容交換不會導(dǎo)致指向容器的迭代器、引用和指針失效(容器類型為array和string的情況除外)。
上表中列出的與賦值相關(guān)的運算符可用于所有容器。賦值運算符將其左邊容器中的全部元素替換為右邊容器中元素的拷貝:
c1 = c2;//將c1的內(nèi)容替換為c2中元素的拷貝 c1 = {a,b,c};//賦值后,c1大小為3第一個賦值運算后,左邊容器將與右邊容器相等。如果兩個容器原來大小不同,賦值運算后兩者的大小都與右邊容器的原大小相同。第二個賦值運算后,c1的size變?yōu)?,即花括號列表中值的數(shù)目。
與內(nèi)置數(shù)組不同,標準庫array類型允許賦值。賦值號左右兩邊的運算對象必須具有相同的類型:
array<int,10> a1 ={0,1,2,3,4,5,6,7,8,9}; array<int,10> a2 ={0}; //所有元素值均為0 a1 = a2;//替換a1中的元素 a2 = {0};//錯誤:不能將一個花括號列表賦予數(shù)組由于右邊運算對象的大小可能與左邊運算對象的大小不同,因此 array類型不支持assign,也不允許用花括號包圍的值列表進行賦值。
使用assign(僅順序容器)
賦值運算符要求左邊和右邊的運算對象具有相同的類型。它將右邊運算對象中所有元素拷貝到左邊運算對象中。順序容器(array除外)還定義了一個名為assign的成員,允許我們從一個不同但相容的類型賦值,或者從容器的一個子序列賦值。assign操作用參數(shù)所指定的元素(的拷貝)替換左邊容器中的所有元素。例如,我們可以用assgin實現(xiàn)將一個vector中的一段char*值賦予一個list中的string:
list<string> names; vector<const char*> oldstyle; names = oldstyle; // error: container types don't match // ok: can convert from const char*to string names.assign(oldstyle.cbegin(), oldstyle.cend());這段代碼中對assign的調(diào)用將names中的元素替換為迭代器指定的范圍中的元素的拷貝。assign的參數(shù)決定了容器中將有多少個元素以及它們的值都是什么。
WARNING:由于其舊元素被替換,因此傳遞給assign的迭代器不能指向調(diào)用assign的容器。
assign的第二個版本接受一個整型值和一個元素值。它用指定數(shù)目且具有相同給定值的元素替換容器中原有的元素:
// equivalent to slist1.clear(); // followed by slist1.insert(slist1.begin(), 10, "Hiya!"); list<string> slist1(1); // one element, which is the empty string slist1.assign(10, "Hiya!"); // ten elements; each one is Hiya !使用swap
swap操作交換兩個相同類型容器的內(nèi)容。調(diào)用swap之后,兩個容器中的元素將會交換:
vector<string> svec1(10); // 10個元素的vector vector<string> svec2(24);// 24個元素的vector swap (svec1, svec2) ;調(diào)用swap后,svecl將包含24個string元素,svec2將包含10個string.除array外,交換兩個容器內(nèi)容的操作保證會很快——-元素本身并未交換,swap只是交換了兩個容器的內(nèi)部數(shù)據(jù)結(jié)構(gòu)。
Note:除array外,swap不對任何元素進行拷貝、刪除或插入操作,因此可以保證在常數(shù)時間內(nèi)完成。
元素不會被移動的事實意味著,除string外,指向容器的迭代器、引用和指針在swap操作之后都不會失效。它們?nèi)灾赶騭wap操作之前所指向的那些元素。但是,在swap之后,這些元素已經(jīng)屬于不同的容器了。例如,假定iter在swap之前指向svec1[3]的string,那么在swap之后它指向svec2[3]的元素。與其他容器不同,對一個string調(diào)用swap會導(dǎo)致迭代器、引用和指針失效。
與其他容器不同,swap兩個array會真正交換它們的元素。因此,交換兩個array所需的時間與array中元素的數(shù)目成正比。
因此,對于array,在swap操作之后,指針、引用和迭代器所綁定的元素保持不變,但元素值已經(jīng)與另一個array 中對應(yīng)元素的值進行了交換。
在新標準庫中,容器既提供成員函數(shù)版本的swap,也提供非成員版本的swap。而早期標準庫版本只提供成員函數(shù)版本的swap。非成員版本的swap在泛型編程中是非常重要的。統(tǒng)一使用非成員版本的swap是一個好習(xí)慣。
容器大小操作
除了一個例外,每個容器類型都有三個與大小相關(guān)的操作。
forward_list支持max_size和empty,但不支持size,原因我們將在下一節(jié)解釋。
關(guān)系運算符
每個容器類型都支持相等運算符(==和!=);除了無序關(guān)聯(lián)容器外的所有容器都支持關(guān)系運算符(>、>=、<、<=)。關(guān)系運算符左右兩邊的運算對象必須是相同類型的容器,且必須保存相同類型的元素。即,我們只能將一個vector<int>與另一個vector<int>進行比較,而不能將一個vector<int>與一個list<int>或一個vector<double>進行比較。
比較兩個容器實際上是進行元素的逐對比較。這些運算符的工作方式與string的關(guān)系運算類似:
-
如果兩個容器具有相同大小且所有元素都兩兩對應(yīng)相等,則這兩個容器相等;否則兩個容器不等。
-
如果兩個容器大小不同,但較小容器中每個元素都等于較大容器中的對應(yīng)元素,則較小容器小于較大容器。
-
如果兩個容器之間都不是另一個容器的前綴子序列,則它們的比較結(jié)果取決于第一個不相等的元素的比較結(jié)果。
下面的例子展示了這些關(guān)系運算符是如何工作的:
vector<int> v1 = { 1, 3, 5, 7, 9, 12 }; vector<int> v2 = { 1, 3, 9 }; vector<int> v3 = { 1, 3, 5, 7 }; vector<int> v4 = { 1, 3, 5, 7, 9, 12 }; v1 < v2 // true; v1 and v2 differ at element [2]: v1[2] is less than v2[2] v1 < v3 // false; all elements are equal, but v3 has fewer of them; v1 == v4 // true; each element is equal and v1 and v4 have the same size() v1 == v2 // false; v2 has fewer elements than v1容器的關(guān)系運算符使用元素的關(guān)系運算符完成比較
Note:只有當其元素類型也定義了相應(yīng)的比較運算符時,我們才可以使用關(guān)系運算符來比較兩個容器。
容器的相等運算符實際上是使用元素的==運算符實現(xiàn)比較的,而其他關(guān)系運算符是使用元素的<運算符。如果元素類型不支持所需運算符,那么保存這種元素的容器就不能使用相應(yīng)的關(guān)系運算。
例如,我們在第7章中定義的Sales_data類型并未定義==和<運算。因此,就不能比較兩個保存Sales_data元素的容器:
vector<Sales_data> storeA, storeB; if (storeA < storeB) // error: Sales_data has no less-than operator順序容器操作
順序容器和關(guān)聯(lián)容器的不同之處在于兩者組織元素的方式。這些不同之處直接關(guān)系到了元素如何存儲、訪問、添加以及刪除。上一節(jié)介紹了所有容器都支持的操作。本章剩余部分將介紹順序容器所特有的操作。
向順序容器添加元素
除array外,所有標準庫容器都提供靈活的內(nèi)存管理。在運行時可以動態(tài)添加或刪除元素來改變?nèi)萜鞔笮 O卤砹谐隽讼蝽樞蛉萜?#xff08;非array)添加元素的操作。
注意:
- 這些操作會改變?nèi)萜鞯拇笮? array不支持這些操作。
- forward_list有自己專有版本的insert 和emplace;
- forward_list不支持push_back 和emplace_back。
- vector和string不支持push_front和emplace_front。
- 向一個vector、string或deque插入元素會使所有指向容器的迭代器、引用和指針失效。
向順序容器添加元素的操作
| c.push_back(t) | 在c的尾部創(chuàng)建一個值為t或由args 創(chuàng)建的元素。返回void |
| c.emplace_back(args) | 同上一條 |
| c.push_front(it) | 在c的頭部創(chuàng)建一個值為t或由args 創(chuàng)建的元素。返回void |
| c.emplace_front(args) | 同上一條 |
| c.insert(p,t) | 在迭代器p指向的元素之前創(chuàng)建一個值為t或由args創(chuàng)建的元素。返回指向新添加的元素的迭代器 |
| c.emplace(p, args) | 同上一條 |
| c.insert(p, n, t) | 在迭代器p指向的元素之前插入n個值為t的元素。返回指向新添加的第一個元素的迭代器:若n為0,則返回p |
| c.insert(p, b, e) | 將迭代器b和e指定的范圍內(nèi)的元素插入到迭代器p指向的元素之前。b和e不能指向c中的元素。返回指向新添加的第一個元素的迭代器;若范圍為空,則返回 p |
| c.insert(p, il) | il是一個花括號包圍的元素值列表。將這些給定值插入到迭代器p指向的元素之前。返回指向新添加的第一個元素的迭代器;若列表為空,則返回p |
當我們使用這些操作時,必須記得不同容器使用不同的策略來分配元素空間,而這些策略直接影響性能。在一個vector或string的尾部之外的任何位置,或是一個deque的首尾之外的任何位置添加元素,都需要移動元素。而且,向一個vector或string添加元素可能引起整個對象存儲空間的重新分配。重新分配一個對象的存儲空間需要分配新的內(nèi)存,并將元素從舊的空間移動到新的空間中。
(MyNote:盲猜vector和deque底層是用數(shù)組實現(xiàn)。)
使用push_back
在第3章,看到push_back將一個元素追加到一個vector的尾部。除 array和forward_list之外,每個順序容器(包括string類型)都支持push_back。
例如,下面的循環(huán)每次讀取一個string 到 word中,然后追加到容器尾部:
//從標準輸入讀取數(shù)據(jù),將每個單詞放到容器末尾 string word; while (cin >> word)container.push_back (word);對push_back的調(diào)用在container尾部創(chuàng)建了一個新的元素,將container的size增大了1。該元素的值為word的一個拷貝。container的類型可以是list、vector或deque。
由于string是一個字符容器,我們也可以用push_back在string末尾添加字符:
關(guān)鍵概念:容器元素是拷貝
當我們用一個對象來初始化容器時,或?qū)⒁粋€對象插入到容器中時,實際上放入到容器中的是對象值的一個拷貝,而不是對象本身。就像我們將一個對象傳遞給非引用參數(shù)一樣,容器中的元素與提供值的對象之間沒有任何關(guān)聯(lián)。隨后對容器中元素的任何改變都不會影響到原始對象,反之亦然。
使用push_front
除了push_back、list、forward_list和deque容器還支持名為push_front的類似操作。此操作將元素插入到容器頭部:
list<int> ilist; //將元素添加到ilist開頭 for (size_t ix = 0; ix != 4; ++ix)ilist.push_front (ix) ;此循環(huán)將元素0、1、2、3添加到ilist頭部。每個元素都插入到list的新的開始位置(new beginning)。即,當我們插入1時,它會被放置在0之前,2被放置在1之前,依此類推。因此,在循環(huán)中以這種方式將元素添加到容器中,最終會形成逆序。在循環(huán)執(zhí)行完畢后,ilist保存序列3、2、1、0。
注意,deque像vector一樣提供了隨機訪問元素的能力,但它提供了vector所
不支持的push_front。deque 保證在容器首尾進行插入和刪除元素的操作都只花費常數(shù)時間。與vector一樣,在deque首尾之外的位置插入元素會很耗時。
在容器中的特定位置添加元素
push_back和push_front操作提供了一種方便地在順序容器尾部或頭部插入單個元素的方法。insert成員提供了更一般的添加功能,它允許我們在容器中任意位置插入0個或多個元素。vector、deque、list和string都支持insert成員。forward_list提供了特殊版本的insert成員,隨后介紹。
每個insert函數(shù)都接受一個迭代器作為其第一個參數(shù)。迭代器指出了在容器中什么位置放置新元素。它可以指向容器中任何位置,包括容器尾部之后的下一個位置。由于迭代器可能指向容器尾部之后不存在的元素的位置,而且在容器開始位置插入元素是很有用的功能,所以insert函數(shù)將元素插入到迭代器所指定的位置之前。
例如,下面的語句
slist.insert(iter,"Hello!");//將"Hello! "添加到iter之前的位置將一個值為"Hello"的string插入到iter指向的元素之前的位置。
雖然某些容器不支持 push_front操作,但它們對于insert操作并無類似的限制(插入開始位置)。因此我們可以將元素插入到容器的開始位置,而不必擔心容器是否支持push_front:
vector<string> svec; list<string> slist; //等價于調(diào)用slist.push_front("Hello! "); slist.insert(slist.begin(), "Hello! ");//vector不支持push_front,但我們可以插入到begin()之前 //警告:插入到vector末尾之外的任何位置都可能很慢 svec.insert(svec.begin() , "Hello ! " );WARNING:將元素插入到vector、deque和string中的任何位置都是合法的。然而,這樣做可能很耗時。
插入范圍內(nèi)元素
除了第一個迭代器參數(shù)之外,insert函數(shù)還可以接受更多的參數(shù),這與容器構(gòu)造函數(shù)類似。其中一個版本接受一個元素數(shù)目和一個值,它將指定數(shù)量的元素添加到指定位置之前,這些元素都按給定值初始化:
svec.insert(svec.end(), 10, "Anna" );這行代碼將10個元素插入到svec的末尾,并將所有元素都初始化為string “Anna”。
接受一對迭代器或一個初始化列表的 insert版本將給定范圍中的元素插入到指定位置之前:
vector<string> v = {"quasi", "simba", "frollo", "scar"}; //將v的最后兩個元素添加到slist的開始位置 slist.insert(slist.begin(), v.end() - 2, v.end()); slist.insert(slist.end() ,{ "these", "words","will","go", "at", "the", "end"});//運行時錯誤:迭代器表示要拷貝的范圍,不能指向與目的位置相同的容器 slist.insert(slist.begin(), slist.begin(), slist.end());如果我們傳遞給insert一對迭代器,它們不能指向添加元素的目標容器。
在新標準下,接受元素個數(shù)或范圍的insert版本返回指向第一個新加入元素的迭代器。(在舊版本的標準庫中,這些操作返回void。)如果范圍為空,不插入任何元素,insert操作會將第一個參數(shù)返回。
使用insert的返回值
通過使用insert 的返回值,可以在容器中一個特定位置反復(fù)插入元素:
list<string> lst; auto iter = lst.begin();while (cin >> word)//insert返回的迭代器恰好指向新元素。iter = lst.insert(iter, word); //等價于調(diào)用push_frontNote:理解這個循環(huán)是如何工作的非常重要,特別是理解這個循環(huán)為什么等價于調(diào)用push_front尤為重要。
使用emplace 操作
新標準引入了三個新成員:
這些操作構(gòu)造而不是拷貝元素。
這些操作分別對應(yīng)
允許我們將元素放置在容器頭部、一個指定位置之前或容器尾部。
當調(diào)用push或insert成員函數(shù)時,我們將元素類型的對象傳遞給它們,這些對象被拷貝到容器中。而當我們調(diào)用一個emplace成員函數(shù)時,則是將參數(shù)傳遞給元素類型的構(gòu)造函數(shù)。emplace 成員使用這些參數(shù)在容器管理的內(nèi)存空間中直接構(gòu)造元素。例如,假定c保存Sales_data(參見第7章)元素:
//在c的末尾構(gòu)造一個Sales_data對象 //使用三個參數(shù)的sales_data構(gòu)造函數(shù) c.emplace_back("978-0590353403", 25, 15.99);//錯誤:沒有接受三個參數(shù)的push_back版本 c.push_back("978-0590353403", 25, 15.99);//正確:創(chuàng)建一個臨時的sales_data對象傳遞給push_back c.push_back(Sales_data("978-0590353403",25,15.99));其中對emplace_back的調(diào)用和第二個push_back調(diào)用都會創(chuàng)建新的Sales_data對象。在調(diào)用emplace_back 時,會在容器管理的內(nèi)存空間中直接創(chuàng)建對象。而調(diào)用push_back則會創(chuàng)建一個局部臨時對象,并將其壓入容器中。
emplace函數(shù)的參數(shù)根據(jù)元素類型而變化,參數(shù)必須與元素類型的構(gòu)造函數(shù)相匹配:
//iter指向c中一個元素,其中保存了sales_data元素 c.emplace_back() ; //使用sales_data的默認構(gòu)造函數(shù) c.emplace(iter,"999-999999999");//使用sales_data (string)//使用sales_data的接受一個工SBN、一個count和一個price的構(gòu)造函數(shù) c.emplace_front("978-0590353403",25,15.99);Note:emplace函數(shù)在容器中直接構(gòu)造元素。傳遞給emplace函數(shù)的參數(shù)必須與元素類型的構(gòu)造函數(shù)相匹配。
訪問元素
下表列出了我們可以用來在順序容器中訪問元素的操作。如果容器中沒有元素,訪問操作的結(jié)果是未定義的。
注意:
- at和下標操作只適用于string、vector、 deque和array。
- back不適用于forward_list。
- 對一個空容器調(diào)用 front和back,就像使用一個越界的下標一樣,是一種嚴重的程序設(shè)計錯誤。
| c.back() | 返回c中尾元素的引用。若c為空,函數(shù)行為未定義 |
| c.front() | 返回c中首元素的引用。若c為空,函數(shù)行為未定義 |
| c[n] | 返回c中下標為n的元素的引用,n是一個無符號整數(shù)。若n>=c.size(),則函數(shù)行為未定義 |
| c.at(n) | 返回下標為n的元素的引用。如果下標越界,則拋出一out_of_range異常 |
包括array在內(nèi)的每個順序容器都有一個front成員函數(shù),而除forward_list之外的所有順序容器都有一個back 成員函數(shù)。這兩個操作分別返回首元素和尾元素的引用:
//在解引用一個迭代器或調(diào)用front或 back之前檢查是否有元素 if(!c.empty()){// val 和val2是c中第一個元素值的拷貝auto val = *c.begin(), val2 = c.front();// val3和val4是c中最后一個元素值的拷貝auto last = c.end();auto val3 = *(--last); //不能遞減forward_list選代器auto val4 = c.back(); // forward_list不支持 }此程序用兩種不同方式來獲取c中的首元素和尾元素的引用。直接的方法是調(diào)用front和 back。而間接的方法是通過解引用begin返回的迭代器來獲得首元素的引用,以及通過遞減然后解引用end返回的迭代器來獲得尾元素的引用。
這個程序有兩點值得注意:
訪問成員函數(shù)返回的是引用
在容器中訪問元素的成員函數(shù)(即,front、back、下標和at)返回的都是引用。如果容器是一個const對象,則返回值是const 的引用。如果容器不是const的,則返回值是普通引用,我們可以用來改變元素的值:
if (!c.empty()){c.front () = 42;//將42賦予c中的第一個元素auto &v = c.back ();//獲得指向最后一個元素的引用v= 1024;//改變c中的元素auto v2 = c.back () ;// v2不是一個引用,它是c.back ()的一個拷貝v2 = 0;//未改變c中的元素 }與往常一樣,如果我們使用auto變量來保存這些函數(shù)的返回值,并且希望使用此變量來改變元素的值,必須記得將變量定義為引用類型。
下標操作和安全的隨機訪問
提供快速隨機訪問的容器(string、vector、deque和 array)也都提供下標運算符。就像我們已經(jīng)看到的那樣,下標運算符接受一個下標參數(shù),返回容器中該位置的元素的引用。給定下標必須“在范圍內(nèi)”(即,大于等于0,且小于容器的大小)。
保證下標有效是程序員的責任,下標運算符并不檢查下標是否在合法范圍內(nèi)。使用越界的下標是一種嚴重的程序設(shè)計錯誤,而且編譯器并不檢查這種錯誤。
如果我們希望確保下標是合法的,可以使用at成員函數(shù)。at成員函數(shù)類似下標運算符,但如果下標越界,at會拋出一個out_of_range異常:
vector<string> svec;//空vector cout << svec[0];//運行時錯誤:svec中沒有元素! cout << svec.at(0);//拋出一個out_of_range異常刪除元素
與添加元素的多種方式類似,(非 array)容器也有多種刪除元素的方式。下表列出了這些成員函數(shù)。
注意:
- 這些操作會改變?nèi)萜鞯拇笮?#xff0c;所以不適用于array。
- forward_list有特殊版本的erase,隨后介紹。
- forward_list不支持pop_back; vector和string不支持pop_front。
- 刪除deque中除首尾位置之外的任何元素都會使所有迭代器、引用和指針失效。指向vector或string中刪除點之后位置的迭代器、引用和指針I(yè)NG都會失效。
- 刪除元素的成員函數(shù)并不檢查其參數(shù)。在刪除元素之前,程序員必須確保它(們)是存在的。
| c.pop_back() | 刪除c中尾元素。若c為空,則函數(shù)行為未定義。函數(shù)返回void |
| c.pop_front() | 刪除c中首元素。若c為空,則函數(shù)行為未定義。函數(shù)返回void |
| c.erase§ | 刪除迭代器p所指定的元素,返回一個指向被刪元素之后元素的迭代器,若p指向尾元素,則返回尾后(off-the-end)迭代器。若p是尾后迭代器,則函數(shù)行為未定義 |
| c.erase(b,e) | 刪除deque中除首尾位置之外的任何元素都會使所有迭代器、引用和指針失效。指向vector或string中刪除點之后位置的迭代器、引用和指針都會失效。 |
| c.clear() | 刪除元素的成員函數(shù)并不檢查其參數(shù)。在刪除元素之前,程序員必須確保它(們)是存在的。 |
pop_front和pop_back成員函數(shù)
pop_front和pop_back成員函數(shù)分別刪除首元素和尾元素。與vector和string不支持 push_front一樣,這些類型也不支持pop_front。類似的,forward_list不支持pop_back。與元素訪問成員函數(shù)類似,不能對一個空容器執(zhí)行彈出操作。
這些操作返回void。如果你需要彈出的元素的值,就必須在執(zhí)行彈出操作之前保存它:
while (!ilist.empty()) {process (ilist.front()) ; // 對ilist的首元素進行一些處理ilist.pop_front();//完成處理后刪除首元素 }從容器內(nèi)部刪除一個元素
成員函數(shù)erase從容器中指定位置刪除元素。我們可以刪除由一個迭代器指定的單個元素,也可以刪除由一對迭代器指定的范圍內(nèi)的所有元素。兩種形式的erase 都返回指向刪除的(最后一個)元素之后位置的迭代器。即,若j是i之后的元素,那么erase (i)將返回指向j的迭代器。
例如,下面的循環(huán)刪除一個list 中的所有奇數(shù)元素:
list<int> lst = {0,1,2,3,4,5,6,7,8,9}; auto it = lst.begin() ; while (it != lst.end() )if(*it % 2) //若元素為奇數(shù)it = lst.erase (it); //刪除此元素else++it;每個循環(huán)步中,首先檢查當前元素是否是奇數(shù)。如果是,就刪除該元素,并將it設(shè)置為我們所刪除的元素之后的元素。如果*it為偶數(shù),我們將it遞增,從而在下一步循環(huán)檢查下一個元素。
刪除多個元素
接受一對迭代器的erase版本允許我們刪除一個范圍內(nèi)的元素:
//刪除兩個迭代器表示的范圍內(nèi)的元素 //返回指向最后一個被刪元素之后位置的迭代器 eleml = slist.erase(elem1,elem2); //調(diào)用后,eleml == elem2迭代器elem1指向我們要刪除的第一個元素,elem2指向我們要刪除的最后一個元素之后的位置。
為了刪除一個容器中的所有元素,我們既可以調(diào)用clear,也可以用begin和end獲得的迭代器作為參數(shù)調(diào)用erase:
slist.clear(); //刪除容器中所有元素 slist.erase(slist.begin(), slist.end()) ; //等價調(diào)用特殊的forward_list操作
為了理解forward_list為什么有特殊版本的添加和刪除操作,考慮當我們從一個單向鏈表中刪除一個元素時會發(fā)生什么。如下圖所示,刪除一個元素會改變序列中的鏈接。在此情況下,刪除elem3會改變elem2,elem2原來指向elem3,但刪除elem3后,elem2指向了elem4。
當添加或刪除一個元素時,刪除或添加的元素之前的那個元素的后繼會發(fā)生改變。為了添加或刪除一個元素,我們需要訪問其前驅(qū),以便改變前驅(qū)的鏈接。但是,forward_list 是單向鏈表。在一個單向鏈表中,沒有簡單的方法來獲取一個元素的前驅(qū)。出于這個原因,在一個forward_list中添加或刪除元素的操作是通過改變給定元素之后的元素來完成的。這樣,我們總是可以訪問到被添加或刪除操作所影響的元素。
(MyNote:forward_list 是數(shù)據(jù)結(jié)構(gòu)種的單向鏈表。)
由于這些操作與其他容器上的操作的實現(xiàn)方式不同,forward_list并未定義insert、emplace和erase,而是定義了名為insert_after、emplace_after和erase_after的操作。例如,在我們的例子中,為了刪除elem3,應(yīng)該用指向elem2的迭代器調(diào)用erase_after。為了支持這些操作,forward_list也定義了before_begin,它返回一個首前( off-the-beginning)迭代器。這個迭代器允許我們在鏈表首元素之前并不存在的元素“之后”添加或刪除元素(亦即在鏈表首元素之前添加刪除元素)。
在forward list中插入或刪除元素的操作
| lst.before_begin() | 返回指向鏈表首元素之前不存在的元素的迭代器。 此迭代器不能解引用。cbefore_begin()返回一個const_iterator |
| lst.cbefore_begin() | 同上一條 |
| lst.insert_after(p, t) | 在迭代器p之后的位置插入元素。t是一個對象,n是數(shù)量, b和e是表示范圍的一對迭代器(b和e不能指向lst內(nèi)), il是一個花括號列表。返回一個指向最后一個插入元素的迭代器。 如果范圍為空,則返回p。若p為尾后迭代器,則函數(shù)行為未定義 |
| lst.insert_after(p, n, t) | 同上一條 |
| lst.insert_after(p, b, e) | 同上一條 |
| lst.insert_after(p, il) | 同上一條 |
| emplace_after(p, args) | 使用args在p指定的位置之后創(chuàng)建一個元素。返回一個指向這個新元素的迭代器。 若p為尾后迭代器,則函數(shù)行為未定義 |
| lst.erase_after§ | 刪除p指向的位置之后的元素,或刪除從b之后直到(但不包含)e之間的元素。 返回一個指向被刪元素之后元素的迭代器,若不存在這樣的元素,則返回尾后迭代器。 如果p 指向lst的尾元素或者是一個尾后迭代器,則函數(shù)行為未定義 |
| lst.erase_after(b, e) | 同上一條 |
當在forward_list 中添加或刪除元素時,我們必須關(guān)注兩個迭代器:
例如,從list中刪除奇數(shù)元素的循環(huán)程序,將其改為從forward_list中刪除元素:
forward_list<int> flst = {0,1,2,3,4,5,6,7,8,9}; auto prev = flst.before_begin(); // denotes element "off the start" of flst auto curr = flst.begin(); // denotes the first element in flst while (curr != flst.end()) { // while there are still elements to processif (*curr % 2) // if the element is oddcurr = flst.erase_after(prev); // erase it and move currelse {prev = curr; // move the iterators to denote the next++curr; // element and one before the nextelement} }改變?nèi)萜鞔笮?/h3>
如下表所描述,我們可以用resize來增大或縮小容器,與往常一樣,array不支持resize。如果當前大小大于所要求的大小,容器后部的元素會被刪除;如果當前大小小于新大小,會將新元素添加到容器后部:
list<int> ilist (10,42);// 10個int:每個的值都是42 ilist.resize(15);//將5個值為0的元素添加到ilist的末尾 ilist.resize(25,-1);//將10個值為-1的元素添加到ilist的末尾 ilist.resize(5);//從ilist末尾刪除20個元素resize操作接受一個可選的元素值參數(shù),用來初始化添加到容器中的元素。如果調(diào)用者未提供此參數(shù),新元素進行值初始化。如果容器保存的是類類型元素,且resize向容器添加新元素,則我們必須提供初始值,或者元素類型必須提供一個默認構(gòu)造函數(shù)。
注意:
- resize不使用于array。
- 如果resize縮小容器,則指向被刪除元素的迭代器、引用和指針都會失效;對vector、string或deque進行resize可能導(dǎo)致迭代器、指針和引用失效。
順序容器大小操作
| c.resize(n) | 調(diào)整c的大小為n個元素。若n<c.size(),則多出的元素被丟棄。若必須添加新元素,對新元素進行值初始化 |
| c.resize(n,t) | 調(diào)整c的大小為n個元素。任何新添加的元素都初始化為值t |
容器操作可能使迭代器失效
向容器中添加元素和從容器中刪除元素的操作可能會使指向容器元素的指針、引用或迭代器失效。一個失效的指針、引用或迭代器將不再表示任何元素。使用失效的指針、引用或迭代器是一種嚴重的程序設(shè)計錯誤,很可能引起與使用未初始化指針一樣的問題
在向容器添加元素后:
- 如果容器是vector或string,且存儲空間被重新分配,則指向容器的迭代器、指針和引用都會失效。如果存儲空間未重新分配,指向插入位置之前的元素的迭代器、指針和引用仍有效,但指向插入位置之后元素的迭代器、指針和引用將會失效。(MyNote:底層是用連續(xù)空間的數(shù)組實現(xiàn)。)
- 對于 deque,插入到除首尾位置之外的任何位置都會導(dǎo)致迭代器、指針和引用失效。如果在首尾位置添加元素,迭代器會失效,但指向存在的元素的引用和指針不會失效。
- 對于list和forward_list,指向容器的迭代器(包括尾后迭代器和首前迭代器)、指針和引用仍有效。
當我們從一個容器中刪除元素后,指向被刪除元素的迭代器、指針和引用會失效,這應(yīng)該不會令人驚訝。畢竟,這些元素都已經(jīng)被銷毀了。
當我們刪除一個元素后:
-
對于list和forward_list,指向容器其他位置的迭代器(包括尾后迭代器和首前迭代器)、引用和指針仍有效。
-
對于deque,如果在首尾之外的任何位置刪除元素,那么指向被刪除元素外其他元素的迭代器、引用或指針也會失效。如果是刪除 deque的尾元素,則尾后迭代器也會失效,但其他迭代器、引用和指針不受影響;如果是刪除首元素,這些也不會受影響。
-
對于vector和string,指向被刪元素之前元素的迭代器、引用和指針仍有效。注意:當我們刪除元素時,尾后迭代器總是會失效。
WARNING:使用失效的迭代器、指針或引用是嚴重的運行時錯誤。
建議:管理迭代器
當你使用迭代器(或指向容器元素的引用或指針)時,最小化要求迭代器必須保持有效的程序片段是一個好的方法。
由于向迭代器添加元素和從迭代器刪除元素的代碼可能會使迭代器失效,因此必須保證每次改變?nèi)萜鞯牟僮髦蠖颊_地重新定位迭代器。這個建議對vector、string和deque尤為重要。
編寫改變?nèi)萜鞯难h(huán)程序
添加/刪除vector、string 或deque元素的循環(huán)程序必須考慮迭代器、引用和指針可能失效的問題。程序必須保證每個循環(huán)步中都更新迭代器、引用或指針。如果循環(huán)中調(diào)用的是insert 或erase,那么更新迭代器很容易。這些操作都返回迭代器,我們可以用來更新:
// silly loop to remove even-valued elements and insert a duplicate of odd-valued elements//這silly并不意味error, vector<int> vi = {0,1,2,3,4,5,6,7,8,9}; auto iter = vi.begin(); // call begin, not cbegin because we're changing vi while (iter != vi.end()) {if (*iter % 2) {//奇數(shù)iter = vi.insert(iter, *iter); // duplicate the current elementiter += 2; // advance past this element and the one inserted before it} else {//偶數(shù)iter = vi.erase(iter); // remove even elements// don't advance the iterator; iter denotes the element after the one we erased} }此程序刪除vector中的偶數(shù)值元素,并復(fù)制每個奇數(shù)值元素。我們在調(diào)用insert和erase后都更新迭代器,因為兩者都會使迭代器失效。
在調(diào)用erase后,不必遞增迭代器,因為 erase返回的迭代器已經(jīng)指向序列中下一個元素。調(diào)用insert后,需要遞增迭代器兩次。記住,insert在給定位置之前插入新元素,然后返回指向新插入元素的迭代器。因此,在調(diào)用insert后,iter指向新插入元素,位于我們正在處理的元素之前。我們將迭代器遞增兩次,恰好越過了新添加的元素和正在處理的元素,指向下一個未處理的元素。
不要保存end返回的迭代器
當我們添加/刪除vector或string 的元素后,或在 deque 中首元素之外任何位置添加/刪除元素后,原來end返回的迭代器總是會失效。因此,添加或刪除元素的循環(huán)程序必須反復(fù)調(diào)用end,而不能在循環(huán)之前保存end返回的迭代器,一直當作容器末尾使用。通常C++標準庫的實現(xiàn)中end()操作都很快,部分就是因為這個原因。
例如,考慮這樣一個循環(huán),它處理容器中的每個元素,在其后添加一個新元素。我們希望循環(huán)能跳過新添加的元素,只處理原有元素。在每步循環(huán)之后,我們將定位迭代器,使其指向下一個原有元素。如果我們試圖“優(yōu)化”這個循環(huán),在循環(huán)之前保存end()返回的迭代器,一直用作容器末尾,就會導(dǎo)致一場災(zāi)難:
//災(zāi)難:此循環(huán)的行為是未定義的 auto begin = v.begin (),end = v.end(); //保存尾迭代器的值是一個壞主意 while (begin != end) {//做一些處理//插入新值,對begin重新賦值,否則的話它就會失效++begin; //向前移動begin,因為我們想在此元素之后插入元素begin = v.insert(begin, 42); //插入新值++begin; //向前移動begin跳過我們剛剛加入的元素 }此代碼的行為是未定義的。在很多標準庫實現(xiàn)上,此代碼會導(dǎo)致無限循環(huán)。問題在于我們將end操作返回的迭代器保存在一個名為end 的局部變量中。在循環(huán)體中,我們向容器中添加了一個元素,這個操作使保存在end中的迭代器失效了。這個迭代器不再指向v中任何元素,或是v中尾元素之后的位置。
Tip:如果在一個循環(huán)中插入/刪除 deque、string或 vector 中的元素,不要緩存end返回的迭代器。
必須在每次插入操作后重新調(diào)用end(),而不能在循環(huán)開始前保存它返回的迭代器:
// safer: recalculate end on each trip whenever the loop adds/erases elements while (begin != v.end()) {// do some processing++begin; // advance begin because we want to insert after this elementbegin = v.insert(begin, 42); // insert the new value++begin; // advance begin past the element we just added }vector對象是如何增長
為了支持快速隨機訪問,vector將元素連續(xù)存儲,也就是說每個元素緊挨著前一個元素存儲。通常情況下,我們不必關(guān)心一個標準庫類型是如何實現(xiàn)的,而只需關(guān)心它如何使用。然而,對于vector和 string,其部分實現(xiàn)滲透到了接口中。
(MyNote:C++的vector與Java的ArrayList類似。)
假定容器中元素是連續(xù)存儲的,且容器的大小是可變的,考慮向vector或string中添加元素會發(fā)生什么:如果沒有空間容納新元素,容器不可能簡單地將它添加到內(nèi)存中其他位置——因為元素必須連續(xù)存儲。容器必須分配新的內(nèi)存空間來保存已有元素和新元素,將已有元素從舊位置移動到新空間中,然后添加新元素,釋放舊存儲空間。(MyNote:擴容)如果我們每添加一個新元素,vector就執(zhí)行一次這樣的內(nèi)存分配和釋放操作,性能會慢到不可接受。
為了避免這種代價,標準庫實現(xiàn)者采用了可以減少容器空間重新分配次數(shù)的策略。當不得不獲取新的內(nèi)存空間時,vector和string 的實現(xiàn)通常會分配比新的空間需求更大的內(nèi)存空間。容器預(yù)留這些空間作為備用,可用來保存更多的新元素。這樣,就不需要每次添加新元素都重新分配容器的內(nèi)存空間了。
這種分配策略比每次添加新元素時都重新分配容器內(nèi)存空間的策略要高效得多。其實際性能也表現(xiàn)得足夠好。雖然vector在每次重新分配內(nèi)存空間時都要移動所有元素,但使用此策略后,其擴張操作通常比list和deque還要快。
管理容量的成員函數(shù)
如下表所示,vector和string類型提供了一些成員函數(shù),允許我們與它的實現(xiàn)中內(nèi)存分配部分互動。
- capacity操作告訴我們?nèi)萜髟诓粩U張內(nèi)存空間的情況下可以容納多少個元素。
- reserve操作允許我們通知容器它應(yīng)該準備保存多少個元素。
reserve
英 [r??z??v] 美 [r??z??rv]
v. 預(yù)訂,預(yù)約(座位、席位、房間等);保留;貯備;擁有,保持,保留(某種權(quán)利)
n. 儲備(量);儲藏(量);(動植物)保護區(qū);自然保護區(qū);內(nèi)向;寡言少語;矜持
reverse
英 [r??v??s] 美 [r??v??rs]
v. 顛倒;徹底轉(zhuǎn)變;使完全相反;撤銷,廢除(決定、法律等);使反轉(zhuǎn);使次序顛倒
n. 相反的情況(或事物);后面;背面;反面;倒擋
adj. 相反的;反面的;反向的;背面的;后面的
(MyNote:reserve和reverse傻傻分不清楚。)
注意:
- shrink_to_fit只適用于vector、 string和deque。
- capacity和reserve只適用于vector和string。
容器大小管理操作
| c.shrink_to_fit() | 請將capacity()減少為與size()相同大小 |
| c.capacity() | 不重新分配內(nèi)存空間的話,c可以保存多少元素 |
| c.reserve(n) | 分配至少能容納n個元素的內(nèi)存空間 |
Note:reserve并不改變?nèi)萜髦性氐臄?shù)量,它僅影響 vector預(yù)先分配多大的內(nèi)存空間。
只有當需要的內(nèi)存空間超過當前容量時,reserve調(diào)用才會改變vector的容量。如果需求大小大于(>)當前容量,reserve至少分配與需求一樣大的內(nèi)存空間(可能更大)。
如果需求大小小于或等于當前容量,reserve什么也不做。特別是,當需求大小小于(<)當前容量時,容器不會退回內(nèi)存空間。因此,在調(diào)用reserve之后,capacity將會大于或等于傳遞給reserve的參數(shù)。
這樣,調(diào)用reserve永遠也不會減少容器占用的內(nèi)存空間。類似的,resize成員函數(shù)只改變?nèi)萜髦性氐臄?shù)目,而不是容器的容量。我們同樣不能使用resize來減少容器預(yù)留的內(nèi)存空間。(MyNote:注意reserve與最大容量有關(guān),resize與實際容量有關(guān)。)
在新標準庫中,我們可以調(diào)用shrink_to_fit來要求deque、vector或string退回不需要的內(nèi)存空間。此函數(shù)指出我們不再需要任何多余的內(nèi)存空間。但是,具體的實現(xiàn)可以選擇忽略此請求。也就是說,調(diào)用shrink_to_fit也并不保證一定退回內(nèi)存空間。(MyNote:shrink_to_fit會盡力了。)
capacity和size
理解capacity和size的區(qū)別非常重要。容器的size是指它已經(jīng)保存的元素的數(shù)目;而capacity則是在不分配新的內(nèi)存空間的前提下它最多可以保存多少元素。(MyNote:注意capacity與最大容量有關(guān),size與實際容量有關(guān)。)
下面的代碼展示了size和capacity之間的相互作用:
vector<int> ivec; // size should be zero; capacity is implementation defined cout << "ivec: size: " << ivec.size()<< " capacity: " << ivec.capacity() << endl;// give ivec 24 elements for (vector<int>::size_type ix = 0; ix != 24; ++ix)ivec.push_back(ix);// size should be 24; capacity will be >= 24 and is implementation defined cout << "ivec: size: " << ivec.size()<< " capacity: " << ivec.capacity() << endl;當在我們的系統(tǒng)上運行時,這段程序得到如下輸出:
ivec: size: 0 capacity: 0 ivec: size: 24 capacity: 32我們知道一個空vector的size為0,顯然在我們的標準庫實現(xiàn)中一個空vector的capacity也為0。當向vector中添加元素時,我們知道size與添加的元素數(shù)目相等。而capacity至少與size一樣大,具體會分配多少額外空間則視標準庫具體實現(xiàn)而定。在我們的標準庫實現(xiàn)中,每次添加1個元素,共添加24個元素,會使capacity變?yōu)?2。
可以想象ivec的當前狀態(tài)如下圖所示:
現(xiàn)在可以預(yù)分配一些額外空間:
ivec.reserve(50); //將capacity至少設(shè)定為50,可能會更大//size應(yīng)該為24; capacity應(yīng)該大于等于50,具體值依賴于標準庫實現(xiàn) cout << "ivec: size: " << ivec.size()<<" capacity: " << ivec.capacity() <<endl;程序的輸出表明reserve嚴格按照我們需求的大小分配了新的空間:
ivec: size: 24 capacity: 50接下來可以用光這些預(yù)留空間:
//添加元素用光多余容量 while (ivec.size() != ivec.capacity())ivec.push_back(O);//capacity應(yīng)該未改變,size和capacity不相等 cout << "ivec: size: " << ivec.size ()<<" capacity: "<< ivec.capacity () << endl;程序輸出表明此時我們確實用光了預(yù)留空間,size和 capacity相等:
ivec: size: 50 capacity: 50由于我們只使用了預(yù)留空間,因此沒有必要為vector分配新的空間。實際上,只要沒有操作需求超出vector的容量,vector就不能重新分配內(nèi)存空間。
如果我們現(xiàn)在再添加一個新元素,vector就不得不重新分配空間:
ivec.push_back(42);//再添加一個元素 // size應(yīng)該為51; capacity應(yīng)該大于等于51,具體值依賴于標準庫實現(xiàn)cout<<"ivec: size: " << ivec.size()<<" capacity: "<< ivec.capacity()<< endl;這段程序的輸出為
ivec: size: 51 capacity: 100這表明vector的實現(xiàn)采用的策略似乎是在每次需要分配新內(nèi)存空間時將當前容量翻倍。
(MyNote:真的不夠用,才去擴容。)
可以調(diào)用shrink_to_fit來要求vector將超出當前大小的多余內(nèi)存退回給系統(tǒng):
ivec.shrink_to_fit(); //要求歸還內(nèi)存 // size應(yīng)該未改變;capacity的值依賴于具體實現(xiàn) cout <<"ivec: size: " << ivec.size()<<" capacity: "<< ivec.capacity() <<endl;調(diào)用shrink_to_fit只是一個請求,標準庫并不保證退還內(nèi)存。
Note:每個 vector實現(xiàn)都可以選擇自己的內(nèi)存分配策略。但是必須遵守的一條原則是:只有當迫不得已時才可以分配新的內(nèi)存空間。
只有在執(zhí)行insert操作時size與capacity相等,或者調(diào)用resize或reserve時給定的大小超過當前capacity,vector才可能重新分配內(nèi)存空間。會分配多少超過給定容量的額外空間,取決于具體實現(xiàn)。
雖然不同的實現(xiàn)可以采用不同的分配策略,但所有實現(xiàn)都應(yīng)遵循一個原則:確保用push_back向vector添加元素的操作有高效率。從技術(shù)角度說,就是通過在一個初始為空的vector上調(diào)用n次push_back來創(chuàng)建一個n個元素的vector,所花費的時間不能超過n的常數(shù)倍。
額外的string操作
除了順序容器共同的操作之外,string類型還提供了一些額外的操作。這些操作中的大部分要么是提供string類和C風(fēng)格字符數(shù)組之間的相互轉(zhuǎn)換,要么是增加了允許我們用下標代替迭代器的版本。
標準庫string類型定義了大量函數(shù)。幸運的是,這些函數(shù)使用了重復(fù)的模式。可按需查閱。
構(gòu)造string的其他方法
第3章介紹過string部分構(gòu)造函數(shù),以及其他順序容器相同的構(gòu)造函數(shù)外,string類型還支持另外三個構(gòu)造函數(shù),如表所示:
注釋:
- n、len2和pos2都是無符號值
| string s(cp, n) | s是cp指向的數(shù)組中前n個字符的拷貝。 此數(shù)組至少應(yīng)該包含n個字符 |
| string s(s2, pos2) | s 是 string s2從下標pos2開始的字符的拷貝。 若pos2 > s2.size (),構(gòu)造函數(shù)的行為未定義 |
| string s(s2, pos2, len2) | s是string s2從下標pos2開始len2個字符的拷貝。 若pos2 > s2.size (),構(gòu)造函數(shù)的行為未定義。 不管len2的值是多少,構(gòu)造函數(shù)至多拷貝s2.size()-pos2個字符 |
這些構(gòu)造函數(shù)接受一個string或一個const char*參數(shù),還接受(可選的)指定拷貝多少個字符的參數(shù)。當我們傳遞給它們的是一個string 時,還可以給定一個下標來指出從哪里開始拷貝:
const char *cp = "Hello world!!!";//以空字符結(jié)束的數(shù)組 char noNull[]= {"H", "i"};//不是以空字符結(jié)束 string s1(cp);//拷貝cp中的字符直到遇到空字符;s1 == "Hello world! ! !"//1. string s2(noNull, 2);//從noNull拷貝兩個字符; s2 == "Hi" string s3(noNull);//未定義:noNull不是以空字符結(jié)束//2.string s4(cp + 6,5);//從cp[6]開始拷貝5個字符;s4 == "wor1d"//3. string s5(s1, 6,5);//從s1[6]開始拷貝5個字符;s5 -= "world" string s6(s1, 6);//從s1 [6]開始拷貝,直至s1末尾;s6== "wor1d! ! !" string s7(s1, 6, 20);//正確,只拷貝到s1末尾;s7 =="world! ! ! "//4. string s8(s1, 16);//拋出一個out_of_range異常//3.通常當我們從一個const char*創(chuàng)建string 時,指針指向的數(shù)組必須以空字符結(jié)尾,拷貝操作遇到空字符時停止。如果我們還傳遞給構(gòu)造函數(shù)一個計數(shù)值,數(shù)組就不必以空字符結(jié)尾。
如果我們未傳遞計數(shù)值且數(shù)組也未以空字符結(jié)尾,或者給定計數(shù)值大于數(shù)組大小,則構(gòu)造函數(shù)的行為是未定義的。
當從一個string拷貝字符時,我們可以提供一個可選的開始位置和一個計數(shù)值。開始位置必須小于或等于給定的string的大小。如果位置大于size,則構(gòu)造函數(shù)拋出一個out_of_range異常。
如果我們傳遞了一個計數(shù)值,則從給定位置開始拷貝這么多個字符。不管我們要求拷貝多少個字符,標準庫最多拷貝到string結(jié)尾,不會更多。
substr操作
| s.substr(pos, n) | 返回一個string,包含s中從pos開始的n個字符的拷貝。pos的默認值為0。n的默認值為s.size () - pos,即拷貝從pos開始的所有字符 |
substr操作返回一個string,它是原始string的一部分或全部的拷貝。可以傳遞給substr一個可選的開始位置和計數(shù)值:
string s( "hello world"); string s2 = s.substr(0,5);// s2 = hello string s3 = s.substr(6);//s3 = world string s4 = s.substr(6,11);//s3 = world string s5 = s.substr(12);//拋出一個out_of_range異常如果開始位置超過了string的大小,則substr函數(shù)拋出一個out_of_range異常。
如果開始位置加上計數(shù)值大于string的大小,則substr會調(diào)整計數(shù)值,只拷貝到string的末尾。
改變string的其他方法
string類型支持順序容器的賦值運算符以及assign、insert和erase操作。除此之外,它還定義了額外的insert和erase版本。
除了接受迭代器的insert和erase版本外,string還提供了接受下標的版本。下標指出了開始刪除的位置,或是insert到給定值之前的位置:
s.insert(s.size(), 5, '!');//在s末尾插入5個感嘆號 s.erase(s.size() - 5, 5); //從s刪除最后5個字符標準庫string類型還提供了接受C風(fēng)格字符數(shù)組的insert和 assign版本。例如,我們可以將以空字符結(jié)尾的字符數(shù)組insert到或assign給一個string:
const char *cp = "Stately, plump Buck"; s.assign(cp, 7); // s == "Stately" s.insert(s.size(), cp + 7); // s == "Stately, plump Buck"此處我們首先通過調(diào)用assign替換s的內(nèi)容。我們賦予s的是從cp指向的地址開始的7個字符。要求賦值的字符數(shù)必須小于或等于cp 指向的數(shù)組中的字符數(shù)(不包括結(jié)尾的空字符)。
接下來在s上調(diào)用insert,我們的意圖是將字符插入到s[size()]處(不存在的)元素之前的位置。在此例中,我們將cp開始的7個字符(至多到結(jié)尾空字符之前)拷貝到s中。
我們也可以指定將來自其他string或子字符串的字符插入到當前string中或賦予當前string:
string s = "some string", s2 = "some other string"; s.insert(0, s2);//在s中位置0之前插入s2的拷貝 //在s[0]之前插入s2中s2[0]開始的s2.size()個字符 s.insert(0,s2,0,s2.size());append和replace函數(shù)
string類定義了兩個額外的成員函數(shù):append和replace,這兩個函數(shù)可以改變string的內(nèi)容。下表描述了這兩個函數(shù)的功能。
append操作是在string末尾進行插入操作的一種簡寫形式:
string s("C++ Primer"), s2 = s;//將s和s2初始化為"C++ Primer" s.insert(s.size(), " 4th Ed."); // s == "C++ Primer 4th Ed." s2.append(" 4th Ed.");//等價方法:將”4th Ed."追加到s2;s == s2replace操作是調(diào)用erase和insert的一種簡寫形式:
//將"4th"替換為"5th"的等價方法 s.erase(11, 3); // s == "C++ Primer Ed . " s.insert(11, "5th");// s =="C++ Primer 5th Ed . " //從位置11開始,刪除3個字符并插入"5th" s2.replace(11, 3, "5th");//等價方法:s == s2此例中調(diào)用replace時,插入的文本恰好與刪除的文本一樣長。這不是必須的,可以插入一個更長或更短的string:
s.replace(11, 3, "Fifth");// s == "C++ Primer Fifth Ed."在此調(diào)用中,刪除了3個字符,但在其位置插入了5個新字符。
修改string的操作
| s.insert(pos, args) | 在pos之前插入args指定的字符。pos可以是一個下標或一個迭代器。 接受下標的版本返回一個指向s的引用; 接受迭代器的版本返回指向第一個插入字符的迭代器 |
| s.erase(pos, len) | 刪除從位置pos開始的len個字符。如果len被省略, 則刪除從pos開始直至s末尾的所有字符。返回一個指向s的引用 |
| s.assign(args) | 將s中的字符替換為args指定的字符。返回一個指向s的引用 |
| s.append(args) | 將args追加到s。返回一個指向s的引用 |
| s.replace(range, args) | 刪除s中范圍range內(nèi)的字符,替換為args指定的字符。 range或者是一個下標和一個長度,或者是一對指向s 的迭代器。 返回一個指向s的引用 |
上表的args可以是下列形式之一; append和assign可以使用所有形式。
str不能與s相同(MyNote:str與s互不wweiei),迭代器b和e不能指向s。
| str | 字符串str |
| str, pos, len | str中從pos開始最多l(xiāng)en個字 |
| cp, len | 從cp指向的字符數(shù)組的前(最多) len個字符 |
| cp | cp指向的以空字符結(jié)尾的字符數(shù)組 |
| n, c | n個字符c |
| b, e | 迭代器b和e指定的范圍內(nèi)的字符 |
| 初始化列表 | 花括號包圍的,以逗號分隔的字符列表 |
replace和insert所允許的args形式依賴于range和pos是如何指定的。
| T | T | T | str | |
| T | T | str, pos, len | ||
| T | T | T | cp, len | |
| T | T | cp | ||
| T | T | T | T | n, c |
| T | T | b, e | ||
| T | T | 初始化列表 |
說明:空的表示F。
改變string 的多種重載函數(shù)
上面表列出的append、assign、insert和replace函數(shù)有多個重載版本。根據(jù)我們?nèi)绾沃付ㄒ砑拥淖址蛃tring 中被替換的部分,這些函數(shù)的參數(shù)有不同版本。幸運的是,這些函數(shù)有共同的接口。
assign 和 append 函數(shù)無須指定要替換string中哪個部分:
- assign總是替換string中的所有內(nèi)容,
- append總是將新字符追加到string末尾。
replace函數(shù)提供了兩種指定刪除元素范圍的方式。
insert函數(shù)允許我們用兩種方式指定插入點:
在兩種情況下,新元素都會插入到給定下標(或迭代器)之前的位置。
可以用好幾種方式來指定要添加到string中的字符。新字符可以
- 來自于另一個string,
- 來自于一個字符指針(指向的字符數(shù)組),
- 來自于一個花括號包圍的字符列表,或者是一個字符和一個計數(shù)值。
當字符來自于一個string或一個字符指針時,我們可以傳遞一個額外的參數(shù)來控制是拷貝部分還是全部字符。
并不是每個函數(shù)都支持所有形式的參數(shù)。例如,insert就不支持下標和初始化列表參數(shù)。類似的,如果我們希望用迭代器指定插入點,就不能用字符指針指定新字符的來源。
string搜索操作
string類提供了6個不同的搜索函數(shù),每個函數(shù)都有4個重載版本。
下表描述了這些搜索成員函數(shù)及其參數(shù)。每個搜索操作都返回一個string:size_type值,表示匹配發(fā)生位置的下標。
如果搜索失敗,則返回一個名為string:npos的static成員。標準庫將npos定義為一個const string:size_type類型,并初始化為值-1。由于npos是一個unsigned類型,此初始值意味著npos等于任何string最大的可能大小。
WARNING:string搜索函數(shù)返回string::size_type值,該類型是一個unsigned類型。因此,用一個 int或其他帶符號類型來保存這些函數(shù)的返回值不是一個好主意。
string搜索操作
| s.find(args) | 查找s中args第一次出現(xiàn)的位置 |
| s.rfind(args) | 查找s中args最后一次出現(xiàn)的位置 |
| s.find_first_of(args) | 在s中查找args中任何一個字符第一次出現(xiàn)的位置 |
| s.find_last_of(args) | 在s中查找args中任何一個字符最后一次出現(xiàn)的位置 |
| s.find_first_not_of(args) | 在s中查找第一個不在args中的字符 |
| s.find_last_not_of(args) | 在s中查找最后一個不在args中的字符 |
args必須是以下形式之一
| c, pos | 從s中位置pos開始查找字符c。pos默認為О |
| s2, pos | 從s中位置pos開始查找字符串s2。pos默認為О |
| cp, pos | 從s中位置pos開始查找指針cp指向的以空字符結(jié)尾的C風(fēng)格字符串。pos默認為0 |
| cp, pos, n | 從s中位置pos開始查找指針cp指向的數(shù)組的前n個字符。pos和 n無默認值 |
find函數(shù)完成最簡單的搜索。它查找參數(shù)指定的字符串,若找到,則返回第一個匹配位置的下標,否則返回npos:
string name ( "AnnaBelle" ) ; auto pos1 = name.find ( "Anna"); //pos1 == 0這段程序返回0,即子字符串"Anna"在"AnnaBelle"中第一次出現(xiàn)的下標。
搜索(以及其他string操作)是大小寫敏感的。當在string中查找子字符串時,要注意大小寫:
string lowercase("annabelle"); pos1 = lowercase.find("Anna"); // posl == npos這段代碼會將pos1置為npos,因為Anna與anna不匹配。
一個更復(fù)雜一些的問題是查找與給定字符串中任何一個字符匹配的位置。例如,下面代碼定位name中的第一個數(shù)字:
string numbers("0123456789"), name("r2d2");//返回1,即,name中第一個數(shù)字的下標 auto pos = name.find_first_of(numbers) ;如果是要搜索第一個不在參數(shù)中的字符,我們應(yīng)該調(diào)用find_first_not_of。例如,為了搜索一個string中第一個非數(shù)字字符,可以這樣做:
string dept("03714p3"); //返回5——字符'p'的下標 auto pos = dept.find_first_not_of(numbers);指定在哪里開始搜索
我們可以傳遞給find操作一個可選的開始位置。這個可選的參數(shù)指出從哪個位置開始進行搜索。默認情況下,此位置被置為0。一種常見的程序設(shè)計模式是用這個可選參數(shù)在字符串中循環(huán)地搜索子字符串出現(xiàn)的所有位置:
string::size_type pos = 0; //每步循環(huán)查找name中下一個數(shù) while((pos = name.find_first_of(numbers, pos))!= string::npos){cout << "found number at index : " << pos<< " element is " << name[pos] << endl;++pos;//移動到下一個字符 }while的循環(huán)條件將pos重置為從 pos開始遇到的第一個數(shù)字的下標。只要find_first_of返回一個合法下標,我們就打印當前結(jié)果并遞增pos。
如果我們忽略了遞增pos,循環(huán)就永遠也不會終止。為了搞清楚原因,考慮如果不做遞增運算會發(fā)生什么。在第二步循環(huán)中,我們從 pos 指向的字符開始搜索。這個字符是一個數(shù)字,因此find_first_of會(重復(fù)地)返回pos!
逆向搜索
到現(xiàn)在為止,我們已經(jīng)用過的find操作都是由左至右搜索。標準庫還提供了類似的,但由右至左搜索的操作。rfind成員函數(shù)搜索最后一個匹配,即子字符串最靠右的出現(xiàn)位置:
string river("Mississippi"); auto first_pos = river.find("is") ; //返回1 auto last_pos = river.rfind("is"); //返回4find返回下標1,表示第一個"is"的位置,而rfind返回下標4,表示最后一個"is"的位置。
類似的,find_last函數(shù)的功能與find_first函數(shù)相似,只是它們返回最后一個而不是第一個匹配:
- find_last_of搜索與給定string 中任何一個字符匹配的最后一個字符。
- find_last_not_of搜索最后一個不出現(xiàn)在給定string中的字符。
每個操作都接受一個可選的第二參數(shù),可用來指出從什么位置開始搜索。
compare函數(shù)
除了關(guān)系運算符外,標準庫string類型還提供了一組compare函數(shù),這些函數(shù)與C標準庫的strcmp函數(shù)很相似。類似strcmp,根據(jù)s是等于、大于還是小于參數(shù)指定的字符串,s.compare返回0、正數(shù)或負數(shù)。
compare有6個版本。根據(jù)我們是要比較兩個string還是一個string與一個字符數(shù)組,參數(shù)各有不同。在這兩種情況下,都可以比較整個或一部分字符串。
s.compare的幾種參數(shù)形式
| s2 | 比較s和s2 |
| pos1, n1, s2 | 將s中從pos1開始的n1個字符與s2進行比較 |
| pos1, n1, s2, pos2, n2 | 將s中從pos1開始的n1個字符與s2中從pos2開始的n2個字符進行比較 |
| cp | 比較s與cp指向的以空字符結(jié)尾的字符數(shù)組 |
| pos1, n1, cp | 將s中從pos1開始的n1個字符與cp指向的以空字符結(jié)尾的字符數(shù)組進行比較 |
| pos1, n1, cp, n2 | 將s中從pos1開始的n1個字符與指針cp指向的地址開始的n2個字符進行比較 |
數(shù)值轉(zhuǎn)換
string和數(shù)值之間的轉(zhuǎn)換:
| to_string(val) | 一組重載函數(shù),返回數(shù)值val的string表示。val可以是任何算術(shù)類型。對每個浮點類型和int或更大的整型,都有相應(yīng)版本的to_string。與往常一樣,小整型會被提升 |
| stoi(s, p, b) | 返回s的起始子串(表示整數(shù)內(nèi)容)的數(shù)值,返回值類型分別是int、long、unsigned long、long long、unsigned long long。b表示轉(zhuǎn)換所用的基數(shù),默認值為10。p是size_t指針,用來保存s中第一個非數(shù)值字符的下標,p默認為0,即,函數(shù)不保存下標 |
| stol(s, p, b) | 同上一條 |
| stoul(s, p, b) | 同上一條 |
| stoll(s, p, b) | 同上一條 |
| stoull(s, p,b) | 同上一條 |
| stof(s, p) | 返回s的起始子串(表示浮點數(shù)內(nèi)容)的數(shù)值,返回值類型分別是float、 double 或 long double。參數(shù)p的作用與整數(shù)轉(zhuǎn)換函數(shù)中一樣 |
| stod(s, p) | 同上一條 |
| stold(s, p) | 同上一條 |
字符串中常常包含表示數(shù)值的字符。例如,我們用兩個字符的string表示數(shù)值15——字符‘1’后跟字符’5’。一般情況,一個數(shù)的字符表示不同于其數(shù)值。數(shù)值15如果保存為16位的 short類型,則其二進制位模式為0000 0000 0000 1111,而字符串"15"存為兩個Latin-1編碼的char,二進制位模式為0011 0001 0011 0101。第一個字節(jié)表示字符’1’,其八進制值為061,第二個字節(jié)表示’5’,其 Latin-1編碼為八進制值065。
新標準引入了多個函數(shù),可以實現(xiàn)數(shù)值數(shù)據(jù)與標準庫 string之間的轉(zhuǎn)換;
int i = 42; string s = to_string(i); //將整數(shù)i轉(zhuǎn)換為字符表示形式 double d = stod(s);//將字符串s轉(zhuǎn)換為浮點數(shù)此例中我們調(diào)用to_string 將42轉(zhuǎn)換為其對應(yīng)的string表示,然后調(diào)用stod將此string轉(zhuǎn)換為浮點值。
要轉(zhuǎn)換為數(shù)值的string中第一個非空白符必須是數(shù)值中可能出現(xiàn)的字符:
string s2 = "pi = 3.14"; //轉(zhuǎn)換s中以數(shù)字開始的第一個子串,結(jié)果d = 3.14 d = stod (s2.substr(s2.find_first_of("+-.0123456789")));在這個stod調(diào)用中,我們調(diào)用了find_first_of來獲得s中第一個可能是數(shù)值的一部分的字符的位置。我們將s中從此位置開始的子串傳遞給stod。stod函數(shù)讀取此參數(shù),處理其中的字符,直至遇到不可能是數(shù)值的一部分的字符。然后它就將找到的這個數(shù)值的字符串表示形式轉(zhuǎn)換為對應(yīng)的雙精度浮點值。
string參數(shù)中第一個非空白符必須是符號(+ 或 -)或數(shù)字。它可以以0x或0x開頭來表示十六進制數(shù)。對那些將字符串轉(zhuǎn)換為浮點值的函數(shù),string參數(shù)也可以以小數(shù)點(.)開頭,并可以包含e或E來表示指數(shù)部分。對于那些將字符串轉(zhuǎn)換為整型值的函數(shù),根據(jù)基數(shù)不同,string參數(shù)可以包含字母字符,對應(yīng)大于數(shù)字9的數(shù)。
Note:如果string不能轉(zhuǎn)換為一個數(shù)值,這些函數(shù)拋出一個invalid_argument異常。如果轉(zhuǎn)換得到的數(shù)值無法用任何類型來表示,則拋出一個out_of_range異常。
容器適配器
概述
除了順序容器外,標準庫還定義了三個順序容器適配器:
適配器(adaptor)是標準庫中的一個通用概念。容器、迭代器和函數(shù)都有適配器。本質(zhì)上,一個適配器是一種機制,能使某種事物的行為看起來像另外一種事物一樣。一個容器適配器接受一種已有的容器類型,使其行為看起來像一種不同的類型。例如,stack適配器接受一個順序容器(除array或forward_list外),并使其操作起來像一個stack一樣。下表列出了所有容器適配器都支持的操作和類型。
所有容器適配器都支持的操作和類型
| size_type | 一種類型,足以保存當前類型的最大對象的大小 |
| value_type | 元素類型 |
| container_type | 實現(xiàn)適配器的底層容器類型 |
| A a; | 創(chuàng)建一個名為a的空適配器 |
| A a?; | 創(chuàng)建一個名為a的適配器,帶有容器c的一個拷貝 |
| 關(guān)系運算符 | 每個適配器都支持所有關(guān)系運算符:==、!=、<、<=、>和>=這些運算符返回底層容器的比較結(jié)果 |
| a.empty() | 若a包含任何元素,返回false,否則返回true |
| a.size() | 返回a中的元素數(shù)目 |
| swap(a,b) | 交換a和b的內(nèi)容,a和b必須有相同類型,包括底層容器類型也必須相同 |
| a.swap(b) | 同上一條 |
定義一個適配器
每個適配器都定義兩個構(gòu)造函數(shù):默認構(gòu)造函數(shù)創(chuàng)建一個空對象,接受一個容器的構(gòu)造函數(shù)拷貝該容器來初始化適配器。例如,假定deq是一個deque<int>,我們可以用deq來初始化一個新的stack,如下所示:
stack<int> stk(deq);//從deq拷貝元素到stk默認情況下,stack和queue是基于deque實現(xiàn)的,priority_queue是在vector之上實現(xiàn)的。我們可以在創(chuàng)建一個適配器時將一個命名的順序容器作為第二個類型參數(shù),來重載默認容器類型。
//在vector上實現(xiàn)的空棧 stack<string,vector<string>> str_stk; // str_stk2在vector上實現(xiàn),初始化時保存svec的拷貝 stack<string,vector<string>> str_stk2(svec);對于一個給定的適配器,可以使用哪些容器是有限制的。所有適配器都要求容器具有添加和刪除元素的能力。因此,適配器不能構(gòu)造在array 之上。類似的,我們也不能用forward_list來構(gòu)造適配器,因為所有適配器都要求容器具有添加、刪除以及訪問尾元素的能力。
- stack只要求push_back、pop_back和back操作因此可以使用除array和forward_list之外的任何容器類型來構(gòu)造stack。
- queue適配器要求back、push_back、front和push_front,因此它可以構(gòu)造于list或deque之上,但不能基于vector構(gòu)造。
- priority_queue除了front、push_back和pop_back操作之外還要求隨機訪問能力,因此它可以構(gòu)造于vector或deque 之上,但不能基于list構(gòu)造。
棧適配器
stack類型定義在stack頭文件中。下表列出了stack所支持的操作。
棧默認基于deque實現(xiàn),也可以在list或vector之上實現(xiàn)。
棧特有操作
| s.pop() | 刪除棧頂元素,但不返回該元素值 |
| s.push(item) | 創(chuàng)建一個新元素壓入棧頂,該元素通過拷貝或移動item而來,或者由args構(gòu)造 |
| s.emplace(args) | 同上一條 |
| s.top() | 返回棧頂元素,但不將元素彈出棧 |
下面的程序展示了如何使用stack:
stack<int> intstack; //空棧//填滿棧 for (size_t ix = 0; ix != 10; ++ix)intstack.push(ix); //intstack 保存0到9十個數(shù)while (!intstack.empty()){//intStack 中有值就繼續(xù)循環(huán)int value = intstack.top();//使用棧頂值的代碼intstack.pop();//彈出棧頂元素,繼續(xù)循環(huán) }其中,聲明語句
stack<int> intstack; //空棧定義了一個保存整型元素的棧intstack,初始時為空。for循環(huán)將10個元素添加到棧中,這些元素被初始化為從О開始連續(xù)的整數(shù)。while循環(huán)遍歷整個stack,獲取top值,將其從棧中彈出,直至棧空。
每個容器適配器都基于底層容器類型的操作定義了自己的特殊操作。我們只可以使用適配器操作,而不能使用底層容器類型的操作。例如,
intstack.push(ix); //intstack保存0到9十個數(shù)此語句試圖在intstack的底層deque對象上調(diào)用push_back。雖然stack是基于deque實現(xiàn)的,但我們不能直接使用deque操作。不能在一個stack上調(diào)用push_back,而必須使用stack自己的操作——push。
隊列適配器
queue和priority_queue適配器定義在 queue頭文件中。下表列出了它們所支持的操作。
queue默認基于deque實現(xiàn),priority_queue 默認基于vector實現(xiàn);
queue也可以用list或vector實現(xiàn),priority_queue也可以用deque實現(xiàn)。
隊列特有的操作
| q.pop() | 返回queue的首元素或priority_queue的最高優(yōu)先級的元素,但不刪除此元素 |
| q.front() | (只適用于queue)返回首元素,但不刪除此元素 |
| q.back() | (只適用于queue)返回尾元素,但不刪除此元素 |
| q.top() | (只適用于priority_queue)返回首元素或尾元素,但不刪除此元素 |
| q.push(item) | 在queue末尾或priority_queue 中恰當?shù)奈恢脛?chuàng)建一個元素,其值為item |
| q.emplace(args) | 在queue末尾或priority_queue 中恰當?shù)奈恢脛?chuàng)建一個元素,或者由args構(gòu)造 |
標準庫queue使用一種先進先出(first-in,first-out,FIFO)的存儲和訪問策略。進入隊列的對象被放置到隊尾,而離開隊列的對象則從隊首刪除。正如飯店按客人到達的順序來為他們安排座位,就是一個先進先出隊列的例子。
priority_queue允許我們?yōu)殛犃兄械脑亟?yōu)先級。新加入的元素會排在所有優(yōu)先級比它低的已有元素之前。正如飯店按照客人預(yù)定時間而不是到來時間的早晚來為他們安排座位,就是一個優(yōu)先隊列的例子。默認情況下,標準庫在元素類型上使用 < 運算符來確定相對優(yōu)先級。我們將在第11章學(xué)習(xí)如何重載這個默認設(shè)置。
小結(jié)
本章順序容器與Java神同形似的容器
| vector | Vector、ArrayList |
| deque | LinkedList、ArrayDeque |
| list | LinkedList |
| forward_list | - |
| array(容量固定,不能增刪元素) | ArrayList(可動態(tài)擴容,能增刪元素) |
| string | String(在Java,它不是容器,另外, 它一旦初始化,內(nèi)容就不能改變) |
| stack | Stack、LinkedList |
| queue | LinkedList |
| priority_queue | PriorityQueue |
總結(jié)
以上是生活随笔為你收集整理的《C++ Primer 5th》笔记(9 / 19):顺序容器的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python(18)-字典diction
- 下一篇: 剑指offer(刷题1-10)--c++