1. STL与string概述
STL(Standard Template Library,标准模板库)是C++标准库的重要组成部分,由惠普实验室的Alexander Stepanov和Meng Lee于1994年开发并开源。STL提供了一系列通用的数据结构和算法,极大地提高了C++的开发效率。
string是STL六大分类容器之一,专门用于管理字符数组,提供了丰富的成员函数来实现字符串的增删改查和各种算法操作。与C语言的strxxx系列库函数相比,C++ string更加安全、易用,并且自动管理内存。
2. string的构造函数
string提供了多种构造函数,可以满足不同的初始化需求:
2.1 基本构造函数
cpp复制string s1; // 默认构造为空字符串
string s2("hello world"); // 用C风格字符串构造
string s3(s2); // 拷贝构造
2.2 带参数的构造函数
cpp复制string s4(s2, 2); // 从s2的第2个字符开始拷贝构造
string s5(s2, 80, 1000); // 从s2的第80个字符开始拷贝最多1000个字符
string s6("hello", 3); // 用字符串前3个字符构造
string s7(10, 'a'); // 用10个'a'字符构造
注意:当指定拷贝长度超过源字符串长度时,实际拷贝的字符数不会超过源字符串剩余的长度。
3. 迭代器与范围for
3.1 迭代器基础
迭代器是STL中通用的访问容器元素的方式,其底层实现可能是指针,也可能是更复杂的数据结构。string提供了多种迭代器:
cpp复制string s("example");
string::iterator it = s.begin(); // 正向迭代器
while (it != s.end()) {
cout << *it << " ";
++it;
}
3.2 反向迭代器
cpp复制string::reverse_iterator rit = s.rbegin(); // 反向迭代器
while (rit != s.rend()) {
cout << *rit << " ";
++rit;
}
3.3 范围for循环
C++11引入的范围for循环底层就是基于迭代器实现的:
cpp复制for (auto& ch : s) {
ch = toupper(ch); // 可以修改字符
cout << ch << " ";
}
提示:在string中,通常使用[]运算符访问元素比迭代器更方便,但在需要与STL算法配合使用时,迭代器是更好的选择。
4. 容量管理
4.1 size与capacity
cpp复制string s("hello");
cout << s.size() << endl; // 5
cout << s.capacity() << endl; // 视实现而定
注意:size()和length()功能相同,length()是为了与C字符串习惯保持一致,size()则是为了与STL容器接口统一。
4.2 resize与reserve
cpp复制s.resize(10, 'x'); // 将size调整为10,新增字符用'x'填充
s.reserve(100); // 预分配至少100个字符的空间
不同编译器下resize和reserve的行为可能不同:
- VS2022:采用近似1.5倍的扩容策略
- g++:采用2倍扩容策略
4.3 shrink_to_fit
C++11新增的成员函数,请求减少capacity以匹配size:
cpp复制s.shrink_to_fit(); // 不保证一定会缩小capacity
5. 元素访问
5.1 operator[]
cpp复制char c = s[0]; // 访问第一个字符
s[0] = 'H'; // 修改第一个字符
警告:使用[]访问越界位置会导致未定义行为,调试模式下可能会触发断言。
5.2 at()
cpp复制try {
char c = s.at(100); // 越界访问会抛出std::out_of_range异常
} catch (const exception& e) {
cout << e.what() << endl;
}
at()比operator[]更安全,但性能稍低。
6. 字符串修改
6.1 基本操作
cpp复制s.push_back('!'); // 追加单个字符
s.append(" world"); // 追加字符串
s += "!!"; // 等价于append
6.2 insert与erase
cpp复制s.insert(5, " dear"); // 在位置5插入字符串
s.erase(5, 5); // 从位置5开始删除5个字符
6.3 replace与substr
cpp复制s.replace(6, 5, "everyone"); // 替换从6开始的5个字符
string sub = s.substr(0, 5); // 获取子串
7. 字符串操作
7.1 c_str()
cpp复制const char* p = s.c_str(); // 获取C风格字符串
FILE* f = fopen("test.txt", "w");
fputs(p, f); // 用于需要C风格字符串的API
fclose(f);
7.2 find系列函数
cpp复制size_t pos = s.find("world"); // 查找子串
pos = s.find_first_of("aeiou"); // 查找第一个元音字母
pos = s.find_last_not_of(" \t\n"); // 查找最后一个非空白字符
8. 类型转换
8.1 数字与字符串转换
cpp复制// 数字转字符串
int num = 123;
string s = to_string(num);
// 字符串转数字
string s2 = "456";
int num2 = stoi(s2);
8.2 字符编码问题
cpp复制string chinese = "中文";
cout << chinese.size() << endl; // 输出取决于编码方式
在UTF-8编码下,一个中文字符通常占3个字节。
9. 实际应用示例
9.1 字符串分割
cpp复制vector<string> split(const string& s, char delimiter) {
vector<string> tokens;
size_t start = 0;
size_t end = s.find(delimiter);
while (end != string::npos) {
tokens.push_back(s.substr(start, end - start));
start = end + 1;
end = s.find(delimiter, start);
}
tokens.push_back(s.substr(start));
return tokens;
}
9.2 URL解析
cpp复制void parseURL(const string& url) {
size_t protocol_end = url.find("://");
string protocol = url.substr(0, protocol_end);
size_t domain_start = protocol_end + 3;
size_t domain_end = url.find("/", domain_start);
string domain = url.substr(domain_start, domain_end - domain_start);
string path = url.substr(domain_end);
cout << "Protocol: " << protocol << endl;
cout << "Domain: " << domain << endl;
cout << "Path: " << path << endl;
}
10. 性能优化建议
-
预分配空间:在知道最终字符串大小时,使用reserve()预分配空间,避免多次重新分配。
-
避免临时对象:尽量使用+=而不是+来拼接字符串,减少临时对象的创建。
-
谨慎使用insert/erase:在字符串中间插入或删除元素会导致后续元素的移动,性能开销较大。
-
考虑string_view:C++17引入的string_view可以避免不必要的字符串拷贝。
-
选择适当的查找方法:根据需求选择find/find_first_of等不同的查找方法,它们的时间复杂度可能不同。
11. 常见问题解答
Q: string的最大长度是多少?
A: 理论上string::max_size()返回的值是string能容纳的最大字符数,实际值取决于实现和系统资源。
Q: string会自动释放内存吗?
A: 是的,string的析构函数会自动释放分配的内存。但某些实现可能会保留部分内存供后续使用。
Q: 如何高效地拼接多个字符串?
A: 可以使用ostringstream或者预先计算总长度并reserve()空间,然后逐个append。
Q: string是否线程安全?
A: 对同一个string对象的并发修改是不安全的,需要外部同步机制。
12. 实际案例分析
12.1 大数相加
cpp复制string addStrings(string num1, string num2) {
string result;
int i = num1.size() - 1, j = num2.size() - 1;
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;
carry = sum / 10;
result.push_back(sum % 10 + '0');
}
reverse(result.begin(), result.end());
return result;
}
12.2 反转字符串中的单词
cpp复制string reverseWords(string s) {
size_t start = 0;
for (size_t i = 0; i <= s.size(); ++i) {
if (i == s.size() || s[i] == ' ') {
reverse(s.begin() + start, s.begin() + i);
start = i + 1;
}
}
return s;
}
13. 进阶话题
13.1 SSO优化
大多数现代string实现都采用了SSO(Small String Optimization)优化,当字符串较短时(通常15-22个字符),直接存储在对象内部,避免堆分配。
13.2 COW技术
早期的一些实现使用COW(Copy-On-Write)技术来优化拷贝性能,但在多线程环境下存在问题,现代实现大多已弃用。
13.3 自定义分配器
string支持自定义内存分配器,可以针对特定场景优化内存分配策略:
cpp复制template<typename T>
class MyAllocator {
// 实现分配器接口
};
using CustomString = std::basic_string<char, std::char_traits<char>, MyAllocator<char>>;
14. 最佳实践总结
-
优先使用string而非C字符串:更安全、更方便。
-
合理使用reserve():对于已知大小的字符串操作,预先分配空间。
-
注意编码问题:处理多语言文本时要明确编码方式。
-
善用算法库:结合STL算法实现复杂字符串操作。
-
考虑性能热点:在性能敏感区域避免不必要的字符串拷贝。
-
保持接口一致性:设计函数时,参数类型尽量使用const string&而非const char*。
-
异常安全:使用at()进行边界检查,或在调用operator[]前自行检查边界。
-
跨平台注意:不同编译器下string的实现细节可能不同,避免依赖特定行为。
