1. C++ string类基础概念解析
1.1 string类的本质与优势
在算法竞赛和日常开发中,string类是C++标准库中最常用的字符串处理工具。与C风格的字符数组(char[])相比,string类具有以下核心优势:
- 自动内存管理:string对象会自动处理内存分配和释放,无需手动管理
- 动态扩容:长度可动态变化,不像字符数组有固定大小限制
- 丰富的成员函数:提供大量便捷的字符串操作方法
- 运算符重载:支持直接用=赋值、+拼接等直观操作
注意:在算法竞赛中,虽然char[]有时性能略高,但string的可读性和安全性优势明显,建议优先使用string,除非遇到极端性能要求的场景。
1.2 string类的内存结构
理解string的内存结构对高效使用很重要。现代C++实现通常采用"小字符串优化"(SSO)策略:
- 短字符串(通常≤15字符)直接存储在对象内部的缓冲区
- 长字符串则在堆上分配内存,对象内部保存指针
这种设计使得短字符串操作极快,而长字符串也能高效处理。可以通过capacity()方法查看当前分配的内存大小。
2. string对象的创建与初始化
2.1 基本创建方式
cpp复制#include <string>
using namespace std;
string s1; // 默认构造,空字符串
string s2("hello"); // 从C风格字符串构造
string s3 = "world";// 赋值构造
string s4(5, 'a'); // 填充构造,创建"aaaaa"
2.2 算法竞赛中的实用技巧
- 预分配空间:在处理已知最大长度的字符串时,可预先分配空间减少扩容开销
cpp复制string s;
s.reserve(1000); // 预分配1000字节空间
- 移动语义:C++11后支持移动构造,大幅提升大字符串传递效率
cpp复制string createLargeString() {
string s(10000, 'x');
return s; // 触发移动构造而非拷贝
}
3. string的输入输出操作
3.1 基础输入方式对比
| 输入方式 | 特点 | 适用场景 |
|---|---|---|
| cin >> s | 遇到空格停止 | 读取单词 |
| getline(cin, s) | 读取整行 | 含空格的文本 |
| getline(cin, s, delim) | 自定义分隔符 | 特殊格式输入 |
3.2 算法竞赛输入优化
在大量数据输入时,标准IO可能成为瓶颈,建议:
- 关闭同步:大幅提升C++流IO速度
cpp复制ios::sync_with_stdio(false);
cin.tie(nullptr);
- 批量读取:对于超大数据量,可考虑一次性读取后处理
cpp复制string input;
input.reserve(1000000);
char c;
while(cin.get(c)) input += c;
4. string的常用操作详解
4.1 长度与容量操作
cpp复制string s = "hello";
s.size(); // 5 (实际字符数)
s.length(); // 5 (同size)
s.empty(); // false
s.capacity(); // 可能≥5 (分配的内存大小)
s.resize(10); // 调整大小为10,多出部分填充\0
s.shrink_to_fit(); // 释放多余内存
4.2 元素访问与修改
cpp复制string s = "hello";
s[0] = 'H'; // 通过[]访问,不检查边界
s.at(1) = 'E'; // 通过at访问,会检查边界
s.front() = 'H'; // 首字符
s.back() = 'O'; // 末字符
警告:operator[]不进行边界检查,越界访问是未定义行为。在调试阶段建议使用at(),它会抛出std::out_of_range异常。
5. string的高级操作与算法应用
5.1 子串操作
cpp复制string s = "hello world";
s.substr(6); // "world" (从位置6开始)
s.substr(0,5); // "hello" (从0开始,长度5)
s.erase(5,1); // 删除位置5的1个字符(空格)
s.insert(5," "); // 在位置5插入空格
5.2 查找与替换
cpp复制string s = "hello world";
s.find("wo"); // 返回6 (首次出现位置)
s.rfind("l"); // 返回9 (最后出现位置)
s.find_first_of("aeiou"); // 查找首个元音
s.replace(6,5,"there"); // "hello there"
5.3 算法竞赛实战技巧
- 字符串分割:竞赛中常见需求
cpp复制vector<string> split(const string& s, char delim) {
vector<string> tokens;
string token;
istringstream iss(s);
while(getline(iss, token, delim)) {
if(!token.empty()) tokens.push_back(token);
}
return tokens;
}
- 快速模式匹配:结合STL算法
cpp复制string s = "ababababc";
string pat = "aba";
auto it = search(s.begin(), s.end(),
boyer_moore_searcher(pat.begin(), pat.end()));
if(it != s.end()) cout << "Found at " << it-s.begin();
6. 性能优化与常见问题
6.1 字符串拼接优化
频繁拼接字符串时,+=操作可能导致多次内存分配:
cpp复制// 低效写法
string result;
for(int i=0; i<10000; i++) {
result += to_string(i); // 可能多次重新分配
}
// 高效写法
string result;
result.reserve(50000); // 预分配足够空间
for(int i=0; i<10000; i++) {
result += to_string(i);
}
6.2 常见问题排查
- 越界访问:使用at()替代[]进行调试
- 迭代器失效:修改字符串会使指向它的迭代器失效
- 多线程安全:多个线程同时修改同一string对象不安全
- 编码问题:string本质是字节序列,处理多字节字符需谨慎
7. 算法竞赛中的string应用实例
7.1 回文串判断
cpp复制bool isPalindrome(const string& s) {
return equal(s.begin(), s.begin()+s.size()/2, s.rbegin());
}
7.2 大数运算
利用string处理超出内置类型范围的大数:
cpp复制string addBigNumbers(string num1, string num2) {
int i = num1.length()-1, j = num2.length()-1;
string res;
int carry = 0;
while(i>=0 || j>=0 || carry) {
int n1 = i>=0 ? num1[i--]-'0' : 0;
int n2 = j>=0 ? num2[j--]-'0' : 0;
int sum = n1 + n2 + carry;
res.push_back(sum%10 + '0');
carry = sum/10;
}
reverse(res.begin(), res.end());
return res;
}
7.3 字符串压缩
cpp复制string compress(const string& s) {
string res;
int count = 1;
for(int i=1; i<=s.length(); i++) {
if(i<s.length() && s[i]==s[i-1]) {
count++;
} else {
res += s[i-1];
if(count > 1) res += to_string(count);
count = 1;
}
}
return res;
}
在实际编码中,我发现预分配足够空间可以显著提升string操作的性能,特别是在处理大规模数据时。另外,理解string的内部实现机制有助于写出更高效的代码,比如知道短字符串会存储在栈上,就可以针对性地优化小字符串的处理逻辑。