在C++编程中,string类可以说是使用频率最高的标准库组件之一。无论是算法竞赛、面试笔试还是实际项目开发,string相关的操作和问题都无处不在。但很多初学者往往低估了string类的复杂性,认为它只是"存储字符的容器"而已。
实际上,string类在C++标准库中的实现相当复杂,它提供了超过100个成员函数,涵盖了字符串操作的方方面面。从简单的查找、替换,到复杂的正则匹配、编码转换,string类都能胜任。更关键的是,string类与C风格字符串(char*)的互操作,以及其内部的内存管理机制,都是面试和实际项目中经常考察的重点。
我见过太多候选人,在面试中被要求实现一个简单的字符串分割函数时,却因为对string类接口不熟悉而手忙脚乱。也有不少项目因为不当的字符串操作导致内存泄漏或性能问题。这些问题都可以通过系统的string类专项练习来避免。
C++的string类本质上是一个模板类basic_string的特化版本:
cpp复制typedef basic_string<char> 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"); // 查找任意匹配字符
这是最基础的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是必须掌握的。
实现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;
}
滑动窗口法的经典应用:
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;
}
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;
}
实现简易版正则表达式匹配器:
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];
}
string的拷贝可能触发内存分配,影响性能:
cpp复制// 不好的写法 - 可能触发拷贝
void processString(string s) {
// ...
}
// 好的写法 - 传const引用
void processString(const string& s) {
// ...
}
// 如果需要修改,可以传引用
void modifyString(string& s) {
// ...
}
频繁的字符串拼接会导致多次内存分配:
cpp复制string result;
// 预先分配足够空间
result.reserve(1000);
for (int i = 0; i < 100; i++) {
result += "some data ";
}
对于临时string对象,使用move避免拷贝:
cpp复制string createString() {
string s(1000, 'a');
return s; // 编译器会自动优化为move
}
string s1 = createString(); // 不会发生拷贝
string s2 = "hello";
string s3 = move(s2); // s2现在为空
解析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;
}
实现简单的日志级别过滤:
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;
}
};
string修改操作可能导致迭代器失效:
cpp复制string s = "hello";
auto it = s.begin() + 2;
s.insert(s.begin(), 'X'); // 插入操作可能导致it失效
// 此时使用it是未定义行为
解决方案:
string按字节处理,不适合直接处理UTF-8等多字节编码:
cpp复制string s = "你好"; // UTF-8编码
cout << s.size(); // 输出6而非2
解决方案:
频繁修改大字符串可能导致内存碎片:
cpp复制string s;
for (int i = 0; i < 10000; i++) {
s += "some data "; // 可能导致多次重新分配
s.erase(0, 5); // 可能导致内存不释放
}
解决方案:
为了系统掌握string类的使用,建议按以下顺序练习:
基础操作题
中级算法题
高级算法题
实际应用题
在实际练习中,我发现很多看似简单的字符串问题其实暗藏陷阱。比如实现split函数时,需要考虑连续分隔符、开头结尾分隔符等情况;处理字符串数字转换时,必须仔细处理溢出和非法输入。这些经验只有在大量练习后才能积累。