第 11 章 字串
本章提要 11-1 C++ 標準中的字串類別 11-2 字串處理函式 11-3 字串與字元陣列 11-4 綜合演練
11-1 C++ 標準中的字串類別 在 C++ 標準中, 已預先定義了兩個專用來存放字串的類別:string、wstring, 前者是以 char 為單位來存放字串中的字元, 而 wstring 則是以 wchar_t 為單位來存放字元。要使用這兩個類別, 只需在程式開頭含括 <string> 這個檔案即可。
<string> 及字串類別 其實在 <string> 中定義的是一個稱為 basic_string 的樣版 (template), 簡單的說, 樣版可讓我們能產生行為相同、但資料成員型別不同的類別。 在 <string> 中就是用 basic_string 樣版定義出兩個專用來存放字串的類別: string:以 char 為單位來存放字串中的字元。 wstring:以 wchar_t 為單位來存放字串中的字元。
<string> 及字串類別 除了用以存放的字元的基本資料型別不同外, 這兩個類別都擁有功能相同的成員函式。但由於 wchar_t 的使用較不普遍, 所以 wstring 類別也較少人使用。 另外在 <string> 中也定義了幾個識別字, 主要是用於宣告函式的參數型別及代表特殊數值, 其中有兩個是稍後介紹成員函式時會用到的: size_type:代表字串大小或索引值的數值型別, 通常等同於 unsigned int 型別。 npos:靜態變數, 其值為 -1。
定義字串 要用 string 類別物件存放字串, 當然要用到其建構函式來建構物件, string 類別已提供了相當多種的字串建構方式, 包括從字元陣列、另一個字串物件、字元等多種方法建構字串物件:
定義字串 其中第 3 個函式可取參數字串中的部份內容來建立新字串, 此時 pos 參數所指的起始字元, 其算法和陣列元素索引一樣, 都是從 0 開始算起。例如要取第 3 個字開始的子字串時, 參數 pos 需為 2 而不是 3。 以下我們就透過範例來熟悉這些建構函式的用法:
定義字串
array array[[0] [1] [2] [3] [4] [5] [6] [7] H a p y n e w r ! str str[1] str[0] str[3] str[2] str[4] “” “xxxxxxxxxx” “Happy new year!” “Happy” “new”
size() 及 length() 成員函式都是傳回字串的大小 定義字串 size() 及 length() 成員函式都是傳回字串的大小
定義字串 1. 第 7 行定義字元陣列字串 array, 以下敘述會以此字元陣列的內容建構 string 物件。 2. 第 8、12 行定義含 5 個元素的 string 物件陣列。其中第 10 行為取 array 前 5 個字元;第 11 行則是 取 array 中第 7 個字開始的 3 個字元;第 12 行是建立由 10 個 'x' 組成的字串。 3. 第 14~19 行的迴圈則是依序輸出各字串的內容及其大小資訊。 4. 第 15 行, 因 C++ 已替 string 多載 << 運算子的夥伴函式, 所以可直接用串流物件輸出字串內容。
定義字串 5. 第 17、18 行呼叫的 size() 及 length() 成員函式都是傳回字串的大小。 從執行結果可發現, 用 sizeof() 運算子查看各物件的大小時, 都是得到 28 (位元組), 而非字串的實際大小, 因此 string 類別特別提供了 size() 及 length() 這兩個可傳回字串中字元總數的成員函式。請注意, 中文字會被視為 2 個字元。
#include<iostream> #include<string> using namespace std; int main() { char array_chiness[] = "中文字元顯示之測試"; string str1[] = { string(), // 空的字串物件 string(array_chiness), // 從字元陣列建立字串 string(array_chiness,6), string(array_chiness,6,4), string(5, 'y')}; // 從字元建立字串 for(int i=0;i<5;i++) { cout << "str1[" << i << ']' << "的內容為:" << str1[i] << endl << "\tsizeof():" << sizeof(str1[i]) // 顯示物件大小 << "\tsize():" << str1[i].size() // 顯示字串大小 << "\tlength():" << str1[i].length() << endl;//字串長度 }
中 文 字 元 顯 示 之 測 試 array_chiness [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] 中 文 字 元 顯 示 之 測 試 str1 str1[1] str1[3] str1[0] str1[2] str1[4] “” "中文字元顯示之測試" “中文字” “元顯” “yyyyy”
基本操作 認識如何建構 string 物件後, 接著介紹一些操 作 string 物件的成員函式, 讓大家可更善用 string 類別。 控制字串大小 變更字串 取得及修改字元 清除及刪除字串內容 字串比較 取子字串
控制字串大小 除了 size() 及 length() 成員函式外, string 類別還有幾個與字串大小 / 量相關的成員函式: 請注意, max_size() 所傳回的容量是理論值, 並非系統實際可存放的大小, 由以下這個範例可體會此一現實:
加入 << s1.length() << endl 控制字串大小 除了不滿15個字元會預先配置15個字元的空間外, 超過部份會以16個字元為單位, 來配置新的空間。 加入 << s1.length() << endl
控制字串大小 15 + 16 相當於4G, 約4*109
控制字串大小 第 9 行 max_size() 傳回的數字相當於是 4G, 相信很少有人的電腦有配備這麼多的記憶體, 所以我們也不可能配置這麼大的字串物件。 另外由第 14 行的輸出結果也可發現一樣有趣的事實:雖然 s2 是沒有內容的空字串, 但建構函式仍預先為它配置了可存放 15 個字元的空間。這是因為 string 字串物件可隨時變更字串的內容及大小, 為了避免在程式執行過程中, 不斷地變更字串, 導致程式需反覆重新配置空間來存放字串而降低程式效能,
控制字串大小 所以 string 類別設計成一開始就配置一定的儲存空間, 以備不時之需。當然若新的字串大於預留的儲存空間, string 物件仍是要重新配置記憶體。 因此 string 類別提供了一 個 reserve() 成員函式, 可預先保留存放字串的空間, 如此就不必在字串變大時又需臨時配置空間, 對於在程式執行過程中, 可能會不斷加大字串長度的程式, 善加應用此成員函式可改善程式的執行效率:
控制字串大小 上面列出另一 個 resize() 函式, 表面上看起來其功用和 reserve() 一樣, 但 reserve() 的目的是預先保留空間給物件, 所以它不會影響目前字串的內容 (長度);而 resize() 則會強制變更字串大小 (長度):若 resize() 成比現存字串長度還小, 將會使原字串後多出的部份被刪除;若是 resize() 成比現存字串長度還大, 則會在字串後面補上空白字元。請參考以下範例: void reserve(size_type n) //保留至少n字元數給物件 void resize(size_type n) //變更字串大小成n個字元
控制字串大小 預留15個字元的空間 15 + 16
控制字串大小 15 + 16 + 16 用 reserve() 保留額外空間後, s 的內容仍是空字串, 所以傳回的字串大小仍為 0
控制字串大小
控制字串大小 1. 在第 11、13 行呼叫 reserve() 函式保留更多空間給字串物件, 但由執行結果可發現, string 物件並不會完全照參數指定的大小來配置空間, 而是每次都以 16 個字元為單位來配置新的空間, 所以 reserve(30) 得到的大小是 31 (15+16);reserve(40) 得到的則是 47 (31+16)。 2. 用 reserve() 保留額外空間後, s 的內容仍是空字串, 所以第 15 行呼叫 size() 傳回的字串大小仍為 0。
控制字串大小 3. 第 17、19 行呼叫 reserve()、resize() 成員函式時所設的參數值都小於字串中的字數, 結果 reserve() 並未動到字串, 但 resize() 則讓字串只剩下指定的字元數。 4. 第 22 行將字串大小變成 150, 隨即輸出字串時, 看似字串沒有變動, 其實字串後面多出了一串空白字元, 使得整個輸出多達 3 行。且第 24 行呼叫 size() 傳回的字串大小也變成 150。
變更字串 resize() 的主要功能是調整大小, 使字串變短只是可能的附帶效果, 若單純要變動字串內容, 可使用其它的成員函式。其中一類是直接指定新字串內容給物件, 包括:
變更字串 = 及 += 的用法應不必多說明, 只要在等號右邊放另一 個 string 物件、字元指標 (或陣列) 或字元, 即可將其內容指定給字串物件, 或是附加到原有的物件後面。至於 assign()、append() 成員函式的用途也和對應的多載運算子相同, 只不過它們的用法更具彈性, 其多載版本比較像 string 建構函式, 例如可在參數中指定取來源字串的部份內容來建構新字串, 例如:
變更字串 append() 函式也具有相同的多載版本, 此處就不一一列出, 請參考以下應用實例:
變更字串
變更字串 1 3 2 5 8 7 4 6
變更字串 此外上列的多載運算子及成員函式都有一項特點, 就是它們都是傳回物件的參考, 所以可以串接使用, 舉例來說, 第 13、15 行的敘述可以合在一起成為:
取得及修改字元 string 類別也提供類似字元陣列的方法, 可讓我們取得指定位置的字元並修改之: 上列 3 個函式均傳回字元參考, 所以我們可以 透過它們修改字串中的某個字元:(如下頁)
取得及修改字元 s s [0] s [1] s [2] ‘+’ 因 size() 傳回值是 size_type (一般都定義為 unsigned int), 因此加上 (int) 轉型以免編譯時出現警告。 與s[i]作用相同
清除及刪除字串內容 有兩個函式可清除或刪除字串的內容: 用 erase() 刪減字元或用 clear() 清除字串時, 都不會影響 string 物件的存放空間 (容量), 如以下範例所示:
清除及刪除字串內容 3 5 1 2 4 請問s.capacity()為何?
清除及刪除字串內容 只有指定一個參數時, erase() 會刪除參數索引之後的所有字元;若是指定 2 個參數時, 則第 1 個參數表示要從第幾個字開始刪, 第 2 個參數則表示要刪幾個字。
字串比較 string 類別也多載了 >、>=、<、<=、==、!= 運算子, 所以我們可以對字串物件比較其大小。如果是要從字串物件中取部份內容出來比對, 則可使用 compare() 成員函式, 其傳回值和前幾章用過的 strcmp() 一樣也是整數, 而且 compare() 有多個版本:
字串比較
字串比較 以下為使用 compare() 成員函式進行字串比對的簡例:
字串比較 由這個執行結果, 我們也可發現:string 類別是有分辨大小寫的。
取子字串 取子字串也是字串應用最常見的操作之一, 子字串 (substring) 即為原字串的一部份, 當我們需要分析字串的內容時, 就會需要將字串分解成數個子字串, 然後做進一步的處理。string 類別提供的取子字串函式為 substr(), 其語法如下:
取子字串 函式會從物件所存字串的 pos 位置開始, 取 len 長度的子字串, 請注意, 此函式傳回的為常數字串, 所以若後續要修改子字串的內容, 需將傳回值指定給另一字串物件。以下簡單示範 substr() 的用法:
取子字串
11-2 字串處理函式 前一節介紹了一些 string 類別的基本成員函式, 本節要繼續介紹可操作字串的成員函式, 包括插尋、插入及取代的函式。 搜尋字串 插入字串 取代字串
搜尋字串 搜尋是使用字串時最常見的操作之一, 我們常常會想從字串中找一個子字串、一個字元或符號等等, string 類別提供的搜尋函式共有 6 個:
搜尋字串 上述函式的傳回值均為第 1 個符合搜尋標的字元索引值, 若找不到則傳回 npos。每個函式又各有數種多載版本, 以下僅列出 find() 成員函式為代表:
搜尋字串 各成員函式的第 1 個參數都是要在字串物件中尋找的標的字串或字元, 第 2 個參數表示要從字串物件中的第幾個字開始搜尋 (預設值 0 表示從頭開始)。上列最後一個函式的第 3 個參數, 則是表示要搜尋字串物件中有無 str 中的前 n 個字。 以下我們先用一個簡單的範例讓大家瞭解這些函式的用法:
搜尋字串
搜尋字串 雖然 find() 函式每次只能找到一個符合位置, 但只要善用迴圈配合有 2 個參數的 find() 函式, 即可找出字串中所有符合條件的位置:
搜尋字串
搜尋字串
搜尋字串 rfind() 函式的作用和 find() 相同, 只不過 rfind() 是由後往前搜尋, 舉例來說, 只要將上一個程式做一點修改, 就變成由後往前搜尋所有符合的字串:
搜尋字串
搜尋字串
搜尋字串
搜尋任何相符的字元 find_first_of() 及 find_last_of() 的搜尋方式是只要物件中, 有出現參數字串中的任一個字元, 就算符合, 而不是比對整個字串。舉例來說, 若呼叫 find_first_of("abc"), 則表示要找 a 或 b 或 c, 而非找 "abc", 請參考以下的範例:
搜尋任何相符的字元
搜尋任何相符的字元
搜尋任何相符的字元 第 7 行定義要搜尋的字串 target 為 "0123456789", 也就是所有的數字字元, 所以只要輸入的字串中含有 0~9 的數字, 就會被視為符合者。 第 9 行呼叫的 getline() 函式為 <string> 中所定義的函式, 此函式可由第 1 個參數所指定的串流物件取得含空白字元的字串內容。若這一行改寫成 "cin >> s;", 則程式在執行結果中所讀到的字串會是 "I"。因此若想取得中間含空白字元的字串, 就應使用 getline() 函式, 而不要使用 "cin >>..."。
搜尋任一不符合的字元 find_first_not_of() 及 find_last_not_of() 的搜尋方式則和前一組成員函式相反, 凡是在指定字串中的任一個字元, 就算不符合。例如呼叫 find_first_not_of ("abc"), 表示只要不是 a、b、c 這 3 個字元, 都是符合者。請參考以下的範例:
搜尋任一不符合的字元
搜尋任一不符合的字元
搜尋任一不符合的字元 第 7 行定義的字串 " \n\t" 代表空白、換行、及定位 (tab) 字元, 我們將它們都視為空白字元, 然後用 find_first_not_of() 成員函式進行搜尋, 即可計算出字串中所有非空白字元的字數。 搜尋的功能經常配合編輯功能使用, 只是單純更換某個字元, 我們可用搜尋函式的傳回值配合 [ ] 運算子達成;若要做更複雜的操作, 則可使用插入及取代的成員函式。
插入字串 要在指定的位置插入另一個字串或字元, 可使用 insert() 成員函式:
插入字串 第 1 個參數都是代表要插入的位置, 第 2 個參數為要插入的字串。若要再指定更多參數, 則代表要取參數字串的某個部份。請參考以下的範例:
插入字串
插入字串 insert() 成員函式還有多載版本可用來插入特定字元:
插入字串
取代字串 取代是另一種常見的編輯操作, replace() 成員函式的用法和 insert() 類似, 但參數略有不同:除了需指定取代的位置外, 還要指定要代換掉的長度, 接著才是指定要取代成什麼字串: 其中第 4、5 個參數省略時, 表示將要代換的地方, 換成 str 字元。請參見以下的範例:
取代字串
取代字串
取代字串
11-3 字串與字元陣列 雖然 C++ 已經提供 string 類別讓我們能以物件的方式操作字串, 但有些情況, 例如需用到早先為 C 設計的字串函式, 或是需將字串提供給外部的程式使用, 此時仍需將字串物件轉型成字元指標或字元陣列來使用。 為此 string 類別特別提供了幾個轉換用函式:
字串與字元陣列 有 2 點要特別注意: 以下是一個簡單的應用範例: c_str()、data() 傳回的都是常數字元陣列, 換言之它們的內容不能被更改。而且取得此字元指標後, 若字串物件又做了配置新空間、插入內容等動作, 則先前取得的指標將失效, 必需重新呼叫成員函式再取得一次。 data() 傳回的字元陣列及 copy() 的複製結果都不含結束字元 '\0', 所以不能當成字串使用, 只能當成字元陣列來操作。 以下是一個簡單的應用範例:
字串與字元陣列
字串與字元陣列
字串與字元陣列 1. 第 13 行配置可存放 string 物件中字串的空間。 2. 第 14 行用 c_str() 成員函式取得字串內容, 並呼叫 <cstring> 中的 strcpy() 函式將之複製到新配置的空間。 3. 第 16~22 行為進行氣泡排序法的迴圈, 會將 cstr 字串中的每個字排序。排序後在第 24 行輸出結果。
11-4 綜合演練 在撰寫程式時, 字串是幾乎一定會用到的資料型別, 許多要求使用者輸入資料的程式, 所接收到的輸入資料也都是字串, 因此熟練字串的用法更是不可或缺的技能。 身份證字號檢查 檢核身份證字號
身份證字號檢查 許多要求使用者認證的程式都會需要輸入身份證字號, 對於這類程式來說, 第一步就是確認使用者所輸入的身份證格式沒有錯誤, 確認無誤後才去驗證該身份證字號是否合法。在這一小節中, 就要示範用字串查驗身份證字號的格式。 一個正確的身份證字號, 必定是由一個英文字母以及 9 個數字所組成, 因此檢查的程式可以這樣寫:
身份證字號檢查
身份證字號檢查 檢查輸入是否恰好為十個字元。 用原型宣告放在 <cctype> 中的 isalpha() 函式檢查字串第 1 個字是否為英文字母。 用 string 類別的 substr() 成員函式取出後 9 個字的子字串。 第一個字不是大寫或小寫英文字母。
身份證字號檢查
身份證字號檢查 這個程式的檢查分成 3 個部份: 1. 第 15 行檢查輸入是否恰好為十個字元。 2. 第 19 行用原型宣告放在 <cctype> 中的 isalpha() 函式檢查字串第 1 個字是否為英文字母。 3. 第 20 行則用 string 類別的 substr() 成員函式取出後 9 個字的子字串, 再用 find_first_not_of() 檢查子字串中是否含數字以外的字元。
檢核身份證字號 確認輸入的身份證字號符合格式之後, 接著就是要檢核輸入的身份證字號是否合法。檢核的規則是這樣的: 1. 首先將第一個字母依據下表代換成數字:
檢核身份證字號 這樣身份證字號就成為一個 11 個位數的數字。 2. 從第 2 個數字開始, 將第 2 個數字乘以 9、第 3 個數字乘以 8、..、第 9 個數字乘以 2、第 10 及第 1 個數字都是乘以 1, 將這些相乘的結果加總起來。 3. 用 10 減去加總值的個位數。 4. 若上述減法的結果個位數和身份證字號的最後一個數字相同, 此身份字號即為合法, 否則即為不合法的身份字號。
檢核身份證字號 我們將上述的規則轉換成程式放在一個函式中, 然後修改前一範例, 在基本的格式檢查通過後, 呼叫此函式以檢查輸入的身份證字號是否合法:
檢核身份證字號 ‘A’ ‘B’ 第一個英文字母若為小寫, 則改為大寫
檢核身份證字號
檢核身份證字號
檢核身份證字號
檢核身份證字號 1. 第 6~27 行定義檢查身份證字號是否合法的函式, 函式一開頭先用陣列定義字母 A~Z 所對應的數值。 2. 第 12 行呼叫 islower() 函式檢查將第 1 個英文字母是否為小寫, 是就用 toupper() 將它轉換成大寫, 這兩個函式都是標準函式庫的函式, 原型宣告於 <cctype> 中。
檢核身份證字號 3. 第 15 行定義的 total 變數即是用來計算加總值, 一開始即先處理將英文字母對應的 2 個數值, 根據前述規則, 第 1 個數字乘 1 第 2 個數字則乘 9 取數值的方式是將字母減掉 'A', 再用相減結果為索引取得先前整數陣列中的值。 4. 第 17 行的迴圈則將後續的數字分別乘上 8~1 並加總起來。
檢核身份證字號 5. 第 21 行即為計算以 10 減去加總值個位數後, 再取個位數的值。 6. 第 23 行就是將前一項所得的數值與身份證字號最後一碼比對, 如果相符就是合法的身份證字號。 7. main() 函式的部份和前一範例幾乎相同, 不同之處是加入 45~48 行的部份, 也就是呼叫 checkID() 函式進行檢查, 若傳回 true 即顯示通過檢查的訊息。
字元也是整數 前幾章提過, char 型別的資料其實是以儲存字元的 ASCII 碼, 而恰好字元 '0'~'9'、'A'~'Z' 的字碼都是連續的, 所以對數字來說, 減去字元 '0' 就是對應的整數值, 例如 '8'-'0' 的結果就是 8;同理大寫英文字母減去 'A', 就可以得到該字母在 26 個英文字母中的序號 (由 0 起算), 所以可將它當成索引存取陣列中對應的數值, 這也是常用的陣列查表技巧之一 。