第3章 字符串、向量和數組
命名空間的using聲明(Namespace using Declarations)
使用using
聲明後就無須再通過專門的前綴去獲取所需的名字了。
using std::cout;
程序中使用的每個名字都需要用獨立的using
聲明引入。
頭文件中通常不應該包含using
聲明。
標準庫類型string(Library string Type)
標準庫類型string
表示可變長的字符序列,定義在頭文件string中。
定義和初始化string對象(Defining and Initializing strings)
初始化string
的方式:
如果使用等號初始化一個變量,實際上執行的是拷貝初始化(copy initialization),編譯器把等號右側的初始值拷貝到新創建的對象中去。如果不使用等號,則執行的是直接初始化(direct initialization)。
string對象上的操作(Operations on strings)
string
的操作:
在執行讀取操作時,string
對象會自動忽略開頭的空白(空格符、換行符、製表符等)並從第一個真正的字符開始讀取,直到遇見下一處空白爲止。
使用getline
函數可以讀取一整行字符。該函數只要遇到換行符就結束讀取並返回結果,如果輸入的開始就是一個換行符,則得到空string
。觸發getline
函數返回的那個換行符實際上被丟棄掉了,得到的string
對象中並不包含該換行符。
size
函數返回string
對象的長度,返回值是string::size_type
類型,這是一種無符號類型。要使用size_type
,必須先指定它是由哪種類型定義的。
如果一個表達式中已經有了size
函數就不要再使用int
了,這樣可以避免混用int
和unsigned int
可能帶來的問題。
當把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
爲了與C兼容,C++語言中的字符串字面值並不是標準庫string
的對象。
處理string對象中的字符(Dealing with the Characters in a string)
頭文件cctype中的字符操作函數:
建議使用C++版本的C標準庫頭文件。C語言中名稱爲name.h的頭文件,在C++中則被命名爲cname。
C++11提供了範圍for
(range for)語句,可以遍歷給定序列中的每個元素並執行某種操作。
for (declaration : expression)
statement
expression部分是一個對象,用於表示一個序列。declaration部分負責定義一個變量,該變量被用於訪問序列中的基礎元素。每次迭代,declaration部分的變量都會被初始化爲expression部分的下一個元素值。
string str("some string");
// print the characters in str one character to a line
for (auto c : str) // for every char in str
cout << c << endl; // print the current character followed by a newline
如果想在範圍for
語句中改變string
對象中字符的值,必須把循環變量定義成引用類型。
下標運算符接收的輸入參數是string::size_type
類型的值,表示要訪問字符的位置,返回值是該位置上字符的引用。
下標數值從0記起,範圍是0至size - 1。使用超出範圍的下標將引發不可預知的後果。
C++標準並不要求標準庫檢測下標是否合法。編程時可以把下標的類型定義爲相應的size_type
,這是一種無符號數,可以確保下標不會小於0,此時代碼只需要保證下標小於size
的值就可以了。另一種確保下標合法的有效手段就是使用範圍for
語句。
標準庫類型vector(Library vector Type)
標準庫類型vector
表示對象的集合,也叫做容器(container),定義在頭文件vector中。vector
中所有對象的類型都相同,每個對象都有一個索引與之對應並用於訪問該對象。
vector
是模板(template)而非類型,由vector
生成的類型必須包含vector
中元素的類型,如vector<int>
。
因爲引用不是對象,所以不存在包含引用的vector
。
在早期的C++標準中,如果vector
的元素還是vector
,定義時必須在外層vector
對象的右尖括號和其元素類型之間添加一個空格,如vector<vector<int> >
。但是在C++11標準中,可以直接寫成vector<vector<int>>
,不需要添加空格。
定義和初始化vector對象(Defining and Initializing vectors)
初始化vector
對象的方法:
初始化vector
對象時如果使用圓括號,可以說提供的值是用來構造(construct)vector
對象的;如果使用的是花括號,則是在列表初始化(list initialize)該vector
對象。
可以只提供vector
對象容納的元素數量而省略初始值,此時會創建一個值初始化(value-initialized)的元素初值,並把它賦給容器中的所有元素。這個初值由vector
對象中的元素類型決定。
向vector對象中添加元素(Adding Elements to a vector)
push_back
函數可以把一個值添加到vector
的尾端。
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
範圍for
語句體內不應該改變其所遍歷序列的大小。
其他vector操作(Other vector Operations)
vector
支持的操作:
size
函數返回vector
對象中元素的個數,返回值是由vector
定義的size_type
類型。vector
對象的類型包含其中元素的類型。
vector<int>::size_type // ok
vector::size_type // error
vector
和string
對象的下標運算符只能用來訪問已經存在的元素,而不能用來添加元素。
vector<int> ivec; // empty vector
for (decltype(ivec.size()) ix = 0; ix != 10; ++ix)
{
ivec[ix] = ix; // disaster: ivec has no elements
ivec.push_back(ix); // ok: adds a new element with value ix
}
迭代器介紹(Introducing Iterators)
迭代器的作用和下標類似,但是更加通用。所有標準庫容器都可以使用迭代器,但是其中只有少數幾種同時支持下標運算符。
使用迭代器(Using Iterators)
定義了迭代器的類型都擁有begin
和end
兩個成員函數。begin
函數返回指向第一個元素的迭代器,end
函數返回指向容器“尾元素的下一位置(one past the end)”的迭代器,通常被稱作尾後迭代器(off-the-end iterator)或者簡稱爲尾迭代器(end iterator)。尾後迭代器僅是個標記,表示程序已經處理完了容器中的所有元素。迭代器一般爲iterator
類型。
// b denotes the first element and e denotes one past the last element in ivec
auto b = ivec.begin(), e = ivec.end(); // b and e have the same type
如果容器爲空,則begin
和end
返回的是同一個迭代器,都是尾後迭代器。
標準容器迭代器的運算符:
C++語言定義了箭頭運算符。箭頭運算符把解引用和成員訪問兩個操作結合在一起。
因爲end
返回的迭代器並不實際指向某個元素,所以不能對其進行遞增或者解引用的操作。
在for
或者其他循環語句的判斷條件中,最好使用!=
而不是<
。所有標準庫容器的迭代器都定義了==
和!=
,但是隻有其中少數同時定義了<
運算符。
如果vector
或string
對象是常量,則只能使用const_iterator
迭代器,該迭代器只能讀元素,不能寫元素。
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
函數,不論vector
或string
對象是否爲常量,都返回const_iterator
迭代器。
任何可能改變容器對象容量的操作,都會使該對象的迭代器失效。
迭代器運算(Iterator Arithmetic)
vector
和string
迭代器支持的操作:
iter1 - iter2得到的類型是difference_type。difference_type
類型用來表示兩個迭代器間的距離,這是一種帶符號整數類型。
數組(Arrays)
數組類似vector
,但數組的大小確定不變,不能隨意向數組中添加元素。
如果不清楚元素的確切個數,應該使用vector
。
定義和初始化內置數組(Defining and Initializing Built-in Arrays)
數組是一種複合類型,聲明形式爲a[d]
,其中a是數組名稱,d是數組維度(dimension)。維度必須是一個常量表達式。
默認情況下,數組的元素被默認初始化。
定義數組的時候必須指定數組的類型,不允許用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
訪問數組元素(Accessing the Elements of an Array)
數組下標通常被定義成size_t
類型,這是一種機器相關的無符號類型,可以表示內存中任意對象的大小。size_t
定義在頭文件cstddef中。
大多數常見的安全問題都源於緩衝區溢出錯誤。當數組或其他類似數據結構的下標越界並試圖訪問非法內存區域時,就會產生此類錯誤。
指針和數組(Pointers and Arrays)
在大多數表達式中,使用數組類型的對象其實是在使用一個指向該數組首元素的指針。
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]
當使用數組作爲一個auto
變量的初始值時,推斷得到的類型是指針而非數組。但decltype
關鍵字不會發生這種轉換,直接返回數組類型。
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
C++11在頭文件iterator中定義了兩個名爲begin
和end
的函數,功能與容器中的兩個同名成員函數類似,參數是一個數組。
int ia[] = {0,1,2,3,4,5,6,7,8,9}; // ia is an array of ten ints
int *beg = begin(ia); // pointer to the first element in ia
int *last = end(ia); // pointer one past the last element in ia
兩個指針相減的結果類型是ptrdiff_t
,這是一種定義在頭文件cstddef中的帶符號類型。
標準庫類型限定使用的下標必須是無符號類型,而內置的下標運算無此要求。
C風格字符串(C-Style Character Strings)
C風格字符串是將字符串存放在字符數組中,並以空字符結束(null terminated)。這不是一種類型,而是一種爲了表達和使用字符串而形成的書寫方法。
C++標準支持C風格字符串,但是最好不要在C++程序中使用它們。對大多數程序來說,使用標準庫string
要比使用C風格字符串更加安全和高效。
C風格字符串的函數:
C風格字符串函數不負責驗證其參數的正確性,傳入此類函數的指針必須指向以空字符作爲結尾的數組。
與舊代碼的接口(Interfacing to Older Code)
任何出現字符串字面值的地方都可以用以空字符結束的字符數組來代替:
- 允許使用以空字符結束的字符數組來初始化
string
對象或爲string
對象賦值。 - 在
string
對象的加法運算中,允許使用以空字符結束的字符數組作爲其中一個運算對象(不能兩個運算對象都是)。 - 在
string
對象的複合賦值運算中,允許使用以空字符結束的字符數組作爲右側運算對象。
不能用string
對象直接初始化指向字符的指針。爲了實現該功能,string
提供了一個名爲c_str
的成員函數,返回const char*
類型的指針,指向一個以空字符結束的字符數組,數組的數據和string
對象一樣。
string s("Hello World"); // s holds Hello World
char *str = s; // error: can't initialize a char* from a string
const char *str = s.c_str(); // ok
針對string
對象的後續操作有可能會讓c_str
函數之前返回的數組失去作用,如果程序想一直都能使用其返回的數組,最好將該數組重新拷貝一份。
可以使用數組來初始化vector
對象,但是需要指明要拷貝區域的首元素地址和尾後地址。
int int_arr[] = {0, 1, 2, 3, 4, 5};
// ivec has six elements; each is a copy of the corresponding element in int_arr
vector<int> ivec(begin(int_arr), end(int_arr));
在新版本的C++程序中應該儘量使用vector
、string
和迭代器,避免使用內置數組、C風格字符串和指針。
多維數組(Multidimensional Arrays)
C++中的多維數組其實就是數組的數組。當一個數組的元素仍然是數組時,通常需要用兩個維度定義它:一個維度表示數組本身的大小,另一個維度表示其元素(也是數組)的大小。通常把二維數組的第一個維度稱作行,第二個維度稱作列。
多維數組初始化的幾種方式:
int ia[3][4] =
{ // three elements; each element is an array of size 4
{0, 1, 2, 3}, // initializers for the row indexed by 0
{4, 5, 6, 7}, // initializers for the row indexed by 1
{8, 9, 10, 11} // initializers for the row indexed by 2
};
// equivalent initialization without the optional nested braces for each row
int ib[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
// explicitly initialize only element 0 in each row
int ic[3][4] = {{ 0 }, { 4 }, { 8 }};
// explicitly initialize row 0; the remaining elements are value initialized
int id[3][4] = {0, 3, 6, 9};
可以使用下標訪問多維數組的元素,數組的每個維度對應一個下標運算符。如果表達式中下標運算符的數量和數組維度一樣多,則表達式的結果是給定類型的元素。如果下標運算符數量比數組維度小,則表達式的結果是給定索引處的一個內層數組。
// assigns the first element of arr to the last element in the last row of ia
ia[2][3] = arr[0][0][0];
int (&row)[4] = ia[1]; // binds row to the second four-element array in ia
使用範圍for
語句處理多維數組時,爲了避免數組被自動轉換成指針,語句中的外層循環控制變量必須聲明成引用類型。
for (const auto &row : ia) // for every element in the outer array
for (auto col : row) // for every element in the inner array
cout << col << endl;
如果row不是引用類型,編譯器初始化row時會自動將數組形式的元素轉換成指向該數組內首元素的指針,這樣得到的row就是int*
類型,而之後的內層循環則試圖在一個int*
內遍歷,程序將無法通過編譯。
for (auto row : ia)
for (auto col : row)
使用範圍for
語句處理多維數組時,除了最內層的循環,其他所有外層循環的控制變量都應該定義成引用類型。
因爲多維數組實際上是數組的數組,所以由多維數組名稱轉換得到的指針指向第一個內層數組。
int ia[3][4]; // array of size 3; each element is an array of ints of size 4
int (*p)[4] = ia; // p points to an array of four ints
p = &ia[2]; // p now points to the last element in ia
聲明指向數組類型的指針時,必須帶有圓括號。
int *ip[4]; // array of pointers to int
int (*ip)[4]; // pointer to an array of four ints
使用auto
和decltype
能省略複雜的指針定義。
// print the value of each element in ia, with each inner array on its own line
// p points to an array of four ints
for (auto p = ia; p != ia + 3; ++p)
{
// q points to the first element of an array of four ints; that is, q points to an int
for (auto q = *p; q != *p + 4; ++q)
cout << *q << ' ';
cout << endl;
}