4. string容器(上):string的构造与赋值、string的拼接与查找、string的替换与比较
大家好,欢迎来到string容器的上半部分。说实话,string在C++里是个很有意思的存在——它既不是基本类型,也不是纯粹的STL容器,但几乎每个项目都离不开它。我个人习惯把string看作是「带智能管理的字符数组」,你想想看,它帮我们处理了内存分配、边界检查这些烦心事,让我们能专注于业务逻辑。
这一章我们重点聊四个核心操作:构造与赋值、拼接与查找、替换与比较。嗯,这些都是日常编码中高频使用的功能,我会结合自己踩过的坑来讲解。
核心提示:string的本质是basic_string<char>的typedef,它管理一块动态内存,支持自动扩容。理解这一点,很多行为就说得通了。
4.1 string的构造与赋值
string的构造方式比你想的要多。我刚开始用C++时,总觉得string就是"hello world"这么简单,直到在项目中遇到各种初始化场景,才发现这里门道不少。
4.1.1 常用构造函数
#include <iostream>
#include <string>
using namespace std;
int main() {
// 1. 默认构造
string s1;
cout << "s1: [" << s1 << "]" << endl; // 空字符串
// 2. 用C风格字符串构造
string s2("hello");
cout << "s2: " << s2 << endl;
// 3. 用n个字符c构造
string s3(5, 'A');
cout << "s3: " << s3 << endl; // "AAAAA"
// 4. 拷贝构造
string s4(s2);
cout << "s4: " << s4 << endl;
// 5. 子串构造(从位置pos开始,取len个字符)
string s5("hello world", 6, 5);
cout << "s5: " << s5 << endl; // "world"
// 6. 用迭代器范围构造
string s6(s2.begin(), s2.end());
cout << "s6: " << s6 << endl;
return 0;
}
这里有个细节我想强调一下:子串构造的第二个参数是起始位置,第三个参数是长度。我曾经在代码里把顺序搞反了,结果调试了半天才发现是参数传错了。嗯,这种低级错误,犯过一次就记住了。
4.1.2 赋值操作
赋值操作同样有多种形式,我个人最常用的是operator=和assign方法。
string s1, s2, s3;
// 1. 用operator=
s1 = "hello";
s2 = s1;
// 2. 用assign方法
s3.assign("world");
s3.assign("hello world", 5); // 取前5个字符 -> "hello"
s3.assign(3, 'X'); // "XXX"
// 3. 用C风格字符串赋值(注意:不推荐,容易越界)
// char buf[] = "test";
// s3 = buf; // 可以,但buf必须是'\0'结尾
我的建议:日常开发中优先使用operator=,代码更简洁。只有在需要指定长度或重复字符时,才用assign。另外,尽量避免用C风格字符串和string混用,容易出bug。
4.2 string的拼接与查找
拼接和查找是string最常用的两个操作。我在做日志系统时,经常需要拼接大量字符串,那时候对性能特别敏感。
4.2.1 拼接操作
拼接有三种主要方式:operator+=、append和push_back。你猜哪个效率最高?
string s = "hello";
// 1. operator+=
s += " world"; // "hello world"
s += '!'; // "hello world!"
// 2. append方法
s.append(" C++"); // "hello world! C++"
s.append(" is fun", 2); // 追加"is"的前2个字符 -> "is"
s.append(3, '.'); // 追加3个'.' -> "..."
// 3. push_back(只能追加单个字符)
s.push_back('!'); // 追加一个感叹号
cout << s << endl; // "hello world! C++ is..."
说实话,operator+=和append在底层实现上差别不大,但append更灵活——你可以指定追加的长度或重复次数。我在项目中遇到过一个问题:用+=拼接大量短字符串时,频繁的内存重分配导致性能下降。后来改用append配合reserve预分配空间,性能提升很明显。
避坑指南:千万不要在循环里用s = s + "xxx"这种方式拼接!每次都会创建临时对象,效率极低。我曾经在一个网络消息处理模块里看到这种写法,每秒处理的消息数直接掉了30%。正确的做法是用+=或append,或者先reserve足够的容量。
4.2.2 查找操作
查找是string里最容易出bug的地方之一。为什么?因为查找失败时的返回值是npos,而不是-1。很多新手会写成if (s.find("xxx") == -1),这在某些平台上能工作,但严格来说是未定义行为。
string s = "hello world, hello C++";
// 1. find:从前往后找
size_t pos = s.find("hello");
if (pos != string::npos) {
cout << "找到hello,位置: " << pos << endl; // 0
}
// 2. rfind:从后往前找
pos = s.rfind("hello");
if (pos != string::npos) {
cout << "从后找hello,位置: " << pos << endl; // 13
}
// 3. find_first_of:查找第一个匹配的字符
pos = s.find_first_of("ow");
cout << "第一个'o'或'w'的位置: " << pos << endl; // 4('o')
// 4. find_last_of:查找最后一个匹配的字符
pos = s.find_last_of("ow");
cout << "最后一个'o'或'w'的位置: " << pos << endl; // 16('o')
// 5. find_first_not_of:查找第一个不匹配的字符
pos = s.find_first_not_of("helo ");
cout << "第一个不是'helo '的字符位置: " << pos << endl; // 5('w')
这里有个小技巧:查找时一定要用string::npos做比较。我见过有人用if (pos < 0)来判断,这在64位系统上会出问题,因为size_t是无符号类型,永远不小于0。
重要:string::npos的值是size_t(-1),也就是最大的无符号整数。所以判断查找失败的正确写法是:if (pos == string::npos)。
4.3 string的替换与比较
替换和比较操作,说白了就是「改内容」和「比大小」。这两个功能在文本处理、配置解析等场景中非常常见。
4.3.1 替换操作
replace方法有很多重载版本,我挑几个最常用的来说。
string s = "I like C++";
// 1. 替换指定位置的子串
// replace(pos, len, str):从pos开始,替换len个字符为str
s.replace(2, 4, "love"); // "I love C++"
cout << s << endl;
// 2. 用迭代器范围替换
string s2 = "hello world";
s2.replace(s2.begin(), s2.begin() + 5, "hi");
cout << s2 << endl; // "hi world"
// 3. 替换为重复字符
string s3 = "*****";
s3.replace(0, 3, 3, 'A'); // 将前3个*替换为3个A
cout << s3 << endl; // "AAA**"
// 4. 用另一个字符串的子串替换
string s4 = "I like C++";
string src = "I love Java";
s4.replace(2, 4, src, 2, 4); // 从src的索引2开始取4个字符 -> "love"
cout << s4 << endl; // "I love C++"
替换操作有个容易忽略的点:替换的长度和原长度不一致时,string会自动调整大小。这意味着替换后字符串的长度可能会变化。我在做文本模板替换时,就利用这个特性实现了动态内容插入。
4.3.2 比较操作
string的比较支持operator==、operator!=、operator<等,也提供了compare方法。我个人更推荐用compare,因为它能指定比较的范围,更灵活。
string s1 = "apple";
string s2 = "banana";
// 1. 用operator比较
if (s1 < s2) {
cout << "apple < banana" << endl; // 字典序比较
}
// 2. 用compare方法
int result = s1.compare(s2);
if (result < 0) {
cout << "s1 < s2" << endl;
} else if (result > 0) {
cout << "s1 > s2" << endl;
} else {
cout << "s1 == s2" << endl;
}
// 3. 比较子串
string s3 = "I have an apple";
string s4 = "apple pie";
// 比较s3从索引7开始的5个字符与s4的前5个字符
result = s3.compare(7, 5, s4, 0, 5);
if (result == 0) {
cout << "子串相等" << endl; // 输出这个
}
// 4. 与C风格字符串比较
result = s1.compare("apple");
if (result == 0) {
cout << "s1 == apple" << endl;
}
我的经验:在需要忽略大小写比较时,不要直接用compare,因为它区分大小写。我一般会先把两个字符串都转成小写(用std::transform配合tolower),然后再比较。或者直接用C++20的starts_with、ends_with,这些新特性用起来很舒服。
4.4 知识体系总览
为了让你对本章内容有个整体认识,我画了一张结构图。你看,string的操作其实围绕「创建-修改-查询-比较」这条主线展开。
这张图把本章的知识点串起来了。你看,构造与赋值是基础,拼接与查找是日常高频操作,替换和比较则是进阶功能。掌握了这些,string的基本用法你就拿下了。
本章小结:
- 构造方式多样,推荐用
string s("hello")或string s(n, 'c') - 赋值用
operator=最简洁,assign更灵活 - 拼接避免
s = s + x,用+=或append - 查找必须用
string::npos判断失败 - 替换注意长度变化,比较推荐用
compare
公众号:蓝海资料掘金营,微信deep3321