作为C++标准库中最常用的容器之一,string类的重要性不言而喻。我在实际开发中发现,很多开发者虽然每天都在使用string,但对它的底层机制和高效用法却知之甚少。本文将结合我多年的C++开发经验,带你全面掌握string类的核心用法和底层原理。
string类提供了多种构造函数,满足不同场景下的初始化需求:
cpp复制// 无参构造(空字符串)
string str1;
// C风格字符串构造
string str2("Hello World");
// 重复字符构造
string str3(5, 'A'); // "AAAAA"
// 拷贝构造
string str4(str2);
注意:使用无参构造函数时,字符串初始长度为0,但内存可能已经分配(取决于编译器实现)。这与直接初始化为""有细微差别。
cpp复制// 部分字符串构造
string str5("Hello World", 5); // 取前5个字符 -> "Hello"
// 子串构造
string str6(str2, 6, 5); // 从第6位开始取5个字符 -> "World"
// 移动构造(C++11)
string str7(std::move(str2)); // str2资源被转移,变为空
在实际项目中,我经常使用子串构造来快速提取特定部分的数据,比如解析日志时截取时间戳部分。
cpp复制string str = "Example";
cout << "size: " << str.size() << endl; // 7
cout << "length: " << str.length() << endl; // 7
cout << "capacity: " << str.capacity() << endl; // 15(取决于实现)
size()和length()完全等价,这是历史原因造成的。capacity()则反映了当前分配的内存空间,这通常大于实际大小。
cpp复制string str;
str.reserve(100); // 预分配100字节
cout << "capacity after reserve: " << str.capacity() << endl;
str.shrink_to_fit(); // 释放多余内存(C++11)
cout << "capacity after shrink: " << str.capacity() << endl;
经验:在已知最终大小时预先reserve()可以避免多次重新分配,显著提升性能。我在处理大型文本文件时,通常会先获取文件大小再reserve。
cpp复制string str = "SafeAccess";
try {
cout << str.at(100) << endl; // 抛出out_of_range异常
} catch(const std::out_of_range& e) {
cerr << "Error: " << e.what() << endl;
}
cout << str[100] << endl; // 未定义行为!
at()方法会进行边界检查,而[]操作符不会。生产环境建议使用at(),除非性能要求极高。
cpp复制// 传统下标遍历
for(size_t i=0; i<str.size(); ++i) {
cout << str[i];
}
// 范围for循环(C++11)
for(char c : str) {
cout << c;
}
// 迭代器遍历
for(auto it=str.begin(); it!=str.end(); ++it) {
cout << *it;
}
// 反向迭代器
for(auto rit=str.rbegin(); rit!=str.rend(); ++rit) {
cout << *rit;
}
在性能测试中,范围for循环和迭代器方式在现代编译器上性能相当,都比传统下标访问略快。
cpp复制string str = "HelloWorld";
// 位置插入
str.insert(5, " "); // "Hello World"
// 迭代器插入
str.insert(str.begin()+6, '!'); // "Hello !World"
// 多重插入
str.insert(0, 3, '*'); // "***Hello !World"
注意:insert操作可能导致重新分配内存,使所有迭代器、指针和引用失效。这是常见的bug来源。
cpp复制string str = "TooMuchInformation";
// 位置删除
str.erase(7, 9); // "TooMuch"
// 迭代器删除
str.erase(str.begin()+3, str.begin()+6); // "Tooch"
// 清空字符串
str.clear(); // 或 str.erase();
我在日志处理系统中经常使用erase来清理过期的日志信息,相比创建新字符串,原地修改效率更高。
cpp复制string a = "Hello", b = "World";
// 基本拼接
string c = a + " " + b; // "Hello World"
// 高效拼接(避免临时对象)
a.append(" ").append(b); // 链式调用
// 性能对比
auto start = chrono::high_resolution_clock::now();
for(int i=0; i<10000; ++i) {
string s = a + b; // 创建临时对象
}
auto end = chrono::high_resolution_clock::now();
在性能测试中,append()比+操作符快约30%,特别是在循环中差异更明显。
string采用"短字符串优化"(SSO),小字符串直接存储在对象内部,大字符串才使用堆内存。典型实现中:
cpp复制string small = "SSO"; // 栈存储
string large = "This is a long string that will use heap allocation";
理解这一点对性能优化很重要。我在处理大量短字符串时,会特意控制字符串长度来利用SSO。
cpp复制string chinese = "你好";
cout << chinese.length(); // 输出可能是4或6,而非2
这是因为string基于字节而非字符。处理中文推荐使用wstring或第三方库如ICU。
cpp复制vector<string> vec;
// 错误示范:频繁扩容
for(int i=0; i<100000; ++i) {
vec.push_back("item"); // 每次都可能复制字符串
}
// 正确做法:预分配+移动语义
vec.reserve(100000);
for(int i=0; i<100000; ++i) {
vec.push_back(std::move(string("item")));
}
通过reserve()+move()可以将性能提升5-10倍,特别是在处理大量字符串时。
不同平台/编译器对string的实现可能有差异:
编写跨平台代码时,应避免依赖这些实现细节。
C++17引入了string_view,作为string的非拥有视图:
cpp复制string long_str = "This is a very long string...";
string_view view(long_str.c_str(), 10); // "This is a"
// 无需拷贝,性能极高
processString(view);
我在解析大型文本时大量使用string_view,可以避免不必要的字符串拷贝。
我曾经优化过一个文本处理系统,通过合理使用reserve()和移动语义,将处理时间从3秒降到了0.5秒。