1. string类基础概述
作为C++标准库中最常用的容器之一,string类极大地简化了字符串操作。与C风格的字符数组相比,string具有自动内存管理、丰富的成员函数和操作符重载等优势。在实际开发中,理解string的各种构造函数是正确使用它的第一步。
string本质上是一个封装了char的动态数组,它会根据内容自动调整存储空间。这种设计带来了几个重要特性:
- 内存由类自动管理,无需手动分配/释放
- 支持动态扩容,不受初始大小限制
- 提供安全的边界检查(通过at()方法)
- 内置丰富的字符串操作方法
注意:虽然operator[]提供了类似数组的访问方式,但在生产环境中建议使用at()方法,因为它会进行越界检查并抛出std::out_of_range异常。
2. 构造函数详解与使用场景
2.1 基础构造方式
cpp复制// 默认构造(空字符串)
string str1;
// C风格字符串构造
string str2("Hello World");
// 重复字符构造
string str3(5, 'A'); // "AAAAA"
// 拷贝构造
string str4(str2);
实际工程中最常用的是C风格字符串构造和拷贝构造。特别是在处理第三方库返回的char*时,直接构造string能避免很多内存管理问题。
2.2 高级构造技巧
cpp复制// 部分拷贝构造
string str5("ABCDEFG", 3); // "ABC"
// 子串构造
string str6(str2, 6, 5); // "World" (pos=6, len=5)
// 移动构造(C++11)
string str7(std::move(str2)); // str2变为空
子串构造在处理大型文本时特别有用,可以避免不必要的拷贝。移动构造在C++11后的高性能场景中尤为重要,它能避免深拷贝带来的性能损耗。
3. 容量与大小管理
3.1 基础容量操作
cpp复制string s = "Example";
cout << s.size(); // 7
cout << s.length(); // 7 (与size()相同)
cout << s.capacity(); // 15 (典型实现)
cout << s.empty(); // 0 (false)
size()和length()完全等效,这是历史原因造成的。capacity()返回的是当前分配的存储空间,通常比size()大,这是为了减少频繁扩容的开销。
3.2 内存预分配策略
cpp复制string s;
s.reserve(100); // 预分配100字节
cout << s.capacity(); // 100或更大
在已知字符串最终大小的情况下,使用reserve()可以显著提高性能。测试表明,对于需要频繁追加的大字符串(10万字符以上),预分配可以减少90%以上的内存操作。
经验:当字符串操作成为性能瓶颈时,reserve()通常是第一个要尝试的优化手段。
4. 元素访问与迭代
4.1 安全访问方法对比
cpp复制string s = "Test";
cout << s[0]; // 'T' (不检查边界)
cout << s.at(0); // 'T' (会检查边界)
try {
cout << s.at(10); // 抛出out_of_range
} catch(...) {
// 错误处理
}
在调试阶段建议统一使用at(),发布版本可以根据性能需求切换为operator[]。现代编译器对operator[]的优化非常好,在确定不会越界的情况下是更好的选择。
4.2 迭代器使用模式
cpp复制string s = "Hello";
// 正向迭代
for(auto it=s.begin(); it!=s.end(); ++it) {
cout << *it;
}
// 反向迭代
for(auto rit=s.rbegin(); rit!=s.rend(); ++rit) {
cout << *rit;
}
// 范围for循环(C++11)
for(char c : s) {
cout << c;
}
迭代器提供了统一的容器访问接口,反向迭代器在处理反向查找时特别有用。C++11引入的范围for循环实际上也是基于迭代器实现的。
5. 字符串修改操作
5.1 插入操作全解析
cpp复制string s = "HelloWorld";
// 位置插入
s.insert(5, " "); // "Hello World"
// 迭代器插入
s.insert(s.begin()+6, '!'); // "Hello !World"
// 重复字符插入
s.insert(0, 3, '*'); // "***Hello !World"
insert()有多个重载版本,最常用的是位置插入和迭代器插入。注意插入位置是基于0的索引,且必须小于等于当前size()。
5.2 删除操作实践
cpp复制string s = "This is an example";
// 位置删除
s.erase(8, 3); // "This is example"
// 迭代器删除
s.erase(s.begin()+5, s.begin()+7); // "This example"
// 清空
s.clear(); // 或 s.erase();
erase()的迭代器版本在配合查找算法时特别有用。clear()和erase()完全等效,但clear()的意图更明确。
6. 字符串查找与替换
6.1 查找算法详解
cpp复制string s = "The quick brown fox jumps over the lazy dog";
// 简单查找
size_t pos = s.find("fox"); // 16
// 反向查找
pos = s.rfind("the"); // 31 (注意大小写)
// 字符集查找
pos = s.find_first_of("aeiou"); // 2 (第一个元音'e')
find()系列方法返回的是size_t类型的位置索引,如果未找到则返回string::npos。这些方法通常用于解析文本或路径。
6.2 替换操作实战
cpp复制string s = "C++ is hard";
// 简单替换
s.replace(7, 4, "easy"); // "C++ is easy"
// 迭代器替换
s.replace(s.begin(), s.begin()+3, "Python"); // "Python is easy"
// 多字符替换
s.replace(0, 6, 3, 'A'); // "AAA is easy"
replace()的性能通常比erase()+insert()的组合要好,因为它可能只需要一次内存操作。对于大型字符串的频繁修改,这种差异会非常明显。
7. 字符串比较与连接
7.1 安全比较方法
cpp复制string s1 = "apple";
string s2 = "APPLE";
// 区分大小写比较
if(s1 == s2) { /* false */ }
// 不区分大小写比较
if(strcasecmp(s1.c_str(), s2.c_str()) == 0) { /* true */ }
string的operator==是区分大小写的。如需不区分大小写的比较,需要转换为C字符串后使用strcasecmp(POSIX)或编写自定义比较函数。
7.2 高效连接技巧
cpp复制string s1 = "Hello";
string s2 = "World";
// 简单连接
string s3 = s1 + " " + s2; // "Hello World"
// 高效连接(避免临时对象)
string s4;
s4.reserve(s1.size() + s2.size() + 1);
s4 += s1;
s4 += " ";
s4 += s2;
虽然operator+很直观,但在循环中拼接字符串时会产生大量临时对象。这种情况下应该使用ostringstream或+=操作符。
8. 子串与分割技术
8.1 子串提取实践
cpp复制string s = "2023-08-15";
// 提取年月日
string year = s.substr(0, 4);
string month = s.substr(5, 2);
string day = s.substr(8, 2);
substr()是处理固定格式字符串的利器,常用于解析日期、路径等结构化文本。注意第二个参数是长度而非结束位置。
8.2 高级分割技术
cpp复制vector<string> split(const string& s, char delim) {
vector<string> tokens;
size_t start = 0, end = 0;
while((end = s.find(delim, start)) != string::npos) {
tokens.push_back(s.substr(start, end-start));
start = end + 1;
}
tokens.push_back(s.substr(start));
return tokens;
}
标准库没有提供直接的split函数,但可以很容易地基于find和substr实现。对于复杂的分割需求,也可以考虑使用regex或第三方库。
9. 数值转换与格式化
9.1 字符串与数值互转
cpp复制// 字符串转数值
string numStr = "123.45";
double d = stod(numStr);
int i = stoi(numStr);
// 数值转字符串
string s1 = to_string(123);
string s2 = to_string(3.14159);
C++11引入的sto系列函数(stoi, stol, stoul, stof, stod等)比传统的atoi更安全,会进行错误检查。to_string()则提供了简单的数值格式化。
9.2 高级格式化技巧
cpp复制// 使用ostringstream格式化
ostringstream oss;
oss << fixed << setprecision(2) << 3.14159;
string s = oss.str(); // "3.14"
// 使用format(C++20)
string s = format("{:.2f}", 3.14159); // "3.14"
对于复杂格式化,ostringstream提供了强大而灵活的方式。C++20引入的format()函数使字符串格式化更加直观,类似于Python的format语法。
10. 性能优化与最佳实践
10.1 避免常见性能陷阱
-
循环中的字符串连接:
cpp复制// 不好:O(n²)时间复杂度 string result; for(int i=0; i<10000; i++) { result += "a"; } // 好:O(n)时间复杂度 string result; result.reserve(10000); for(int i=0; i<10000; i++) { result += "a"; } -
不必要的拷贝:
cpp复制void process(const string& s); // 使用引用避免拷贝 -
小字符串优化:大多数实现会对短字符串(通常15-22字符)进行特殊处理,避免堆分配。
10.2 现代C++技巧
-
字符串视图(C++17):
cpp复制string_view sv = "Hello"; // 不拥有数据 cout << sv.substr(1, 3); // "ell" (零拷贝) -
resize_and_overwrite(C++23):
cpp复制string s; s.resize_and_overwrite(10, [](char* p, size_t n) { fill(p, p+n, 'x'); return n; }); // "xxxxxxxxxx" -
contains(C++23):
cpp复制if(s.contains("substr")) { ... }
在实际项目中,合理选择字符串操作方式可以显著影响性能。特别是在处理大型文本(如日志分析、数据解析)时,这些优化技巧可能带来数量级的性能提升。
