1. string类概述与基本特性
string是C++标准库中用于管理字符串的类,它封装了字符数组的操作,提供了丰富的成员函数来简化字符串处理。作为C++程序员日常开发中最常用的工具之一,string相比C风格的字符数组(char*)具有明显优势:
- 自动内存管理:无需手动分配和释放内存
- 边界检查:提供安全的访问机制
- 丰富的操作接口:拼接、查找、替换等操作简单易用
- 与STL良好集成:可与算法、容器等协同工作
要使用string类,需要包含头文件<string>。虽然在现代C++中,由于头文件包含的传递性,有时不显式包含也能编译通过,但显式包含是更好的编程实践。
2. string的初始化方式
string提供了多种初始化方式,每种方式都有其适用场景。下面详细介绍7种常见的初始化方法:
2.1 默认初始化
cpp复制string s1; // 空字符串
这是最简单的初始化方式,创建一个空字符串。此时字符串长度为0,但具体容量取决于实现。在VS2022中,默认容量为15(预留空间为16,包含结尾的'\0')。
2.2 使用C风格字符串初始化
cpp复制string s2("hello"); // 使用字符串字面量初始化
string s3 = "world"; // 赋值初始化
这种方式直接从C风格字符串构造string对象。需要注意的是,传入的指针不能为nullptr,否则会导致未定义行为。
2.3 使用另一个string对象初始化
cpp复制string s4(s2); // 拷贝构造
string s5 = s2; // 拷贝赋值
这两种方式都会创建一个与源string内容相同的新对象。现代C++编译器通常会优化这种拷贝操作。
2.4 使用部分字符串初始化
cpp复制string s6(s2, 1, 3); // 从s2的第1个字符开始,拷贝3个字符
string s7(s2, 1); // 从s2的第1个字符开始,拷贝到结尾
当源字符串较短时,实际拷贝的长度不会超过可用字符数。如果不指定长度参数,默认使用npos(size_t类型的最大值,约42亿),即拷贝到字符串结尾。
2.5 使用重复字符初始化
cpp复制string s8(5, 'a'); // "aaaaa"
这种方式适合需要创建由重复字符组成的字符串的场景,比如生成分隔线或占位符。
2.6 使用迭代器范围初始化
cpp复制string s9(s2.begin(), s2.begin()+3); // "hel"
这种初始化方式非常灵活,可以从任何支持迭代器的容器中提取部分内容构造字符串。
注意:临时变量的初始化形式
string()会被编译器视为函数声明而非对象创建,这是C++语法解析的一个常见陷阱。
3. string的元素访问
3.1 下标操作符重载
string本质是一个动态字符数组,通过重载operator[]实现了类似数组的访问方式:
cpp复制string s = "hello";
char c1 = s[1]; // 'e'
s[1] = 'a'; // 修改为"hallo"
与C风格数组不同,string的下标访问在调试模式下通常会进行边界检查(通过assert),但在发布模式下为了性能可能会移除检查。
3.2 at()成员函数
cpp复制char c2 = s.at(2); // 'l'
at()与operator[]功能类似,但at()在任何情况下都会进行边界检查,如果越界会抛出std::out_of_range异常。
3.3 前端和后端访问
cpp复制char front = s.front(); // 'h'
char back = s.back(); // 'o'
这两个方法提供了快速访问首尾字符的方式,比使用下标更直观。
4. 迭代器与范围for
4.1 迭代器基础
迭代器是STL的核心概念,它提供了一种统一的方式来遍历容器。string的迭代器本质是字符指针的封装:
cpp复制string s = "example";
for(string::iterator it = s.begin(); it != s.end(); ++it) {
cout << *it << " ";
}
begin()返回指向第一个元素的迭代器,end()返回指向末尾(最后一个元素的下一个位置)的迭代器。这种半开区间设计是STL的通用模式。
4.2 常量迭代器
对于const string对象,需要使用const_iterator:
cpp复制const string cs = "const";
for(string::const_iterator it = cs.begin(); it != cs.end(); ++it) {
// *it = 'x'; // 错误:不能修改const对象
cout << *it;
}
现代C++编译器能自动选择最匹配的迭代器类型,因此在实际编码中通常使用auto简化声明:
cpp复制for(auto it = s.begin(); it != s.end(); ++it) {
// ...
}
4.3 反向迭代器
string还支持反向遍历:
cpp复制for(string::reverse_iterator rit = s.rbegin(); rit != s.rend(); ++rit) {
cout << *rit; // 逆序输出
}
注意反向迭代器的++操作是向前移动,与常规迭代器方向相反。
4.4 范围for循环
C++11引入的范围for语法糖大大简化了容器遍历:
cpp复制for(char c : s) {
cout << c; // 输出每个字符
}
范围for的底层实现其实就是迭代器遍历。如果要修改元素,需要使用引用:
cpp复制for(char &c : s) {
c = toupper(c); // 转换为大写
}
5. 容量操作
5.1 size()与length()
cpp复制string s = "hello";
cout << s.size(); // 5
cout << s.length(); // 5
这两个方法功能完全相同,都返回字符串中的字符数(不包括结尾的'\0')。size()是STL容器的统一接口,而length()是为了与C字符串传统保持一致。
5.2 capacity()
capacity()返回当前分配的存储空间大小(不包括结尾的'\0'),通常大于等于size():
cpp复制cout << s.capacity(); // 15 (VS2022初始分配)
了解capacity有助于优化性能,特别是在需要大量拼接操作的场景。
5.3 扩容机制
string采用动态扩容策略来平衡内存使用和性能。在VS2022中的典型行为:
- 初始容量:15(分配16字节)
- 第一次扩容:2倍
- 后续扩容:约1.5倍
这种策略减少了频繁重新分配和拷贝的开销。实际实现中,string可能使用小字符串优化(SSO),即短字符串直接存储在对象内部,而不进行堆分配。
5.4 reserve()与shrink_to_fit()
cpp复制s.reserve(100); // 预分配空间
s.shrink_to_fit(); // 释放多余空间
reserve()可以提前分配足够空间,避免多次扩容。注意它不会改变字符串内容。shrink_to_fit()请求释放未使用的内存,但实现不一定保证执行。
5.5 resize()
resize()改变字符串长度,有三种情况:
cpp复制s.resize(3); // 截断为"hel"
s.resize(6); // 补充为"hel\0\0\0"
s.resize(100); // 扩容并补充\0
新元素默认初始化为'\0',也可以指定初始化值:
cpp复制s.resize(10, 'x'); // 用'x'填充
6. 修改操作
6.1 clear()
清空字符串内容,但通常不释放内存:
cpp复制s.clear(); // size=0, capacity不变
6.2 append()与operator+=
cpp复制s.append(" world");
s += "!"; // 等价于append
这两种方式都用于字符串拼接,但+=更简洁。多次拼接时应考虑reserve()以避免反复扩容。
6.3 assign()
替换字符串内容:
cpp复制s.assign("new content");
s.assign(5, 'x'); // "xxxxx"
6.4 insert()
在指定位置插入内容:
cpp复制s.insert(3, "inserted");
注意插入位置必须在[0, size()]范围内。
6.5 erase()
删除部分内容:
cpp复制s.erase(3, 5); // 从位置3开始删除5个字符
s.erase(s.begin()+2); // 删除单个字符
6.6 replace()
替换部分内容:
cpp复制s.replace(3, 2, "replacement");
7. 字符串操作
7.1 c_str()与data()
cpp复制const char* p = s.c_str(); // 获取C风格字符串
c_str()返回以'\0'结尾的字符数组,适用于需要与C API交互的场景。data()在C++11后也保证返回null-terminated字符串。
7.2 copy()
cpp复制char buf[10];
size_t copied = s.copy(buf, sizeof(buf));
将字符串内容复制到字符数组中,不会自动添加'\0'。
7.3 substr()
cpp复制string sub = s.substr(2, 4); // 从位置2开始,长度4
提取子字符串,是字符串处理的常用操作。
7.4 find系列
cpp复制size_t pos = s.find("ll"); // 查找子串
pos = s.rfind("l"); // 反向查找
pos = s.find_first_of("aeiou"); // 查找任何元音
pos = s.find_last_not_of(" \t"); // 查找最后一个非空白字符
查找失败时返回string::npos。
8. 非成员函数
8.1 运算符重载
cpp复制string s1 = "hello", s2 = "world";
bool b1 = (s1 == s2); // 比较
string s3 = s1 + s2; // 连接
string支持各种比较和连接操作,使用起来非常直观。
8.2 流操作
cpp复制ostringstream oss;
oss << "Value: " << 42;
string s = oss.str(); // "Value: 42"
istringstream iss("123 456");
int a, b;
iss >> a >> b;
字符串流提供了灵活的类型转换和格式化能力。
8.3 stoi/stol/stoll等
cpp复制string num = "1234";
int n = stoi(num); // 字符串转整数
这些函数比C风格的atoi更安全,会进行错误检查。
9. 性能与最佳实践
- 预分配空间:对于已知大小的字符串操作,先reserve()可显著提高性能
- 避免临时对象:如
string s = "a" + "b";是错误的,应该string s = string("a") + "b"; - 移动语义:C++11后,string支持移动构造和移动赋值,减少拷贝开销
- 引用传递:函数参数尽量使用
const string&避免不必要的拷贝 - 小字符串优化:多数实现对小字符串有特殊优化,通常16字节以内
10. 常见问题与解决方案
10.1 越界访问
cpp复制try {
char c = s.at(100); // 抛出异常
} catch(const std::out_of_range& e) {
cerr << e.what();
}
使用at()替代operator[]可以在调试时更容易发现问题。
10.2 迭代器失效
cpp复制auto it = s.begin();
s.append("new content"); // 可能导致it失效
// *it; // 未定义行为
在修改字符串后,之前获取的迭代器可能失效,需要重新获取。
10.3 内存碎片
频繁修改大字符串可能导致内存碎片,此时可以:
cpp复制string(s).swap(s); // C++03方式
s.shrink_to_fit(); // C++11方式
10.4 编码问题
string本质是字节序列,对多字节编码(如UTF-8)需要特别处理:
cpp复制u8string utf8 = u8"中文"; // C++11引入
对于复杂的文本处理,可能需要专门的库如ICU。
11. 实际应用示例
11.1 字符串分割
cpp复制vector<string> split(const string& s, char delim) {
vector<string> tokens;
size_t start = 0, end = s.find(delim);
while(end != string::npos) {
tokens.push_back(s.substr(start, end-start));
start = end + 1;
end = s.find(delim, start);
}
tokens.push_back(s.substr(start));
return tokens;
}
11.2 字符串trim
cpp复制string trim(const string& s) {
auto start = s.find_first_not_of(" \t\n\r");
if(start == string::npos) return "";
auto end = s.find_last_not_of(" \t\n\r");
return s.substr(start, end-start+1);
}
11.3 格式化字符串
cpp复制string format(const string& pattern, const vector<string>& args) {
string result;
size_t i = 0, pos = 0;
while(true) {
size_t new_pos = pattern.find("{}", pos);
if(new_pos == string::npos) break;
result.append(pattern, pos, new_pos-pos);
if(i < args.size()) {
result += args[i++];
}
pos = new_pos + 2;
}
result.append(pattern, pos, pattern.size()-pos);
return result;
}
string类是C++中最常用的工具之一,掌握它的各种特性和最佳实践对提高编程效率和代码质量至关重要。在实际开发中,应根据具体需求选择合适的操作方式,并注意性能与安全性的平衡。
