std::string:终于不用伺候 \0 了
在上一篇教程里,咱们花了大把篇幅和 C 风格字符串搏斗:手动管理 \0 终止符、小心翼翼地防止缓冲区溢出、用 strncpy 和 snprintf 如履薄冰地操作每一段字符数组。您要是跟笔者一样被这些折腾得够呛,那接下来这个消息会让您松一大口气:C++ 标准库给咱们准备了一个真正的字符串类型,叫做 std::string,它自动管理内存、自动处理长度、支持直观的拼接和比较,基本上把咱们之前在 C 里踩过的坑全部填平了。
这一章咱们从 std::string 的构造方式开始,一路走到拼接、查找、子串提取、与 C 字符串的互操作,最后用一个综合的字符串处理程序把所有知识串起来。学完之后您会发现,以前那些让人血压拉满的字符串操作(笔者就是,最开始学会std::string后有时候反而用不明白C String了),在 C++ 里可以写得既安全又省事。
构造一个 string 的各种方式
std::string 提供了相当丰富的构造函数,覆盖了您能想到的几乎所有场景:
展开代码收起代码共 26 行
// string_construct.cpp
#include <iostream>
#include <string>
int main()
{
// 从字面量构造
std::string s1 = "hello";
// 重复字符:10 个 'x'
std::string s2(10, 'x');
// 拷贝构造
std::string s3(s1);
// 从另一个 string 的一部分构造(起始位置,长度)
std::string s4(s1, 1, 3); // "ell"
// 用 + 直接拼接构造
std::string s5 = s1 + " world";
// 空字符串
std::string s6;
// 移动构造(C++11)
std::string s7 = std::move(s5);
std::cout << s1 << "\n" << s2 << "\n" << s3 << "\n"
<< s4 << "\n" << s7 << "\n"
<< "s6 empty: " << std::boolalpha << s6.empty() << "\n";
return 0;
}输出:
hello
xxxxxxxxxx
hello
ell
hello world
s6 empty: true第一种和第五种写法看起来像赋值,但实际上编译器做的是构造,这是 C++ 的拷贝初始化语法,效果和 std::string s1("hello") 一样。std::string s4(s1, 1, 3) 从 s1 的下标 1 开始截取 3 个字符,结果是 "ell",这种"部分构造"在解析字符串的时候非常好用。移动构造咱们暂时不用深究,只需要知道它比拷贝更快,因为它把内部资源"偷"了过来而不是复制一份。
移动之后的源对象(上面的 s5)处于"有效但未指定"(valid but unspecified)的状态——咱们可以对它赋值、可以析构它,但不要读取它的值做任何有意义的判断。这是 C++ 移动语义的基本契约,后续章节讲到移动语义的时候咱们会详细展开。
基本操作:大小、访问和判空
std::string s = "Hello, C++";
s.size(); // 10
s.length(); // 10(和 size 等价)
s.empty(); // false
s[0]; // 'H'
s.at(1); // 'e'(越界时抛 std::out_of_range)
s.front(); // 'H'
s.back(); // '+'size() 和 length() 完全等价。大多数 C++ 开发者倾向于 size(),因为它和其他标准库容器保持一致,咱们也照这个惯例来。
operator[] 和 at() 都能通过下标访问字符,区别在于越界行为:s[100] 不做任何检查,行为完全未定义;s.at(100) 则会抛出 std::out_of_range 异常。您要是没有百分之百的把握,用 at() 更安全——比起花两小时追查一个内存越界的 bug,这点性能开销根本不算什么。
std::string 的 size() 返回的是底层 char 的个数,不是"肉眼看到的字数"。对于纯 ASCII 字符串两者一致,但如果字符串里包含 UTF-8 编码的中文,std::string s = "你好"; 的 s.size() 是 6 而不是 2,因为每个中文字符占 3 个字节。正确处理 Unicode 字符串需要专门的库(比如 ICU),但这个坑咱们一定要提前知道。
拼接、插入、删除与替换
std::string s = "Hello";
s += " World"; // "Hello World"
s.append("!!!"); // "Hello World!!!"
s.push_back('?'); // "Hello World!!!?"
s.insert(5, ","); // "Hello, World!!!?"
s.erase(5, 1); // "Hello World!!!?" 删掉刚才插入的逗号
s.replace(6, 5, "C++"); // "Hello C++!!!?" World -> C++
s.clear(); // 变成空字符串+= 和 append() 功能类似,咱们平时用 += 更多,append() 提供更多重载版本(比如只追加另一个 string 的某一段)。push_back() 只能追加单个字符,和 vector 的 push_back() 接口一致。insert(pos, str) 在 pos 处插入 str,erase(pos, len) 从 pos 开始删除 len 个字符,replace(pos, len, new_str) 把从 pos 开始的 len 个字符替换成 new_str,新字符串的长度可以和被替换的部分不同。
这些操作之所以安全,是因为 std::string 内部自动管理内存:插入时空间不够会自动扩容,删除时不需要手动移动后面的字符。相比 C 里手动算偏移量、小心翼翼调用 memmove,这些操作咱们用起来放心得多。
查找与子串
std::string s = "Hello, hello, HELLO!";
s.find("hello"); // 7(区分大小写)
s.find("Hello"); // 0
s.find("xyz"); // std::string::npos
s.find("hello", 2); // 7(从位置 2 开始找)
s.rfind("hello"); // 7(反向查找)
s.find_first_of("aeiou"); // 1(第一个元音字母 e)
s.find_last_of("aeiou"); // 11(区分大小写,最后命中的是第二个 hello 里的 o)这里最关键的概念是 std::string::npos。它是一个常量,值是 std::size_t 的最大值。当查找操作没找到目标时返回 npos。所以咱们每次调用 find 之后,都要检查返回值是否等于 npos,而不是拿它当 bool 用——因为 npos 转换成 bool 是 true,直接写 if (s.find("x")) 在没找到的时候反而进入分支,这是另一个经典的新手陷阱。
咱们要注意 find_first_of 和 find_last_of 的行为比较特殊:它们不是查找整个子串,而是查找参数字符串中的任意一个字符。find_first_of("aeiou") 返回 1,因为 s[1] 是 'e',是 "aeiou" 中最先匹配到的字符。
| 写法 | 函数意思 | 找到时返回什么 |
|---|---|---|
s.find("abc") | 找连续、顺序相同的完整 "abc" | 这段字符串的起始下标 |
s.find('a') | 找字符 'a' | 第一个 'a' 的下标 |
s.find_first_of("abc") | 找 a/b/c 中任意一个 | 第一个属于这个集合的字符下标 |
s.find_first_not_of("abc") | 找第一个不是 a/b/c 的字符 | 第一个不属于这个集合的字符下标 |
咱们看到这几种写法都从左向右查找,默认从下标 0 开始,只返回第一个符合条件的位置。它们不会修改原字符串。
子串提取咱们用 substr(pos, len),从位置 pos 开始截取 len 个字符,返回一个新的 std::string。省略 len 则取到末尾:
std::string t = "Hello, World!";
t.substr(7, 5); // "World"
t.substr(7); // "World!"substr() 返回的是一个新对象,会分配内存并拷贝字符。您要是只需要遍历某个范围而不需要独立的副本,用 std::string_view(C++17)会更高效——这个咱们在后续章节再展开。
比较字符串
咱们在 C 里比较两个字符串得用 strcmp,C++ 的 std::string 重载了比较运算符,直观得多:
std::string a = "apple", b = "banana", c = "apple";
a == c; // true
a != b; // true
a < b; // true(字典序)
a.compare(b); // 负数(等价于 strcmp 的返回值语义)compare() 成员函数的优势在于支持部分比较,例如 s.compare(7, 5, "World") 拿 s 从下标 7 开始的 5 个字符和 "World" 比较是否相等。咱们会在解析协议、处理固定格式文本时用到这种能力。
与 C 字符串互操作
不管 std::string 有多好用,很多第三方库、操作系统 API、嵌入式 SDK 依然接受 const char*。咱们从 std::string 拿到 C 风格字符串需要两个关键函数:
std::string s = "Hello, C API!";
const char* p = s.c_str(); // 返回以 \0 结尾的 const char*
const char* q = s.data(); // C++17 起与 c_str() 完全等价c_str() 保证返回以 \0 结尾的 const char*,咱们可以直接传给 fopen、printf 等任何期望 C 字符串的函数。data() 在 C++17 起行为和 c_str() 完全一致。
这里有一条必须牢记的规则:c_str() 和 data() 返回的指针由 string 对象持有,一旦 string 被修改或销毁,指针就失效了。所以咱们永远不要把 c_str() 的返回值存下来然后去做可能改变 string 的操作——先完成所有修改,最后再调用 c_str() 传给 C API。
数值转换与行输入
// 数值 -> 字符串
std::to_string(42); // "42"
std::to_string(3.14); // "3.140000"(注意:用的是 %f 格式)
// 字符串 -> 数值
std::stoi("42"); // int: 42
std::stol("1234567890"); // long: 1234567890
std::stod("3.14159"); // double: 3.14159
std::stoi(" 123abc"); // 123(跳过前导空白,遇非数字停止)
// 读取一整行(cin >> s 遇空格就停,getline 会读到换行为止)
std::string line;
std::getline(std::cin, line);std::to_string 对浮点数的结果可能不太"漂亮":to_string(3.14) 输出 3.140000,因为它用的是 %f 格式化。您要是需要精确控制浮点数的输出格式,还是得用 <iomanip> 里的 std::setprecision 或者 std::snprintf。
实战演练——综合字符串处理
咱们把前面学到的所有知识综合起来,写一个稍微有实际意义的字符串处理程序。这个程序演示几种常见的文本处理模式:按分隔符拆分、统计字符频率、查找替换、以及简单的 CSV 解析。
展开代码收起代码共 82 行
// string_demo.cpp
#include <iostream>
#include <map>
#include <string>
/// @brief 把句子按空格拆分成单词,输出每个单词
void split_into_words(const std::string& sentence)
{
std::cout << "--- 拆分单词 ---" << std::endl;
std::size_t start = 0;
std::size_t end = 0;
while (start < sentence.size()) {
start = sentence.find_first_not_of(' ', start);
if (start == std::string::npos) {
break;
}
end = sentence.find(' ', start);
if (end == std::string::npos) {
end = sentence.size();
}
std::cout << " [" << sentence.substr(start, end - start) << "]\n";
start = end + 1;
}
}
/// @brief 统计每个字符出现的次数(区分大小写)
void count_char_frequency(const std::string& text)
{
std::cout << "\n--- 字符频率统计 ---" << std::endl;
std::map<char, int> freq;
for (char c : text) {
freq[c]++;
}
for (const auto& [ch, count] : freq) {
std::cout << " '" << ch << "': " << count << "\n";
}
}
/// @brief 在 text 中查找所有 target 并替换为 replacement
std::string find_and_replace(std::string text,
const std::string& target,
const std::string& replacement)
{
std::cout << "\n--- 查找替换 ---\n 原文: " << text << std::endl;
std::size_t pos = 0;
while ((pos = text.find(target, pos)) != std::string::npos) {
text.replace(pos, target.size(), replacement);
pos += replacement.size(); // 跳过已替换部分,避免死循环
}
std::cout << " 结果: " << text << std::endl;
return text;
}
/// @brief 简单的 CSV 行解析(不处理引号转义)
void parse_csv_line(const std::string& line)
{
std::cout << "\n--- CSV 解析 ---\n 输入: " << line << std::endl;
std::size_t start = 0;
int idx = 0;
while (true) {
std::size_t comma = line.find(',', start);
if (comma == std::string::npos) {
std::cout << " 字段 " << idx << ": [" << line.substr(start)
<< "]\n";
break;
}
std::cout << " 字段 " << idx << ": ["
<< line.substr(start, comma - start) << "]\n";
start = comma + 1;
idx++;
}
}
int main()
{
split_into_words("C++ is a powerful and efficient language");
count_char_frequency("hello world");
find_and_replace("the cat sat on the mat", "the", "a");
parse_csv_line("Alice,30,Engineer,New York");
return 0;
}编译运行:
g++ -std=c++17 -Wall -Wextra -o string_demo string_demo.cpp
./string_demo输出:
展开代码收起代码共 29 行
--- 拆分单词 ---
[C++]
[is]
[a]
[powerful]
[and]
[efficient]
[language]
--- 字符频率统计 ---
' ': 1
'd': 1
'e': 1
'h': 1
'l': 3
'o': 2
'r': 1
'w': 1
--- 查找替换 ---
原文: the cat sat on the mat
结果: a cat sat on a mat
--- CSV 解析 ---
输入: Alice,30,Engineer,New York
字段 0: [Alice]
字段 1: [30]
字段 2: [Engineer]
字段 3: [New York]咱们逐个看这几个函数的思路。split_into_words 的核心是反复调用 find_first_not_of 跳过空白、再用 find 定位下一个分隔符,然后 substr 截取单词。这个"跳过空白、找到分隔符、截取、循环"的模式在文本处理中非常常见,建议您当作固定套路来记。
count_char_frequency 用了 std::map 来统计频率。std::map 内部是排序的,所以输出按字符的字典序排列。这里咱们第一次用到了关联容器,不需要理解全部细节,只需要知道它是一个"键-值"对的集合,[] 访问时如果键不存在会自动创建默认值(int 就是 0)。
find_and_replace 展示了一个重要的模式:咱们写循环做 find + replace 时,每次替换完要把搜索起始位置挪到替换结果之后,否则如果 replacement 里包含 target 的内容就会陷入死循环。parse_csv_line 的逻辑和拆分单词类似,只是分隔符换成了逗号。
练习
这三道练习覆盖了 std::string 最核心的操作,建议您自己动手写完再对照思路检查。
练习 1:单词计数器
请您写一个函数 count_words(const std::string& s),统计字符串中有多少个单词(以空格分隔,忽略连续空格和首尾空格)。提示:可以用循环配合 find 和 find_first_not_of,也可以数"从空白到非空白的过渡次数"。
参考答案
展开代码收起代码共 31 行
#include <iostream>
#include <string>
int count_words(const std::string& str)
{
std::cout << "--- 拆分单词并统计单词数量 ---" << std::endl;
std::size_t start = 0;
std::size_t end = 0;
std::size_t count = 0;
while (true) {
start = str.find_first_not_of(" ,.", end);
if (start == std::string::npos) {
break;
}
end = str.find_first_of(" ,.", start);
if (end == std::string::npos) {
end = str.size();
}
count++;
std::cout << " [" << str.substr(start, end - start) << "]\n";
}
return count;
}
int main()
{
std::string str = "Hello, this is a sample string for counting words.";
int word_count = count_words(str);
std::cout << "单词总数: " << word_count << std::endl;
return 0;
}编译运行:
g++ -std=c++17 -Wall -Wextra main.cpp -o main &&./main运行结果:
--- 拆分单词并统计单词数量 ---
[Hello]
[this]
[is]
[a]
[sample]
[string]
[for]
[counting]
[words]
单词总数: 9这一份示例代码在拆分单词时,除了拆掉空格,还会帮咱们忽略
,、.等标点符号
练习 2:简单查找替换工具
请您写一个函数 replace_all(std::string& text, const std::string& from, const std::string& to),把 text 中所有出现的 from 替换为 to。要求处理 from 为空字符串的情况(直接返回原文,否则 find("") 会返回 0 导致死循环)。
参考答案
展开代码收起代码共 24 行
#include <iostream>
#include <string>
void replace_all(std::string& str, const std::string& from, const std::string& to)
{
std::size_t start_pos = 0;
if (from.empty()) {
return;
}
while ((start_pos = str.find(from, start_pos)) != std::string::npos) {
str.replace(start_pos, from.length(), to);
start_pos += to.length();
}
}
int main()
{
const std::string original = "Hello, World! World is beautiful.";
std::string modified = original;
replace_all(modified, "World", "Universe");
std::cout << "Original: " << original << std::endl;
std::cout << "Modified: " << modified << std::endl;
return 0;
}编译运行:
g++ -std=c++17 -Wall -Wextra main.cpp -o main &&./main运行结果:
Original: Hello, World! World is beautiful.
Modified: Hello, Universe! Universe is beautiful.练习 3:trim 函数
请您写两个函数 ltrim 和 rtrim,分别去掉字符串开头和末尾的空白字符(空格、\t、\n),然后组合出一个 trim 函数。提示:ltrim 用 find_first_not_of(" \t\n") 找到第一个非空白字符然后 substr;rtrim 类似,用 find_last_not_of。
参考答案
展开代码收起代码共 33 行
#include <iostream>
#include <string>
void ltrim(std::string& s)
{
std::size_t pos = s.find_first_not_of(" ");
if (pos != std::string::npos) {
s = s.substr(pos);
}
}
void rtrim(std::string& s)
{
std::size_t pos = s.find_last_not_of(" ");
if (pos != std::string::npos) {
s = s.substr(0, pos + 1);
}
}
void trim(std::string& s)
{
ltrim(s);
rtrim(s);
}
int main()
{
std::string str = " Hello, World! ";
std::cout << "原始的: '" << str << "'" << std::endl;
trim(str);
std::cout << "修剪后的: '" << str << "'" << std::endl;
return 0;
}编译运行:
g++ -std=c++17 -Wall -Wextra main.cpp -o main &&./main运行结果:
原始的: ' Hello, World! '
修剪后的: 'Hello, World!'