1. 为什么需要专门练习string类题目
在C++编程中,string类可以说是使用频率最高的标准库组件之一。无论是算法竞赛、面试笔试还是实际项目开发,string相关的操作和问题都无处不在。但很多初学者往往低估了string类的复杂性,认为它只是"存储字符的容器"而已。
实际上,string类在C++标准库中的实现相当复杂,它提供了超过100个成员函数,涵盖了字符串操作的方方面面。从简单的查找、替换,到复杂的正则匹配、编码转换,string类都能胜任。更关键的是,string类与C风格字符串(char*)的互操作,以及其内部的内存管理机制,都是面试和实际项目中经常考察的重点。
我见过太多候选人,在面试中被要求实现一个简单的字符串分割函数时,却因为对string类接口不熟悉而手忙脚乱。也有不少项目因为不当的字符串操作导致内存泄漏或性能问题。这些问题都可以通过系统的string类专项练习来避免。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. string类核心知识点梳理
2.1 string类的基本特性
C++的string类本质上是一个模板类basic_string的特化版本:
cpp复制typedef basic_string<char> string;
它有几个重要特性需要掌握:
- 自动内存管理:string对象会自动处理内存分配和释放,无需手动管理
- 可变长度:与C风格字符串不同,string的长度可以动态变化
- 丰富的接口:提供查找、修改、比较、迭代等多种操作
- 与C风格字符串兼容:可以通过c_str()方法获取C风格字符串
2.2 string类的常用操作
string类的操作可以分为几大类:
构造与赋值
cpp复制string s1; // 空字符串
string s2("hello"); // 从C字符串构造
string s3(s2); // 拷贝构造
string s4(5, 'a'); // 5个'a'
s1 = "world"; // 赋值操作
元素访问
cpp复制s[0] = 'H'; // 通过下标访问(不检查边界)
s.at(1) = 'E'; // 通过at访问(会检查边界)
char c = s.front(); // 首字符
char c = s.back(); // 尾字符
容量操作
cpp复制s.size(); // 当前字符数
s.length(); // 同size()
s.capacity(); // 当前分配的内存大小
s.reserve(100); // 预分配内存
s.shrink_to_fit(); // 减少内存占用
修改操作
cpp复制s += " world"; // 追加
s.append("!!!"); // 同+=
s.insert(5, " dear"); // 在指定位置插入
s.erase(5, 5); // 删除子串
s.replace(6, 5, "C++"); // 替换子串
s.clear(); // 清空字符串
字符串操作
cpp复制s.substr(6, 3); // 获取子串
s.compare("hello"); // 比较字符串
s.find("ll"); // 查找子串
s.rfind("l"); // 反向查找
s.find_first_of("aeiou"); // 查找任意匹配字符
3. string类常见面试题解析
3.1 字符串反转
这是最基础的string操作题,通常有以下几种实现方式:
cpp复制// 方法1:使用algorithm中的reverse
string reverse1(string s) {
reverse(s.begin(), s.end());
return s;
}
// 方法2:双指针法
string reverse2(string s) {
int left = 0, right = s.size() - 1;
while (left < right) {
swap(s[left++], s[right--]);
}
return s;
}
// 方法3:使用栈
string reverse3(string s) {
stack<char> st;
for (char c : s) st.push(c);
for (int i = 0; i < s.size(); i++) {
s[i] = st.top();
st.pop();
}
return s;
}
注意:面试时可能会限制不能使用标准库算法,因此方法2是必须掌握的。
3.2 字符串转整数
实现atoi函数是经典面试题,需要考虑多种边界情况:
cpp复制int myAtoi(string s) {
int i = 0, sign = 1, res = 0;
// 跳过前导空格
while (i < s.size() && s[i] == ' ') i++;
// 处理符号
if (i < s.size() && (s[i] == '+' || s[i] == '-')) {
sign = (s[i++] == '+') ? 1 : -1;
}
// 转换数字
while (i < s.size() && isdigit(s[i])) {
int digit = s[i] - '0';
// 处理溢出
if (res > INT_MAX / 10 || (res == INT_MAX / 10 && digit > INT_MAX % 10)) {
return sign == 1 ? INT_MAX : INT_MIN;
}
res = res * 10 + digit;
i++;
}
return sign * res;
}
3.3 最长无重复字符子串
滑动窗口法的经典应用:
cpp复制int lengthOfLongestSubstring(string s) {
unordered_map<char, int> lastSeen;
int start = 0, maxLen = 0;
for (int end = 0; end < s.size(); end++) {
char c = s[end];
if (lastSeen.count(c) && lastSeen[c] >= start) {
start = lastSeen[c] + 1;
}
lastSeen[c] = end;
maxLen = max(maxLen, end - start + 1);
}
return maxLen;
}
4. string类的高阶应用
4.1 KMP算法实现
KMP算法是字符串匹配的高效算法,理解next数组的构建是关键:
cpp复制vector<int> buildNext(const string& pattern) {
vector<int> next(pattern.size(), 0);
int j = 0;
for (int i = 1; i < pattern.size(); i++) {
while (j > 0 && pattern[i] != pattern[j]) {
j = next[j - 1];
}
if (pattern[i] == pattern[j]) {
j++;
}
next[i] = j;
}
return next;
}
int kmpSearch(const string& text, const string& pattern) {
vector<int> next = buildNext(pattern);
int j = 0;
for (int i = 0; i < text.size(); i++) {
while (j > 0 && text[i] != pattern[j]) {
j = next[j - 1];
}
if (text[i] == pattern[j]) {
j++;
}
if (j == pattern.size()) {
return i - j + 1;
}
}
return -1;
}
4.2 正则表达式匹配
实现简易版正则表达式匹配器:
cpp复制bool isMatch(string s, string p) {
int m = s.size(), n = p.size();
vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
dp[0][0] = true;
for (int j = 1; j <= n; j++) {
if (p[j - 1] == '*') {
dp[0][j] = dp[0][j - 2];
}
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (p[j - 1] == '.' || p[j - 1] == s[i - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else if (p[j - 1] == '*') {
dp[i][j] = dp[i][j - 2];
if (p[j - 2] == '.' || p[j - 2] == s[i - 1]) {
dp[i][j] = dp[i][j] || dp[i - 1][j];
}
}
}
}
return dp[m][n];
}
5. string类的性能优化技巧
5.1 避免不必要的拷贝
string的拷贝可能触发内存分配,影响性能:
cpp复制// 不好的写法 - 可能触发拷贝
void processString(string s) {
// ...
}
// 好的写法 - 传const引用
void processString(const string& s) {
// ...
}
// 如果需要修改,可以传引用
void modifyString(string& s) {
// ...
}
5.2 使用reserve预分配内存
频繁的字符串拼接会导致多次内存分配:
cpp复制string result;
// 预先分配足够空间
result.reserve(1000);
for (int i = 0; i < 100; i++) {
result += "some data ";
}
5.3 使用move语义
对于临时string对象,使用move避免拷贝:
cpp复制string createString() {
string s(1000, 'a');
return s; // 编译器会自动优化为move
}
string s1 = createString(); // 不会发生拷贝
string s2 = "hello";
string s3 = move(s2); // s2现在为空
6. string类在实际项目中的应用
6.1 配置文件解析
解析key=value格式的配置文件:
cpp复制unordered_map<string, string> parseConfig(const string& filename) {
unordered_map<string, string> config;
ifstream file(filename);
string line;
while (getline(file, line)) {
// 跳过注释和空行
if (line.empty() || line[0] == '#') continue;
size_t pos = line.find('=');
if (pos != string::npos) {
string key = line.substr(0, pos);
string value = line.substr(pos + 1);
// 去除前后空格
key.erase(0, key.find_first_not_of(" \t"));
key.erase(key.find_last_not_of(" \t") + 1);
value.erase(0, value.find_first_not_of(" \t"));
value.erase(value.find_last_not_of(" \t") + 1);
config[key] = value;
}
}
return config;
}
6.2 日志处理系统
实现简单的日志级别过滤:
cpp复制class Logger {
public:
enum Level { DEBUG, INFO, WARNING, ERROR };
void log(Level level, const string& message) {
if (level < currentLevel) return;
string prefix;
switch (level) {
case DEBUG: prefix = "[DEBUG] "; break;
case INFO: prefix = "[INFO] "; break;
case WARNING: prefix = "[WARNING] "; break;
case ERROR: prefix = "[ERROR] "; break;
}
string formatted = prefix + getCurrentTime() + " " + message;
writeToFile(formatted);
}
private:
Level currentLevel = INFO;
string getCurrentTime() {
time_t now = time(nullptr);
char buf[20];
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", localtime(&now));
return string(buf);
}
void writeToFile(const string& message) {
ofstream file("app.log", ios::app);
file << message << endl;
}
};
7. string类常见陷阱与解决方案
7.1 迭代器失效问题
string修改操作可能导致迭代器失效:
cpp复制string s = "hello";
auto it = s.begin() + 2;
s.insert(s.begin(), 'X'); // 插入操作可能导致it失效
// 此时使用it是未定义行为
解决方案:
- 在修改后重新获取迭代器
- 使用索引而非迭代器
- 先收集所有修改位置,再反向处理
7.2 多字节字符处理
string按字节处理,不适合直接处理UTF-8等多字节编码:
cpp复制string s = "你好"; // UTF-8编码
cout << s.size(); // 输出6而非2
解决方案:
- 使用专门的库如ICU
- 使用C++20的char8_t和u8string
- 对于简单需求,可以自行实现UTF-8处理
7.3 内存碎片问题
频繁修改大字符串可能导致内存碎片:
cpp复制string s;
for (int i = 0; i < 10000; i++) {
s += "some data "; // 可能导致多次重新分配
s.erase(0, 5); // 可能导致内存不释放
}
解决方案:
- 使用reserve预分配足够空间
- 考虑使用deque
或自定义缓冲区 - 定期使用shrink_to_fit
8. 推荐练习题目
为了系统掌握string类的使用,建议按以下顺序练习:
-
基础操作题
- 字符串反转
- 验证回文串
- 字符串转整数
- 最长公共前缀
-
中级算法题
- 最长无重复字符子串
- 字符串的排列组合
- 字符串相乘
- 字母异位词分组
-
高级算法题
- 正则表达式匹配
- 通配符匹配
- 最小覆盖子串
- 不同的子序列
-
实际应用题
- CSV文件解析
- 简单模板引擎实现
- Markdown标题解析
- URL解析与构建
在实际练习中,我发现很多看似简单的字符串问题其实暗藏陷阱。比如实现split函数时,需要考虑连续分隔符、开头结尾分隔符等情况;处理字符串数字转换时,必须仔细处理溢出和非法输入。这些经验只有在大量练习后才能积累。
