《C++ Primer》學習筆記(九):順序容器
順序容器爲程序員提供了控制元素存儲和訪問順序的能力,這種順序與元素加入容器時的位置相對應。而與之相對的,關聯容器則是根據關鍵字的值來存儲元素。
容器庫概述
容器選擇基本原則:
- 除非有合適的理由選擇其他容器,否則應該使用
vector
。 - 如果程序有很多小的元素,且空間的額外開銷很重要,則不要使用
list
或forward_list
。 - 如果程序要求隨機訪問容器元素,則應該使用
vector
或deque
。 - 如果程序需要在容器頭尾位置插入/刪除元素,但不會在中間位置操作,則應該使用
deque
。 - 如果程序只有在讀取輸入時才需要在容器中間位置插入元素,之後需要隨機訪問元素。則:
- 先確定是否真的需要在容器中間位置插入元素。當處理輸入數據時,可以先向 vector 追加數據,再調用標準庫的 sort 函數重排元素,從而避免在中間位置添加元素。
- 如果必須在中間位置插入元素,可以在輸入階段使用
list
。輸入完成後將 list 中的內容拷貝到vector
中。
- 不確定應該使用哪種容器時,可以先只使用
vector
和list
的公共操作:使用迭代器,不使用下標操作,避免隨機訪問。這樣在必要時選擇vector
或list
都很方便。
容器定義和初始化
爲了創建一個容器爲另一個容器的拷貝,兩個容器的容器類型和元素類型都必須相同。傳遞迭代器參數來拷貝一個範圍時,不要求容器類型相同,而且新容器和原容器中的元素類型也可以不同,但是要能進行類型轉換。
// 每個容器有三個元素,用給定的初始化器進行初始化
list<string> authors = {"Milton", "Shakespeare", "Austen"};
vector<const char*> articles = {"a", "an", "the"};
list<string> list2(authors); // 正確:類型匹配
deque<string> authList(authors); // 錯誤:容器類型不匹配
vector<string> words(articles); // 錯誤:容器類型必須匹配
// 正確:可以將const char*元素轉換爲string
forward_list<string> words(articles.begin(), articles.end());
定義和使用 array
類型時,需要同時指定元素類型和容器大小。
array<int, 42> // 類型爲:保存42個int的數組
array<string, 10> // 類型爲:保存10個string的數組
array<int, 10>::size_type i; // 數組類型包括元素類型和大小
array<int>::size_type j; // 錯誤:array<int>不是一個類型
雖然我們不能對內置數組類型進行拷貝或對象賦值操作,但是我們可以對 array
進行拷貝或賦值操作,前提是二者的元素類型和大小都相同。
int digs[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int cpy[10] = digs; //錯誤:內置數組類型不支持拷貝或賦值
array<int, 10> digits = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
array<int, 10> copy = digits;//正確:只要數組類型匹配即合法
賦值和swap
賦值運算符要求兩側的運算對象有相同的類型。而assign
允許我們從一個不同但相容的類型賦值,或者從一個容器的子序列賦值。
list<string> names;
vector<const char*> oldstyle;
names = oldstyle; // 錯誤: 容器類型不匹配
// 正確:可以將const char*轉換爲string
names.assign(oldstyle.cbegin(), oldstyle.cend());
注意:由於其舊元素被替換,因此傳遞給assign
的迭代器不能指向調用assign
的容器。
除 array
外,swap
不對任何元素進行拷貝、刪除或插入操作,只交換兩個容器的內部數據結構,因此可以保證快速完成。
vector<string> svec1(10); // 10個元素的vector
vector<string> svec2(24); // 24個元素的vector
swap(svec1, svec2);
與其他容器不同,對一個string
調用swap
會導致迭代器、引用和指針失效。
與其他容器不同,swap
兩個array
會真正交換它們的元素。
新標準庫同時提供了成員和非成員函數版本的 swap
。非成員版本的 swap
在泛型編程中非常重要,建議統一使用非成員版本的 swap
。
順序容器操作
向順序容器中添加元素
- 除了
array
和forward_list
之外,每個順序容器(包括string
類型)都支持push_back
。 list
、forward_list
和deque
支持push_front
操作。vector
、deque
、list
和string
都支持insert
成員。forward_list
提供了特殊版本的insert
成員。
通過使用insert
的返回值,可以在容器中的一個特定位置反覆插入元素:
list<string> lst;
auto iter = lst.begin();
string word;
while(cin >> word)
iter = lst.insert(iter, word); //等價於調用push_front
emplace
函數在容器中直接構造元素。傳遞給emplace
函數的參數必須與元素類型的構造函數相匹配。
// 在c的末尾構造一個Sales_data對象
// 使用三個參數的Sales_data構造函數
c.emplace_back("978-0590353403", 25, 15.99);
// 錯誤:沒有接受三個參數的push_back版本
c.push_back("978-0590353403", 25, 15.99);
// 正確:創建一個臨時的Sales_data對象傳遞給push_back
c.push_back(Sales_data("978-0590353403", 25, 15.99));
訪問元素
表中所述的訪問元素操作返回的都是引用。如果容器是一個const
對象,則返回值是const
的引用。如果容器不是const
的,則返回的是普通引用。
刪除元素
刪除元素的成員函數並不檢查其參數。在刪除元素之前,程序用必須確保它(們)是存在的。
特殊的forward_list操作
在一個forward_list
中添加或刪除元素的操作是通過改變給定元素之後的元素來完成的。由於這些操作與其他容器上的操作的實現方式不同,forward_list
並未定義insert
、emplace
和erase
,而是定義了insert_after
、emplace_after
和erase_after
操作。例如圖9.1所示,爲了刪除elem3
,應該用指向elem2
的迭代器調用erase_after
。爲了支持這些操作,forward_list
也定義了before_begin
。它返回一個首前迭代器。
forward_list<int> flst = {0,1,2,3,4,5,6,7,8,9};
auto prev = flst.before_begin(); //表示flst的“首前元素”
auto curr = flst.begin(); //表示flst中的第一個元素
while(curr != flst.end()){ //仍有元素要處理
if(*curr % 2) //若元素爲奇數
curr = flst.erase_after(prev);//刪除它並移動curr
else{
prev = curr;
++curr;
}
}
改變容器大小
resize
函數接受一個可選的元素值參數,用來初始化添加到容器中的元素,否則新元素進行值初始化。如果容器保存的是類類型元素,且 resize
向容器添加新元素,則必須提供初始值,或元素類型提供默認構造函數。
容器操作可能使迭代器失效
向容器中添加或刪除元素可能會使指向容器元素的指針、引用或迭代器失效。失效的指針、引用或迭代器不再表示任何元素,使用它們是一種嚴重的程序設計錯誤。
- 向容器中添加元素後:
- 如果容器是
vector
或string
類型,且存儲空間被重新分配,則指向容器的迭代器、指針和引用都會失效。如果存儲空間未重新分配,指向插入位置之前元素的迭代器、指針和引用仍然有效,但指向插入位置之後元素的迭代器、指針和引用都會失效。 - 如果容器是
deque
類型,添加到除首尾之外的任何位置都會使迭代器、指針和引用失效。如果添加到首尾位置,則迭代器會失效,而指針和引用不會失效。 - 如果容器是
list
或forward_list
類型,指向容器的迭代器、指針和引用仍然有效。
- 如果容器是
- 從容器中刪除元素後,指向被刪除元素的迭代器、指針和引用失效, 對於其他元素:
- 如果容器是
list
或forward_list
類型,指向容器其他位置的迭代器、指針和引用仍然有效。 - 如果容器是
deque
類型,刪除除首尾之外的任何元素都會使迭代器、指針和引用失效。如果刪除尾元素,則尾後迭代器失效,其他迭代器、指針和引用不受影響。如果刪除首元素,這些也不會受影響。 - 如果容器是
vector
或string
類型,指向刪除位置之前元素的迭代器、指針和引用仍然有效。但尾後迭代器總會失效。
- 如果容器是
建議:管理迭代器
當你使用迭代器(或指向容器元素的引用或指針)時,最小化要求迭代器必須保持有效的程序片段是一個好的方法。
由於向迭代器添加元素和從迭代器刪除元素的代碼可能會使選代器失效,因此必須保證每次改變容器的操作之後都正確地重新定位迭代器。這個建議對 vector
、string
和 deque
尤爲重要。
//傻瓜循環、刪除偶數元素,複製每個奇數元素
vector<int> vi = {0,1,2,3,4,5,6,7,8,9};
auto iter = vi.begin(); //調用begin而不是cbegin,因爲我們要改變vi
while(iter != vi.end())
{
if(*iter % 2){
iter = vi.insert(iter, *iter); //複製當前元素
iter += 2; //向前移動迭代器,跳過當前元素以及插入到它之前的元素
}else{
iter = vi.erase(iter); //刪除偶數元素
//不應向前移動迭代器,iter指向我們刪除的元素之後的元素
}
}
注意:添加或刪除元素的循環程序必須反覆調用end
,而不能在循環之前保存end
返回的迭代器。
// 更安全的方法:在每個循環步添加/刪除元素後都重新計算end
while (begin != v.end())
{
// 做一些處理
++begin; // 向前移動begin,因爲我們想在此元素之後插入元素
begin = v.insert(begin, 42); // 插入新位
++begin; // 向前移動begin,跳過我們剛剛加入的元素
}
vector對象是如何增長的
爲了減少容器空間重新分配次數的策略,當不得不獲取新的內存空間時,vector
和string
的實現通常會分配比新的空間需求更大的內存空間。容器預留這些空間備用。
注意:reserve()
並不改變容器中元素的數量,它僅影響vector
預先分配多大的內存空間。只有當需要的內存空間超過當前容量時,reserve
調用纔會改變vector
的容量。如果需求大小小於或等於當前容量,則reserve
什麼也不做;特別是當需求大小小於當前容量時,容器不會退回內存空間(即調用reserve
永遠不會減少容器佔用的內存空間)。
- 容器的
size
是容器當前已經保存的元素數目。 - 容器的
capacity
是容器在不重新分配新的內存空間的前提下最多可以保存多少元素。
額外的string操作
構造string的其他方法
const char *cp = "Hello World!!!"; //以空字符結束的數組
char noNull[] = {'H', 'i'}; //不是以空字符結束
string s1(cp);//拷貝cp中的字符直到遇到空字符,s1="Hello World!!!"
string s2(noNull,2); //從noNull拷貝2個字符,s2="Hi"
string s3(noNull);//未定義:noNull不是以空字符結束
string s4(cp+6, 5);//從cp[6]開始拷貝5個字符,s4="World"
string s5(s1, 6, 5); //從s1[6]開始拷貝5個字符,s5="World"
string s6(s1, 6); //從s1[6]開始拷貝,直至s1末尾;s6="World!!!"
string s7(s1, 6, 20);//正確,只拷貝到s1末尾,s7="World!!!"
string s8(s1, 16);//錯誤:拋出一個out_of_range異常
改變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 == s2
// 將"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
string搜索操作
string
的每個搜索操作都返回一個 string::size_type
值,表示匹配位置的下標。如果搜索失敗,則返回一個名爲 string::npos
的 static
成員。標準庫將 npos
定義爲 const string::size_type
類型,並初始化爲-1
。
注意:string
搜索函數返回string::size_type
值,該值是一個unsigned
類型。因此用一個int
或其他帶符號類型來保存這些函數的返回值不是一個好主意。
find
操作是從左向右搜索,而rfind
是從右向左搜索。
compare函數
string
類型提供了一組類似C標準庫的 strcmp
函數的 compare
函數進行字符串比較操作。
數值轉換
C++11
提供了實現數值數據與標準庫string
類型之間的轉換。
要轉換爲數值的string
中第一個非空白符必須是數值中可能出現的字符。進行數值轉換時,string
參數的第一個非空白字符必須是符號(+
或-
)或數字。它可以以 0x
或 0X
開頭來表示十六進制數。對於轉換目標是浮點值的函數,string
參數也可以以小數點開頭,並可以包含 e
或 E
來表示指數部分。
string s2 = "pi = 3.14";
d = stod(s2.substr(s2.find_first_of("+-.0123456789")));
如果給定的 string
不能轉換爲一個數值,則轉換函數會拋出 invalid_argument
異常。如果轉換得到的數值無法用任何類型表示,則拋出 out_of_range
異常。
容器適配器
標準庫定義了 stack
、queue
和 priority_queue
三種順序容器適配器。容器適配器可以改變已有容器的工作機制,使其行爲看起來像一種不同的類型。
適配器是標準庫中的一個通用概念,容器、迭代器和函數都有適配器。本質上,適配器是一種機制,能使某種事物的行爲看起來像另外一種事物一樣。
默認情況下,stack
和 queue
是基於 deque
實現的,priority_queue
是基於 vector 實現的。可以在創建適配器時將一個命名的順序容器作爲第二個類型參數,來重載默認容器類型。
// 在vector上實現的空棧
stack<string, vector<string>> str_stk;
// strstk2在vector上實現,初始化時保存svec的拷貝
stack<string, vector<string>> str_stk2(svec);
所有適配器都要求容器具有添加和刪除元素的能力,因此適配器不能構造在 array
上。適配器還要求容器具有添加、刪除和訪問尾元素的能力,因此也不能用 forward_list
構造適配器。
stack
可以使用除array
和forward_list
之外的任何容器類型來構造。queue
由於要求push_front
操作,不能使用vector
構造,可以構造於list
或deque
之上。priority_queue
由於需要支持隨機訪問迭代器,以始終在內部保持堆結構,故不能基於list
構造,可以構造與vector
或deque
上。
queue
和priority_queue
都定義在頭文件queue中,queue
使用先進先出(first-in,first-out,FIFO)的存儲和訪問策略。priority_queue
允許我們爲隊列中的元素建立優先級,新加入的元素會排在所有優先級比它低的已有元素前。其支持的操作如下:
練習
- 下面的程序有何錯誤?你應該如何修改它?
list<int> lst1;
list<int>::iterator iter1 = lst1.begin(),
iter2 = lst1.end();
while (iter1 < iter2) /* ... */
list
是將元素以鏈表方式存儲,兩個指針的大小關係與它們指向的元素的前後關係並不一定是吻合的,實現 <
運算將會非常困難和低效。因此list
的迭代器不支持 <
運算,只支持遞增、遞減、=
以及 !=
運算。
- 編寫程序,將一個
list
中的char
* 指針元素賦值給一個vector
中的string
。
#include<iostream>
#include<string>
#include<vector>
#include<list>
using namespace std;
int main()
{
list<char*> clist = { "hello", "world", "!" };
vector<string> svec;
// svec = clist;//錯誤:容器類型不同,不能直接賦值
// 元素類型相容,可採用範圍初始化
svec.assign(clist.begin(), clist.end());
cout << svec.capacity() << " " << svec.size() << " " <<
svec[0] << " " << svec[svec.size() - 1] << endl;
system("pause");
return 0;
}
- 假定 c1 和 c2 是兩個容器,下面的比較操作有何限制?
if (c1 < c2)
-
容器類型和元素類型必須相同。
-
元素類型必須支持 < 運算符。
- 假定
iv
是一個int
的vector
,下面的程序存在什麼錯誤?你將如何修改?
vector<int>::iterator iter = iv.begin(),
mid = iv.begin() + iv.size() / 2;
while (iter != mid)
if (*iter == some_val)
iv.insert(iter, 2 * some_val);
循環中未對 iter
進行遞增操作,iter
無法向中點推進。並且insert()
會使iter 和 mid
失效。
#include<iostream>
#include<vector>
#include<string>
using namespace std;
int main()
{
vector<int> iv = { 1, 1, 2, 1 };
int some_val = 1;
vector<int>::iterator iter = iv.begin();
int org_size = iv.size(), new_ele = 0;
//任何時候,iv.begin()+org_size/2+newele 都能正確指向 iv 原來的中央元素。
while (iter != (iv.begin() + org_size / 2 + new_ele))
{
if (*iter == some_val){
iter = iv.insert(iter, 2 * some_val);
new_ele++;
iter++; iter++;
}
else{
iter++;
}
}
for (iter = iv.begin(); iter != iv.end(); iter++){
cout << *iter << endl;
}
system("pause");
return 0;
}
- 編寫程序,查找並刪除
forward_list<int>
中的奇數元素。
在 forward_list
中插入、刪除元素既需要該元素的迭代器,也需要前驅迭代器。爲此,forward_list
提供了 before_begin
來獲取首元素之前位置的迭代器,且插入、刪除都是 after
形式,即刪除(插入)給定迭代器的後繼。
#include <iostream>
#include <forward_list>
using namespace std;
int main()
{
forward_list<int> iflst = { 1, 2, 3, 4, 5, 6, 7, 8 };
auto prev = iflst.before_begin();
auto curr = iflst.begin();
while (curr != iflst.end())
{
if (*curr & 1){
curr = iflst.erase_after(prev);
}
else{
prev = curr;
++curr;
}
}
for (curr = iflst.begin(); curr != iflst.end(); curr++){
cout << *curr << " ";
}
cout << endl;
system("pause");
return 0;
}
5. 編寫程序,從一個 vector<char>
初始化一個 string
。
#include <iostream>
#include <string>
#include <vector>
using namespace std;
int main()
{
vector<char> vc = { 'a', 'b', 'c', 'd', 'e' };
string s(vc.data(), vc.size());
cout << s << endl;
system("pause");
return 0;
}
- 編寫一個函數,接受三個
string
參數是s
、oldVal
和newVal
。使用迭代器及insert
和erase
函數將s
中所有oldVal
替換爲newVal
。測試你的程序,用它替換通用的簡寫形式,如,將"tho"替換爲"though",將"thru"替換爲"through"。
#include <iostream>
#include <string>
#include <vector>
using namespace std;
void replace_string(string &s, const string &oldVal, const string &newVal)
{
auto old_size = oldVal.size();
if(oldVal.empty())
return;
auto iter1 = s.begin();
while(iter1 < s.end()){
auto iter2 = iter1;
auto iter3 = oldVal.begin();
while(iter3 != oldVal.end() && iter2 != s.end() && *iter2 == *iter3){
++iter2;
++iter3;
}
if(iter3 == oldVal.end()){
iter1 = s.erase(iter1, iter2);
if(!newVal.empty()){
auto iter4 = newVal.end();
do{
--iter4;
iter1 = s.insert(iter1, *iter4);
}while(iter4 > newVal.begin());
}
iter1 += newVal.size();
}else{
++iter1;
}
}
}
int main()
{
string s = "tho thru tho!";
replace_string(s, "thru", "through");
cout << s << endl;
replace_string(s, "tho", "though");
cout << s << endl;
replace_string(s, "through", "");
cout << s << endl;
system("pause");
return 0;
}
- 重寫上一題的函數,這次使用一個下標和
replace
。
#include <iostream>
#include <string>
#include <vector>
using namespace std;
void replace_string(string &s, const string &oldVal, const string &newVal)
{
auto old_size = oldVal.size();
if(oldVal.empty())
return;
auto match_pos = s.find(oldVal);
while(match_pos != string::npos){
s.replace(match_pos, oldVal.size(), newVal);
match_pos += newVal.size();
match_pos = s.find(oldVal, match_pos);
}
}
int main()
{
string s = "tho thru tho!";
replace_string(s, "thru", "through");
cout << s << endl;
replace_string(s, "tho", "though");
cout << s << endl;
replace_string(s, "through", "");
cout << s << endl;
system("pause");
return 0;
}
- 設計一個類,它有三個
unsigned
成員,分別表示年、月和日。爲其編寫構造函數,接受一個表示日期的string
參數。你的構造函數應該能處理不同的數據格式,如January 1,1900
、1/1/1990
、Jan 1 1900
等。
Date.h
:
#ifndef DATE_H_INCLUDED
#define DATE_H_INCLUDED
#include <iostream>
#include <string>
#include<stdexcept> //異常處理機制
using namespace std;
class Date{
public:
friend ostream& operator<<(ostream&, const Date&);
Date() = default;
Date(const string &ds);
unsigned get_year() const {return year;}
unsigned get_month() const {return month;}
unsigned get_day() const {return day;}
private:
unsigned year;
unsigned month;
unsigned day;
};
// 月份全稱
const string month_name[] = { "January", "February", "March",
"April", "May", "June", "July", "August", "September",
"October", "November", "December" };
// 月份簡寫
const string month_abbr[] = { "Jan", "Feb", "Mar", "Apr", "May",
"Jun", "Jul", "Aug", "Sept", "oct", "Nov", "Dec" };
// 每月天數
const int days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
inline unsigned month_parsing(const string& ds, size_t &end_pos){
if(ds.empty() || end_pos >= ds.size()){
throw invalid_argument("illegal date1");
}
int i = 0;
size_t j=0;
//首先檢測是不是月份簡寫
for(i=0; i<12; ++i){
for(j=0; j<month_abbr[i].size(); ++j){
if(ds[j] != month_abbr[i][j]){
break;
}
}
if(j == month_abbr[i].size()){
break; //匹配簡寫成功
}
}
if(i==12){//匹配簡寫失敗
throw invalid_argument("illegal date2");
}
if(ds[j] == ' '){ //空白符,則是簡寫
end_pos = j + 1;
return i+1;//返回對應月份
}else{//如果不是簡寫則繼續匹配完整月份
for(; j<month_name[i].size(); ++j){
if(ds[j] != month_name[i][j]){
break;
}
}
if(j == month_name[i].size() && ds[j] == ' '){//匹配月份全稱成功
end_pos = j + 1;
return i+1;
}
}
throw invalid_argument("illegal date3");;//匹配簡寫和全稱均失敗
}
inline unsigned day_parsing(string&ds, unsigned month, size_t &p){
size_t q;
int day = stoi(ds.substr(p), &q); // 從p開始的部分轉換爲日期值,
if (day<1 || day>days[month])
throw invalid_argument("illegal date4");
p += q;//移動到日期值之後
return day;
}
inline unsigned year_parsing(string &ds, size_t &p){
size_t q;
int year = stoi(ds.substr(p), &q); // 從p開始的部分轉換爲年
if (p + q < ds.size())
throw invalid_argument("illegal ending5");
return year;
}
Date::Date(const string&ds){
string s = ds;
auto pos1 = s.find_first_of("0123456789");//返回第一個數字的位置
if(pos1 == string::npos){
throw invalid_argument("illegal date6");
}
if(pos1 > 0){ //如果string的第一個不是數字,則證明是月份的英文全稱或縮寫
month = month_parsing(s, pos1); //返回月份值,並在函數內修改pos1爲day的下一位的索引
day = day_parsing(s, month, pos1);
if (s[pos1] != ' ' && s[pos1] != ',')
throw invalid_argument("illegal spacer7");
++pos1;
year = year_parsing(s, pos1);
}else{ //string中的月份也是數字格式
size_t pos2 = 0;
month = stoi(s, &pos2); //獲取月份,並將月份的下一位的索引保存在pos2中
pos1 = pos2;
if (month<1 || month >12)
throw invalid_argument("not a legal month value8");
if (s[pos1++] != '/')
throw invalid_argument("illegal spacer9");
day = day_parsing(s, month, pos1);
if (s[pos1++] != '/')
throw invalid_argument("illegal spacer10");
year = year_parsing(s, pos1);
}
}
ostream & operator<<(ostream& out, const Date& d){
out << "year:" << d.get_year() << " month:" << d.get_month() << " day:" << d.get_day() << endl;
return out;
}
#endif // DATE_H_INCLUDED
main.cpp
:
#include <iostream>
#include <string>
#include "Date.h"
using namespace std;
int main()
{
string dates[] = { "Jan 1, 2014", "February 1 2014", "3/1/2014", "3 1 2014"
//"Jcn 1,2014",
//"Janvary 1,2014",
//"Jan 32,2014",
//"Jan 1/2014",
};
try{
for (auto ds : dates){
date dl(ds);
cout << dl;
}
}
catch (invalid_argument e){
cout << e.what() << endl;
}
system("pause");
return 0;
}