C++ Primer 第三章筆記
Chapter 3 Strings, Vectors, and Arrays
3.1 名稱空間的 using 宣告
目前為止,我們用到的庫函式基本上都屬於名稱空間 std,而程式也顯式地將這一點標註出來。例如,std::cin 表示從標準輸入中讀取內容。此處的作用域操作符(::)的含義是:編譯器從操作符左側名字所示的作用域中尋找右側那個名字。因此,std::cin 的意思就是要使用名稱空間 std 的名字 cin。
較為簡單且安全的方法是使用 using 宣告(using declaration),它具有如下的形式:
using namespace::name
#include <iostream>
// using declaration; when we use the name cin, we get the one from the namespace
using std::cin;
int main()
{
int i;
cin >> i; // ok: cin is a synonym for std::cin
cout << i; // error: no using declaration; we must use the full name
std::cout << i; // ok: explicitly use cout from namepsace std
return 0;
}
每個名字都需要獨立的 using 宣告
按照規定,每個 using 宣告引入名稱空間中的一個成員。
標頭檔案不應包含 using 宣告
因為標頭檔案的內容會拷貝到所有引用它的檔案中,如果標頭檔案裡有某個 using 宣告,那麼每個使用了該標頭檔案的檔案就都會有這個宣告。
3.2 標準庫型別 string
標準庫型別 string 表示可變長的字元序列,使用 string 型別必須首先包含 string 標頭檔案。作為標準庫的一部分,string 定義在名稱空間 std 中。
3.2.1 定義和初始化 string 物件
string s1; // default initialization; s1 is the empty string
string s2(s1); // s2 is a copy of s1
string s2 = s1; // equivalent to s2(s1), s2 is a copy of s1
string s3("value"); // s3 is a copy of the string literal, not including the null
string s3 = "value" // equivalent to s3("value"), s3 is a copy of the string literal
string s4(10, 'c'); // s4 is cccccccccc
直接初始化和拷貝初始化
如果使用等號(=)初始化一個變數,執行的是拷貝初始化(copy initialization)。與之相反,不使用等號,執行的是直接初始化(direct initialization)。
3.2.2 string 物件上的操作
讀寫 string 物件
// Note: #include and using declarations must be added to compile this code
int main()
{
string s; // empty string
cin >> s; // read a whitespace-separated string into s
cout << s << endl; // write s to the output
return 0;
}
執行讀取操作時,string 物件會自動忽略開頭的空白(即空格符、換行符、製表符等)並從第一個真正的字元開始讀起,直到遇見下一處空白為止。
讀取未知數量的 string 物件
int main()
{
string word;
while (cin >> word) // read until end-of-file
cout << word << endl; // write each word followed by a new line
return 0;
}
使用 getline 讀取一整行
getline 函式的引數是一個輸入流和一個 string 物件,函式從給定的輸入流中讀入內容,內容遇到換行符為止(注意換行符也被讀進來了),爾後把所讀的內容存入到那個 string 物件中去(不存換行符)。getline 一遇到換行符就結束讀取並返回結果。
int main()
{
string line;
// read input a line at a time until end-of-file
while (getline(cin, line))
cout << line << endl;
return 0;
}
string::size_type 型別
size 函式返回的是一個 string::size_type 型別的值,它是一個無符號型別的值,是一個無符號整型數。如果一條表示式中已經有了 size() 函式就不要再使用 int 了,這樣可以避免混用 int 和 unsigned 可能帶來的問題。例如,假設 n 是一個具有負值的 int,則表示式 s.size() < n 的判斷結果幾乎肯定是 true。因為負值 n 會自動轉換成一個比較大的無符號值。
比較 string 物件
string 類定義了幾種用於比較字串的運算子。這些比較運算子逐一比較 string 物件中的字元,並且對大小寫敏感。相等性運算子(== 和 !=)與關係型運算子(<, <=, >, >=)都按照(大小寫敏感的)字典順序進行比較。
字面值和 string 物件相加
必須確保每個加法運算子的兩側的運算物件至少有一個是 string:
string s4 = s1 + ", "; // ok: adding a string and a literal
string s5 = "hello" + ", "; // error: no string operand
string s6 = s1 + ", " + "world"; // ok: each + has a string operand
string s7 = "hello" + ", " + s2; // error: can't add string literals
切記,字串字面值與 string 是不同的型別。
3.2.3 處理 string 物件中的字元
cctype 標頭檔案裡的函式
處理每個字元?使用基於範圍的 for 語句
for (declaration: expression)
statement
其中,expression 部分是一個物件,用於表示一個序列。declaration 部分負責定義一個變數,該變數將被用於訪問序列中的基礎元素。每次迭代,declaration 部分的變數會被初始化為 expression 部分的下一個元素值。例如:
string s("Hello World!!!");
// punct_cnt has the same type that s.size returns; see § 2.5.3 (p. 70)
decltype(s.size()) punct_cnt = 0;
// count the number of punctuation characters in s
for (auto c : s) // for every char in s
if (ispunct(c)) // if the character is punctuation
++punct_cnt; // increment the punctuation counter
cout << punct_cnt << " punctuation characters in " << s << endl;
// The output of this program is:
// 3 punctuation characters in Hello World!!!
基於範圍 for 語句改變字串的字元
如果想要改變 string 物件中字元的值,必須把迴圈變數定義為引用型別
string s("Hello World!!!");
// convert s to uppercase
for (auto &c : s) // for every char in s (note: c is a reference)
c = toupper(c); // c is a reference, so the assignment changes the char in s
cout << s << endl;
// The output of this code is:
// HELLO WORLD!!!
只處理一部分字元?
一種方法是使用下標,另一種是使用迭代器。
下標運算子([ ])接受的引數是 string::size_type 型別的值,這個引數表示要訪問的字元的位置;返回值是該位置上字元的引用。下標從 0 開始,且必須大於等於 0 而小於 s.size()。下標的值稱為 ”下標“ 或者 ”索引”。
在訪問指定字元前,首先檢查字元是否為空,不管什麼時候,對 string 物件使用下標,都要確認在那個位置上確實有值。
使用下標進行迭代
// process characters in s until we run out of characters or we hit a whitespace
for (decltype(s.size()) index = 0;index != s.size() && !isspace(s[index]); ++index)
s[index] = toupper(s[index]); // capitalize the current character
// This program generates:
// SOME string
3.3 標準庫型別 vector
標準庫型別 vector 表示物件的集合,其中所有物件的型別都相同。集合中的每個物件都有一個與之對應的索引。vector 也常被稱作容器(container)。
要想使用 vector,和使用 string 一樣,必須包含適當的標頭檔案:
#inlcude <iostream>
using std::vector
C++ 既有類模板,也有函式模板,其中 vector 是一個類模板。模板本身不是類或函式,相反可以將模板看作為編譯器生成類或函式編寫的一份說明。編譯器根據模板建立類或函式的過程稱為例項化(initialization),使用模板時,需要指出編譯器應把類或函式例項化成何種型別。
對於類模板,我們需要提供資訊指定模板例項化成什麼樣的類,提供方法如下:
vector<int> ivec; // ivec holds objects of type int
vector<Sales_item> Sales_vec; // holds Sales_items
vector<vector<string>> file; // vector whose elements are vectors
vector 能容納絕大型別的物件作為其元素,但是因為引用不是物件,所以不存在包含引用的 vector。除此之外,其他大多數內建型別和類型別都可以構成 vector 物件,甚至是 vector。在早期版本中,若 vector 裡的元素 還是 vector,則定義方法與 C++11 有所不同,必須在外層 vector 物件的右尖括號和其它元素型別之間新增一個空格,如:
vector<vector<int> > // old C++
vector<vector<int>> // C++11
3.3.1 定義和初始化 vector 物件
和任意一種類型別一樣,vector 模板控制著定義和初始化向量的方法。
列表初始化 vector 物件
用花括號括起來得 0 個或多個初始元素值被賦給 vector 物件:
vector<string> articles = {"a", "an", "the"}; // the vector has three elements; the first holds the string "a", the second holds "an", and the last is "the".
C++ 提供了集中不同得初始化方式,大多可以等價使用,但有例外:其一,使用拷貝初始化時(使用 = 時),只能提供一個初值;其二,如果提供的是一個類內初始值,則只能使用拷貝初始化或使用花括號初始化。第三種特殊的要求時,如果提供的時初始元素值得列表,則只能把初始值都放在花括號裡進行列表初始化,而不能放在圓括號裡:
vector<string> v1{"a", "an", "the"}; // list initialization
vector<string> v2("a", "an", "the"); // error
建立指定數量的元素
vector<int> ivec(10, -1); // ten int elements, each initialized to -1
vector<string> svec(10, "hi!"); // ten strings; each element is "hi!"
值初始化
通常情況下,只提供 vector 物件容納的元素數量而不用略去初始值。此時,庫會建立一個值初始化的(value-initialized)元素初值,並把它付給容器中所有元素,這個初值由 vector 物件中的元素型別決定。
列表初始值還是元素數量?
注意花括號和圓括號:
vector<int> v1(10); // v1 has ten elements with value 0
vector<int> v2{10}; // v2 has one element with value 10
vector<int> v3(10, 1); // v3 has ten elements with value 1
vector<int> v4{10, 1}; // v4 has two elements with values 10 and 1
vector<string> v5{"hi"}; // list initialization: v5 has one element
vector<string> v6("hi"); // error: can't construct a vector from a string literal
vector<string> v7{10}; // v7 has ten default-initialized elements
vector<string> v8{10, "hi"}; // v8 has ten elements with value "hi"
3.3.2 向 vector 物件中新增元素
有時,建立一個 vector 物件時並不清楚實際所需元素個數,元素的值也經常無法確定。此時,更好的處理方法就是先建立一個空 vector,然後再執行時再利用 vector 的成員函式 push_back 向其中新增元素,例如:
vector<int> v2; // empty vector
for (int i = 0; i != 100; ++i)
v2.push_back(i); // append sequential integers to v2
// at end of loop v2 has 100 elements, values 0 . . . 99
// read words from the standard input and store them as elements in a vector
string word;
vector<string> text; // empty vector
while (cin >> word) {
text.push_back(word); // append word to text
}
如果迴圈體內部包含有向 vector 物件新增元素的語句,則不能使用範圍 for 迴圈。範圍 for 語句體內不應改變其所遍歷序列的大小。
3.3.3 其他 vector 操作
訪問 vector 物件中元素的方法也是通過元素再 vector 物件中的位置,與訪問 string 物件類似:
vector<int> v{1,2,3,4,5,6,7,8,9};
for (auto &i : v) // for each element in v (note: i is a reference)
i *= i; // square the element value
for (auto i : v) // for each element in v
cout << i << " "; // print the element
cout << endl;
vector 物件的下標也是從 0 開始計起,vector 物件(以及 string 物件)的下標運算子可用於訪問已存在的元素,而不能用於新增元素。
3.4 迭代器介紹
迭代器也提供了對物件的間接訪問,其物件是容器中的元素或者 string 物件中的字元。使用迭代器可以訪問某個元素,迭代器也能從一個元素移動到另一個元素。迭代器有有效和無效之分,有效的迭代器或者指向某個元素,或者指向容器中尾元素的下一位置;其它所有情況都屬於無效。
3.4.1 使用迭代器
與指標不一樣的是,獲取迭代器不是使用取地址符,有迭代器的型別同時擁有返回迭代器的成員。比如,這些型別都擁有名為 begin 和 end 的成員,其中 begin 成員負責返回指向第一個元素(或第一個字元)的迭代器。如:
// the compiler determines the type of b and e; see § 2.5.2 (p. 68)
// b denotes the first element and e denotes one past the last element in v
auto b = v.begin(), e = v.end() ; // b and e have the same type
end 成員則負責返回指向容器(或 string 物件)“尾元素的下一位置(one past the end)” 的迭代器,即一個本不存在的 “尾後” 元素。這樣的迭代器並沒有什麼實際含義,僅是個標記,表示問哦們已經處理完了容器中的所有元素。end 成員返回的迭代器被稱作尾後迭代器(off-the-end iterator)或者稱為尾迭代器(end iterator)。若容器為空,begin 和 end 返回的是同一個迭代器,都是尾後迭代器。
迭代器運算子
與指標類似,可以解引用迭代器來獲取它所指示的元素,執行解引用的迭代器必須合法並確實指示某個元素。
string s("some string");
if (s.begin() != s.end()) { // make sure s is not empty
auto it = s.begin(); // it denotes the first character in s
*it = toupper(*it); // make that character uppercase
}
將迭代器從一個元素移動到另外一個元素
迭代器使用 ++ 運算子將一個元素移動到下一個元素,迭代器的遞增是將迭代器 “向前移動一個位置”。end 返回的迭代器不實際指示某個元素,所以不能對其進行遞增或解引用的操作。
// process characters in s until we run out of characters or we hit a whitespace
for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it)
*it = toupper(*it); // capitalize the current character
迭代器型別
實際上,擁有迭代器的標準型別使用 iterator 和 const_iterator 來表示迭代器的型別:
vector<int>::iterator it; // it can read and write vector<int> elements
string::iterator it2; // it2 can read and write characters in a string
vector<int>::const_iterator it3; // it3 can read but not write elements
string::const_iterator it4; // it4 can read but not write characters
我們認定某個型別是迭代器當且僅當它支援一套操作,這套操作使得我們能訪問元素或者從某個元素移動到另外一個元素。
begin 和 end 運算子
begin 和 end 返回的具體型別由物件是否是常量決定,如果是常量,begin 和 end 返回 const_iterator;如果物件不是常量,返回 iterator:
vector<int> v;
const vector<int> cv;
auto it1 = v.begin(); // it1 has type vector<int>::iterator
auto it2 = cv.begin(); // it2 has type vector<int>::const_iterator
C++11 引入兩個新的函式,cbegin 和 cend 專門得到 const_iterator 型別的返回值:
auto it3 = v.cbegin(); // it3 has type vector<int>::const_iterator
結合解引用和成員訪問操作
(*it).empty() // dereferences it and calls the member empty on the resulting object
*it.empty() // error: attempts to fetch the member named empty from it
// but it is an iterator and has no member named empty
C++ 定義了箭頭運算子(- >)將解引用和成員訪問兩個操作結合在一起。
// print each line in text up to the first blank line
for (auto it = text.cbegin(); it != text.cend() && !it->empty(); ++it)
cout << *it << endl;
3.4.2 迭代器的算術運算
// compute an iterator to the element closest to the midpoint of vi
auto mid = vi.begin() + vi.size() / 2;
if (it < mid)
// process elements in the first half of vi
使用迭代器運算
// text must be sorted
// beg and end will denote the range we're searching
auto beg = text.begin(), end = text.end();
auto mid = text.begin() + (end - beg)/2; // original midpoint
// while there are still elements to look at and we haven't yet found sought
while (mid != end && *mid != sought) {
if (sought < *mid) // is the element we want in the first half?
end = mid; // if so, adjust the range to ignore the second half
else // the element we want is in the second half
beg = mid + 1; // start looking with the element just after mid
mid = beg + (end - beg)/2; // new midpoint
}
3.5 陣列
不清楚元素的確切個數,請使用 vector。
3.5.1 定義和初始化內建陣列
unsigned cnt = 42; // not a constant expression
constexpr unsigned sz = 42; // constant expression
// constexpr see § 2.4.4 (p. 66)
int arr[10]; // array of ten ints
int *parr[sz]; // array of 42 pointers to int
string bad[cnt]; // error: cnt is not a constant expression
string strs[get_size()]; // ok if get_size is constexpr, error otherwise
定義陣列必須指定陣列的型別,不允許用 auto 關鍵字,且不存在引用的陣列。
顯式初始化陣列元素
對陣列的元素進行列表初始化,此時允許忽略陣列的維度。
const unsigned sz = 3;
int ia1[sz] = {0,1,2}; // array of three ints with values 0, 1, 2
int a2[] = {0, 1, 2}; // an array of dimension 3
int a3[5] = {0, 1, 2}; // equivalent to a3[] = {0, 1, 2, 0, 0}
string a4[3] = {"hi", "bye"}; // same as a4[] = {"hi", "bye", ""}
int a5[2] = {0,1,2}; // error: too many initializers
字元陣列的特殊性
字元陣列可以用字串字面值進行初始化,注意字串字面值結尾還有一個空字元:
char a1[] = {'C', '+', '+'}; // list initialization, no null
char a2[] = {'C', '+', '+', '\0'}; // list initialization, explicit null
char a3[] = "C++"; // null terminator added
automatically
const char a4[6] = "Daniel"; // error: no space for the null!
不允許拷貝和賦值
不能將陣列的內容拷貝給其他陣列作為其初始值,也不能用陣列為其他陣列複製。
理解複雜的陣列宣告
int *ptrs[10]; // ptrs is an array of ten pointers to int
int &refs[10] = /* ? */; // error: no arrays of references
int (*Parray)[10] = &arr; // Parray points to an array of ten ints
int (&arrRef)[10] = arr; // arrRef refers to an array of ten ints
按照由內向外的順序閱讀。
3.5.2 訪問陣列元素
使用陣列下標的時候,通常將其定義為 size_t 型別。size_t 是一種機器相關的無符號型別,它被設計得足夠大以便能表示記憶體中任意物件的大小。
與 vector 和 string 一樣,陣列的下標是否在合理範圍之內由程式設計師負責檢查。大多數常見的安全問題都源於緩衝區溢位錯誤。當陣列或其他類似資料結構的下標越界並試圖訪問非法記憶體區域時,就會產生此類錯誤。
3.5.3 指標和陣列
在大多數表示式中,使用陣列型別的物件其實時使用一個指向該陣列首元素的指標:
string nums[] = {"one", "two", "three"}; // array of strings
string *p = &nums[0]; // p points to the first element in nums
string *p2 = nums; // equivalent to p2 = &nums[0]
int ia[] = {0,1,2,3,4,5,6,7,8,9}; // ia is an array of ten ints
auto ia2(ia); // ia2 is an int* that points to the first element in ia
ia2 = 42; // error: ia2 is a pointer, and we can't assign an int to a pointer
auto ia2(&ia[0]); // now it's clear that ia2 has type int*
// ia3 is an array of ten ints
decltype(ia) ia3 = {0,1,2,3,4,5,6,7,8,9};
ia3 = p; // error: can't assign an int* to an array
ia3[4] = i; // ok: assigns the value of i to an element in ia3
使用 decltype 關鍵字時上述轉換不會發生。
指標也是迭代器
vector 和 string 的迭代器支援的運算,陣列的指標全部支援。例如,允許使用遞增運算子將指向陣列元素的指標向前移動到下一個位置上:
int arr[] = {0,1,2,3,4,5,6,7,8,9};
int *p = arr; // p points to the first element in arr
++p; // p points to arr[1]
獲取尾元素之後的那個並不存在的元素的地址:
int *e = &arr[10]; // pointer just past the last element in arr
這裡顯然索引了一個不存在的元素,唯一的用處是提供地址初始化 e,不能對尾後指標執行解引用或遞增的操作。
標準庫函式 begin 和 end
int ia