1. 引言
如果把字串當作「字元陣列」來看,很容易去手動處理一切:用迴圈去找 email 裡的 @、逐字元地切出網域、透過比較最後四個字元來檢查副檔名……這當然可行,但很快會把程式碼變成一團亂。
C++ 標準函式庫提供了一組方法,能讓常見的文字操作更簡單、也更易讀。這通常是「更短」同時也是「更容易理解」的情況。今天我們要掌握四個概念:搜尋(find())、擷取(substr())、檢查前綴/後綴(starts_with(), ends_with())和判斷是否包含(contains() 或等價的透過 find())。
2. 字串搜尋:find() 與「是否包含」檢查
當你在編輯器按 Ctrl+F 並輸入一個單詞時,編輯器會回傳找到片段的位置。std::string 的模型大致相同:方法 find(...) 會嘗試找到第一個出現的字元或子字串,並回傳找到的起始位置(索引)。
重要的是要習慣一點:find() 並不回傳 true/false。它回傳的是一個位置,如果沒有找到則回傳一個特殊值。
find() 回傳什麼
find() 回傳類型是 std::size_t —— 與 size() 相同的類型。這很合理:字串中的位置就是索引,而在標準函式庫中索引與長度通常使用 std::size_t。
如果沒有找到匹配,find() 會回傳 std::string::npos。這不是「包裝過的 -1」,而是一個特殊值(實際上是 size_t 的一個很大的數)。因此正確的檢查方式是: pos != std::string::npos。
我們用一個小表格來固定概念。取字串 text = "one two three":
| 搜尋內容 | 範例結果 | 原因 |
|---|---|---|
|
|
單字 從索引 開始 |
|
|
沒有這個片段 |
小範例:尋找單字
#include <iostream>
#include <string>
int main() {
std::string text = "one two three";
std::size_t pos = text.find("two");
if (pos != std::string::npos) {
std::cout << "found at " << pos << '\n'; // found at 4
} else {
std::cout << "not found\n";
}
}
這裡可以清楚看到契約:要麼「找到並回傳位置」,要麼「沒找到並回傳 npos」。
find() 與單一字元搜尋
很多時候我們要找的不是一段文字,而是一個分隔字元:例如 email 裡的 @,或檔名中的 .。
#include <iostream>
#include <string>
int main() {
std::string email = "alice@example.com";
std::size_t at = email.find('@');
if (at != std::string::npos) {
std::cout << "@ at " << at << '\n'; // @ at 5
} else {
std::cout << "no @\n";
}
}
對 find() 的最重要安全規則
在使用位置之前,務必檢查 pos != std::string::npos。
為什麼這麼重要?因為 npos 不能當作索引使用。更危險的是在 pos == npos 的情況下做 pos + 1:會變成一個很大的數,接下來你要麼遇到執行期錯誤,要麼得到不正確的結果(視操作而定)。
「字串是否包含…」:用 contains() 或 find()
當程式設計師說「檢查字串是否包含子字串」時,幾乎不會想要位置;他們只需要一個層次上的「是/否」。在更新的 C++ 標準中,std::string 新增了 contains() 方法,可以直接完成這件事。
需要理解的是:即便在你的編譯器或所用的標準庫中 contains() 不可用,透過 find() 的邏輯也能輕易表達相同的檢查。
透過 find() 的檢查(通用)
#include <iostream>
#include <string>
int main() {
std::string filename = "report_2026.txt";
bool has_year = filename.find("2026") != std::string::npos;
std::cout << has_year << '\n'; // 1
}
若可用 contains()
在 C++23 中,std::string::contains() 通常已可用(視標準庫實作而定)。它做的事與上面等價,但可讀性更好:「字串包含這個」。
#include <iostream>
#include <string>
int main() {
std::string filename = "report_2026.txt";
// 如果你的標準函式庫支援 C++23:
std::cout << filename.contains("2026") << '\n'; // 1
}
如果這段程式無法編譯——那不是「你把 C++ 弄壞了」。代表該標準庫/編譯器尚未加入 contains() 的支援,或專案不是用 C++23 模式。這種情況下就安心使用 find() != npos 的老方法。
3. 擷取子字串:substr()
現在一個合理的問題:我們找到位置後,如何取得字串的一部分?例如從 email 取出網域,或取得不含副檔名的檔名。
為此可以使用 substr()。
substr() 主要有兩種用法:
- s.substr(pos) — 從 pos 取到字串結尾;
- s.substr(pos, count) — 從 pos 開始取 count 個字元。
關鍵在於:第二個參數是長度,而不是「結束的索引」。新手常把它弄混,因為直覺會想成「從 pos 到 count 的區間」,但函式庫的想法是:「從 pos 開始,取 count 個字元」。
示意:索引與 substr()
取字串:
email = "alice@example.com"
0123456789...
若 at = 5(@ 的位置),則網域從 at + 1 開始,也就是索引 6:
email.substr(6) → "example.com"
再舉需要第二個參數的例子。想要取出 @ 之前的名字:
email.substr(0, at) — 從索引 0 開始取 at 個字元 → "alice"
小範例:從 email 擷取網域
#include <iostream>
#include <string>
int main() {
std::string email = "alice@example.com";
std::size_t at = email.find('@'); // "at" 是指「at 符號」
if (at != std::string::npos) {
std::string name = email.substr(0, at);
std::string domain = email.substr(at + 1); // 跳過 at 符號
std::cout << name << '\n'; // alice
std::cout << domain << '\n'; // example.com
}
}
注意邊界
如果 pos 大於 s.size(),則 substr(pos, ...) 會導致執行期錯誤(通常會拋出 std::out_of_range 例外,但這裡我們不深入例外處理 —— 只記住:不要這樣做)。
因此一個良好的風格是:pos 通常由 find() 得到,我們事先檢查 pos != npos。這不是多餘的檢查,而是避免夜間惡夢的保險。
4. 檢查開頭與結尾:starts_with() 與 ends_with()
搜尋很好,但有時我們不需要「任意位置」的搜尋。某些字串格式有明確結構:例如必須以 "http://" 開頭、以 ".txt" 結尾,或命令必須以 "add " 開頭。
對這類情況有 starts_with() 與 ends_with()。這些方法被加入標準函式庫,作為對字串結構檢查的增強支援。
為何這比用 find() 更方便?因為可讀性:starts_with("cmd:") 看起來就是在檢查格式,而不是「搜尋某處有沒有東西」。
小範例:檢查副檔名與前綴
#include <iostream>
#include <string>
int main() {
std::string filename = "report_2026.txt";
std::cout << filename.starts_with("report_") << '\n'; // 1
std::cout << filename.ends_with(".txt") << '\n'; // 1
}
能否用 find() 做同樣的事?
可以,例如:
- starts_with(prefix) 等價於檢查 s.find(prefix) == 0 (但要小心:若未找到 find() 會回傳 npos);
- ends_with(suffix) 也可以用索引手動實作,但會稍微長一些。
重點是:我們不只是省下幾個字元。使用專門的方法可以讓檢查更明確。一個月後你再看程式碼時,ends_with(".txt") 會直接被理解,無需額外推敲。
5. 小專案:「TextInspector」
為了把上面內容不是停留在零散範例,我們做一個小型主控台應用:讀入一行字串,做幾項檢查。我們不做完整的命令解析(那會在之後的詞彙切分章節),但已足以從文字中抽出基本意義。
想法很簡單:使用者輸入一行,程式嘗試判斷這行文字像是:
- email(如果包含 @);
- 檔名(如果以 ".txt" 結尾);
- 類 URL 的字串(如果以 "http://" 或 "https://" 開頭)。
骨架:讀取字串並輸出基本回應
#include <iostream>
#include <string>
int main() {
std::string s;
std::getline(std::cin, s);
std::cout << "len=" << s.size() << '\n'; // 範例: len=17
std::cout << "has @: " << (s.find('@') != std::string::npos) << '\n';
}
若看起來像 email,擷取網域
#include <iostream>
#include <string>
int main() {
std::string s;
std::getline(std::cin, s);
std::size_t at = s.find('@');
if (at != std::string::npos && at + 1 < s.size()) {
std::string domain = s.substr(at + 1);
std::cout << "domain=[" << domain << "]\n"; // domain=[example.com]
} else {
std::cout << "not an email\n";
}
}
這裡我們加入了一個重要的小檢查: at + 1 < s.size()。 它可以保護像 "alice@" 這種情況:有 @,但後面沒有網域。
用 ends_with() 檢查檔案副檔名
#include <iostream>
#include <string>
int main() {
std::string s;
std::getline(std::cin, s);
if (s.ends_with(".txt")) {
std::cout << "looks like a text file\n"; // looks like a text file
} else {
std::cout << "not .txt\n";
}
}
用 starts_with() 檢查是否為 URL 類型
#include <iostream>
#include <string>
int main() {
std::string s;
std::getline(std::cin, s);
bool http = s.starts_with("http://") || s.starts_with("https://");
std::cout << "url-like: " << http << '\n'; // url-like: 1
}
將所有檢查組合在一起
程式碼仍然很短,但已經像一個小應用,而不是一組實驗:
#include <iostream>
#include <string>
int main() {
std::string s;
std::getline(std::cin, s);
std::cout << "len=" << s.size() << '\n';
if (s.starts_with("http://") || s.starts_with("https://")) {
std::cout << "type=url\n";
} else if (s.find('@') != std::string::npos) {
std::size_t at = s.find('@');
std::cout << "type=email, domain=" << s.substr(at + 1) << '\n';
} else if (s.ends_with(".txt")) {
std::cout << "type=text-file\n";
} else {
std::cout << "type=unknown\n";
}
}
請注意良好的「可讀」檢查順序:先檢查字串開頭(這樣既快又清楚),再檢查是否包含標記 @,然後檢查是否以 ".txt" 結尾。實際任務中,條件順序是設計的一部分:你決定哪些格式更重要,以及如何處理模糊情況。
6. 使用 find()、substr()、starts_with()、ends_with()、contains() 時的常見錯誤
錯誤 1:把 find() 的結果與 -1 比較。
這種做法有時是因為習慣自其他語言或舊範例。在 C++ 中 find() 回傳 std::size_t,「沒找到」用 std::string::npos 表示。與 -1 比較看似「差不多能用」,但實際上是錯誤的契約,日後很容易出錯或難以移植。
錯誤 2:使用來自 find() 的位置,卻沒檢查 npos。
一開始你可能覺得「我確定那個字元會存在」。然後使用者輸入沒有 @ 的字串,程式就會嘗試做 substr(npos + 1) 而發生執行期錯誤。如果位置來自 find(),正確流程是:先 if (pos != std::string::npos), 然後才做任何算術或呼叫 substr()。
錯誤 3:把 substr(pos, count) 的第二個參數當成「結束索引」。
這是最常見的陷阱之一。substr() 的第二個參數表示「長度」,也就是要取多少字元,而不是「取到哪個索引」。因此像 substr(begin, end) 的寫法常常是錯的;正確的方式是: substr(begin, end - begin), 如果 end 是範圍結束的位置。
錯誤 4:忘了 starts_with()/ends_with() 是檢查結構,不是搜尋。
有時新手嘗試用 starts_with() 取代所有搜尋,然後困惑於為何 "abcXdef" 並不會 starts_with("X")。這是正常的:starts_with() 用來檢查前綴,而「包含」的檢查應使用 contains() 或 find() != npos。選擇方法時,心裡要清楚說出任務:是在開頭?在結尾?還是任意位置?
錯誤 5:把 contains() 當成必要條件,且遇到沒有它的環境就慌張。
即使在 C++23,有些環境的標準函式庫仍未完全更新。這不代表要重寫整個專案;「是否包含」總可以用 find(...) != std::string::npos 來替代。如果 contains() 存在就更好,可讀性更佳;若不存在,則使用經過驗證的老方法。
GO TO FULL VERSION