1. C++ string基础入门
在C++编程中,字符串处理是最基础也是最重要的操作之一。相比于C语言中繁琐的字符数组操作,C++标准库提供的string类极大地简化了字符串处理流程。string不仅封装了字符串的所有操作,还提供了安全、便捷、高效的接口,是现代C++开发中处理字符串的首选工具。
要开始使用string,首先需要包含头文件和声明命名空间:
cpp复制#include <string> // 必须包含的头文件
using namespace std; // 推荐在小型项目中使用
// 或者更安全的做法
// 在大型项目中避免污染全局命名空间
std::string myString;
string的初始化方式灵活多样,可以根据不同场景选择最合适的方式:
cpp复制// 空字符串初始化
string s1; // 默认构造
string s2 = ""; // 显式空字符串
// 字符串常量初始化
string s3 = "Hello"; // 拷贝初始化
string s4("World"); // 直接初始化
// 从另一个string初始化
string s5 = s3; // 拷贝构造
string s6(s4); // 直接构造
// 重复字符初始化
string s7(5, 'a'); // "aaaaa"
// 部分字符串初始化
string s8 = "Hello World";
string s9(s8, 6, 5); // 从索引6开始,取5个字符 -> "World"
2. string核心操作详解
2.1 长度与容量管理
string提供了多种方法来获取和操作字符串的长度和容量:
cpp复制string str = "Hello C++";
// 获取长度
cout << str.size(); // 输出:9 (推荐)
cout << str.length(); // 输出:9 (与size()相同)
// 检查是否为空
if(str.empty()) {
cout << "字符串为空";
}
// 容量相关操作
cout << str.capacity(); // 返回当前分配的存储空间大小
str.reserve(100); // 预分配空间,避免频繁扩容
str.shrink_to_fit(); // 减少容量以适应大小(C++11)
注意:size()和length()在功能上完全相同,但size()与其他STL容器保持一致,推荐优先使用。
2.2 元素访问与修改
string提供了多种访问和修改单个字符的方式:
cpp复制string str = "Hello";
// 使用下标运算符访问
char first = str[0]; // 'H'
str[0] = 'h'; // 修改第一个字符
// 使用at()方法访问(带边界检查)
try {
char last = str.at(4); // 'o'
str.at(5); // 抛出out_of_range异常
} catch(const std::out_of_range& e) {
cerr << "索引越界:" << e.what();
}
// 迭代器访问(C++11范围for)
for(char& c : str) {
c = toupper(c);
}
cout << str; // 输出:"HELLO"
下标运算符[]和at()的主要区别:
- []不进行边界检查,越界访问导致未定义行为
- at()会检查边界,越界时抛出std::out_of_range异常
- []性能略高,at()更安全
2.3 字符串拼接与修改
string提供了丰富的拼接和修改操作:
cpp复制string s1 = "Hello";
string s2 = "World";
// 基本拼接
s1 += " "; // 拼接字符串字面量
s1 += s2; // 拼接另一个string
s1 += '!'; // 拼接单个字符
// 使用append方法
s1.append(" This");
s1.append(" is C++", 4); // 只拼接前4个字符:" is "
// insert方法
s1.insert(5, " beautiful"); // 在位置5插入
// replace方法
s1.replace(0, 5, "Hi"); // 替换前5个字符
cout << s1; // 输出:"Hi beautiful World!"
拼接操作性能提示:
- 小规模拼接使用+=运算符最方便
- 大规模拼接时,可以先reserve()预留足够空间,避免频繁内存分配
- append()方法提供更多控制选项,如指定拼接长度
3. string高级操作与算法
3.1 字符串查找与截取
string提供了强大的查找和子串操作:
cpp复制string text = "The quick brown fox jumps over the lazy dog";
// 查找子串
size_t pos = text.find("fox"); // 返回首次出现位置(16)
pos = text.find("dog", 30); // 从位置30开始查找
// 查找失败返回string::npos
if(text.find("cat") == string::npos) {
cout << "未找到子串";
}
// 反向查找
pos = text.rfind("the"); // 从后向前查找
// 查找字符集合中任意字符首次出现位置
pos = text.find_first_of("aeiou"); // 查找第一个元音字母
// 截取子串
string sub = text.substr(10, 5); // "brown"
sub = text.substr(20); // "jumps over the lazy dog"
查找算法优化技巧:
- 对于频繁查找,可以考虑将字符串转换为更高效的数据结构
- 大规模文本搜索建议使用专门算法(KMP,Boyer-Moore等)
3.2 字符串比较与转换
string的比较操作既直观又强大:
cpp复制string a = "apple";
string b = "banana";
// 基本比较
if(a == "apple") { /*...*/ }
if(a != b) { /*...*/ }
if(a < b) { /*...*/ } // 字典序比较
// 使用compare方法(提供更多控制)
int result = a.compare(0, 2, b, 0, 2); // 比较前两个字符
// 数字转换
string numStr = "123";
int num = stoi(numStr); // 字符串转整数
double d = stod("3.14"); // 字符串转浮点数
string piStr = to_string(3.14159); // 数字转字符串
// 大小写转换
transform(a.begin(), a.end(), a.begin(), ::toupper);
比较操作注意事项:
- 比较运算符执行字典序比较
- compare()方法可以比较子串,返回负数/0/正数
- 数字转换方法(stoi,stol等)会忽略前导空白字符
3.3 字符串与C风格接口互操作
与C语言接口交互时需要进行转换:
cpp复制// string转C风格字符串
string cppStr = "Hello C++";
const char* cStr = cppStr.c_str(); // 只读访问
const char* dataStr = cppStr.data(); // C++17起与c_str()相同
// C风格字符串转string
const char* cStyle = "C string";
string fromCStr(cStyle);
// 带长度的转换(避免缓冲区溢出)
string safeCopy(cStyle, 5); // 只拷贝前5个字符
// 使用string作为缓冲区
vector<char> buffer(100);
strcpy(buffer.data(), cppStr.c_str());
重要注意事项:
- c_str()返回的指针在string修改后可能失效
- 不要尝试修改c_str()返回的内容
- 处理二进制数据时使用data()更合适
4. string性能优化与最佳实践
4.1 内存管理策略
理解string的内存管理机制对性能优化至关重要:
cpp复制string str; // 初始容量通常为15或31(取决于实现)
// 观察容量增长
for(int i=0; i<100; ++i) {
size_t oldCap = str.capacity();
str += 'x';
if(str.capacity() != oldCap) {
cout << "容量从" << oldCap << "增长到"
<< str.capacity() << endl;
}
}
// 优化策略
string largeStr;
largeStr.reserve(1000); // 预分配空间
for(int i=0; i<1000; ++i) {
largeStr += 'x'; // 不会触发重新分配
}
内存管理要点:
- string采用指数增长策略(通常2倍)
- 频繁追加操作前使用reserve()预分配
- shrink_to_fit()可减少内存占用(不保证)
- move语义(C++11)可以避免不必要的拷贝
4.2 高效字符串处理技巧
cpp复制// 1. 避免临时字符串
string result = str1 + str2 + str3; // 创建临时对象
// 更好做法:
string result;
result.reserve(str1.size() + str2.size() + str3.size());
result = str1;
result += str2;
result += str3;
// 2. 使用string_view(C++17)避免拷贝
void processString(string_view sv) {
// 可以处理string和char*而不产生拷贝
}
processString("literal");
processString(str1);
// 3. 高效移除空白字符
string withSpaces = " hello world ";
withSpaces.erase(remove_if(withSpaces.begin(),
withSpaces.end(),
::isspace),
withSpaces.end());
// 4. 使用移动语义
string getLargeString() {
string large(1000, 'x');
return large; // 触发移动构造而非拷贝
}
4.3 常见问题与解决方案
问题1:string与char* 的混淆
cpp复制// 错误示例
string s = "hello";
char* ptr = s.c_str();
s += " world"; // 可能导致ptr失效
cout << ptr; // 未定义行为
// 正确做法
const char* safePtr = s.c_str();
// 如果后续需要修改字符串,先复制
string temp = s;
const char* safePtr2 = temp.c_str();
问题2:多字节字符处理
cpp复制string utf8 = "你好世界"; // UTF-8编码
cout << utf8.length(); // 返回字节数(非字符数)
// 处理UTF-8的正确方式
// 需要使用专门库(如ICU)或C++20的char8_t
问题3:性能热点分析
对于性能关键的字符串处理:
- 避免在循环中创建临时string对象
- 使用reserve()预分配足够空间
- 考虑使用更高效的数据结构(如rope)
- 使用profiler工具识别热点
5. string实际应用案例
5.1 文本处理工具实现
cpp复制class TextProcessor {
public:
// 构造函数
TextProcessor(const string& text) : content(text) {}
// 统计单词数
size_t countWords() const {
istringstream iss(content);
return distance(istream_iterator<string>(iss),
istream_iterator<string>());
}
// 替换所有匹配项
void replaceAll(const string& from, const string& to) {
size_t pos = 0;
while((pos = content.find(from, pos)) != string::npos) {
content.replace(pos, from.length(), to);
pos += to.length();
}
}
// 获取处理后的文本
string getProcessedText() const { return content; }
private:
string content;
};
5.2 自定义字符串分割函数
cpp复制vector<string> splitString(const string& str, char delimiter) {
vector<string> tokens;
string token;
istringstream tokenStream(str);
while(getline(tokenStream, token, delimiter)) {
if(!token.empty()) {
tokens.push_back(token);
}
}
return tokens;
}
// 使用示例
auto parts = splitString("one,two,three", ',');
// parts = {"one", "two", "three"}
5.3 高效字符串拼接模板
cpp复制template<typename... Args>
string concatenate(Args&&... args) {
string result;
size_t totalSize = 0;
// 计算总大小
auto dummy = {(totalSize += forward<Args>(args).size())...};
(void)dummy;
result.reserve(totalSize);
// 拼接所有参数
auto append = [&result](auto&& arg) {
result.append(forward<decltype(arg)>(arg));
};
(append(forward<Args>(args)), ...);
return result;
}
// 使用示例
string s1 = "Hello", s2 = " ", s3 = "World";
auto combined = concatenate(s1, s2, s3);
// combined = "Hello World"
6. C++17/20中的string新特性
6.1 string_view的使用
cpp复制// string_view是轻量级、非所有的字符串视图
void processText(string_view sv) {
// 可以接受string、char*、字面量等
cout << "Length: " << sv.length();
}
string s = "regular string";
processText(s); // string
processText("literal"); // 字面量
processText(s.c_str()); // C风格字符串
// 优点:不产生拷贝,性能更高
// 限制:不管理生命周期,原始数据必须有效
6.2 starts_with/ends_with(C++20)
cpp复制string filename = "config.yaml";
if(filename.ends_with(".yaml") ||
filename.ends_with(".yml")) {
cout << "YAML配置文件";
}
if(filename.starts_with("config")) {
cout << "配置文件";
}
6.3 其他C++20增强
cpp复制// contains方法
string log = "error: file not found";
if(log.contains("error")) {
cerr << "发现错误日志";
}
// 三路比较运算符
string a = "apple", b = "banana";
auto cmp = a <=> b; // 返回strong_ordering类型
if(cmp < 0) {
cout << a << " < " << b;
}
7. string内部实现原理
7.1 常见实现策略
现代C++库中string的典型实现方式:
-
短字符串优化(SSO)
- 小字符串(通常15-23字节)直接存储在对象内部
- 避免堆分配,提高小字符串处理效率
-
写时复制(COW)
- 某些实现使用引用计数共享内存
- C++11后逐渐淘汰,因多线程问题
-
指数容量增长
- 追加操作导致扩容时,通常按2倍或1.5倍增长
- 平衡内存使用和重新分配次数
7.2 自定义string类实现要点
cpp复制class SimpleString {
public:
// 构造函数
SimpleString(const char* str = "") {
size_ = strlen(str);
capacity_ = size_ + 1;
data_ = new char[capacity_];
strcpy(data_, str);
}
// 析构函数
~SimpleString() {
delete[] data_;
}
// 拷贝构造函数
SimpleString(const SimpleString& other) {
copyFrom(other);
}
// 拷贝赋值运算符
SimpleString& operator=(const SimpleString& other) {
if(this != &other) {
delete[] data_;
copyFrom(other);
}
return *this;
}
// 移动构造函数(C++11)
SimpleString(SimpleString&& other) noexcept
: data_(other.data_), size_(other.size_), capacity_(other.capacity_) {
other.data_ = nullptr;
other.size_ = other.capacity_ = 0;
}
size_t size() const { return size_; }
const char* c_str() const { return data_; }
private:
char* data_;
size_t size_;
size_t capacity_;
void copyFrom(const SimpleString& other) {
size_ = other.size_;
capacity_ = other.capacity_;
data_ = new char[capacity_];
strcpy(data_, other.data_);
}
};
实现注意事项:
- 遵循三/五法则(需要实现拷贝控制成员)
- 考虑异常安全性
- 移动语义可以显著提升性能
- 实现SSO需要更复杂的设计
8. 跨平台与编码问题
8.1 多字节与宽字符处理
cpp复制// 窄字符串(通常char)
string narrow = "Hello";
// 宽字符串(wchar_t, 平台相关)
wstring wide = L"宽字符字符串";
// UTF-8字符串(C++20引入u8前缀)
u8string utf8 = u8"UTF-8字符串";
// 编码转换示例(使用标准库)
wstring_convert<codecvt_utf8<wchar_t>> converter;
wstring wideStr = converter.from_bytes(narrow);
string narrowStr = converter.to_bytes(wideStr);
编码处理建议:
- 现代应用优先使用UTF-8
- 跨平台代码避免使用wchar_t(大小平台不同)
- 复杂转换使用专门库(如ICU)
8.2 文件与字符串操作
cpp复制// 读取整个文件到string
string readFile(const string& filename) {
ifstream file(filename, ios::binary | ios::ate);
if(!file) {
throw runtime_error("无法打开文件");
}
auto size = file.tellg();
string content(size, '\0');
file.seekg(0);
if(!file.read(&content[0], size)) {
throw runtime_error("读取失败");
}
return content;
}
// 写入string到文件
void writeFile(const string& filename, const string& content) {
ofstream file(filename, ios::binary);
file.write(content.data(), content.size());
}
文件操作提示:
- 二进制模式避免换行符转换
- 大文件考虑流式处理而非全部读入内存
- C++17的filesystem库提供更现代的文件操作接口
9. 性能测试与对比
9.1 不同拼接方式性能比较
cpp复制void testConcatenation() {
const int iterations = 100000;
// 1. 直接使用+=
{
string s;
auto start = chrono::high_resolution_clock::now();
for(int i=0; i<iterations; ++i) {
s += "test";
}
auto end = chrono::high_resolution_clock::now();
cout << "+= 用时: "
<< chrono::duration_cast<chrono::milliseconds>(end-start).count()
<< "ms\n";
}
// 2. 使用ostringstream
{
ostringstream oss;
auto start = chrono::high_resolution_clock::now();
for(int i=0; i<iterations; ++i) {
oss << "test";
}
string s = oss.str();
auto end = chrono::high_resolution_clock::now();
cout << "ostringstream 用时: "
<< chrono::duration_cast<chrono::milliseconds>(end-start).count()
<< "ms\n";
}
// 3. 预分配空间
{
string s;
s.reserve(iterations * 4); // 预分配足够空间
auto start = chrono::high_resolution_clock::now();
for(int i=0; i<iterations; ++i) {
s += "test";
}
auto end = chrono::high_resolution_clock::now();
cout << "预分配+= 用时: "
<< chrono::duration_cast<chrono::milliseconds>(end-start).count()
<< "ms\n";
}
}
典型测试结果(仅供参考):
- +=运算符:15ms
- ostringstream:45ms
- 预分配+=:8ms
9.2 查找算法性能对比
cpp复制void testSearch() {
// 构建大字符串
string largeText;
largeText.reserve(1000000);
for(int i=0; i<100000; ++i) {
largeText += "sample text ";
}
largeText += "target pattern";
// 测试find性能
auto start = chrono::high_resolution_clock::now();
size_t pos = largeText.find("target pattern");
auto end = chrono::high_resolution_clock::now();
cout << "string::find 用时: "
<< chrono::duration_cast<chrono::microseconds>(end-start).count()
<< "μs\n";
// 对比使用search算法
start = chrono::high_resolution_clock::now();
auto it = search(largeText.begin(), largeText.end(),
"target pattern", "target pattern" + 14);
end = chrono::high_resolution_clock::now();
cout << "std::search 用时: "
<< chrono::duration_cast<chrono::microseconds>(end-start).count()
<< "μs\n";
}
性能优化建议:
- 对于简单查找,string::find通常最优
- 复杂模式匹配考虑正则表达式或专门算法
- 高频查找可建立索引或使用哈希
10. 扩展阅读与资源推荐
10.1 推荐书籍
-
《Effective Modern C++》 - Scott Meyers
- Item 41: 理解隐式接口和编译期多态
- Item 42: 了解typename的双重意义
-
《C++标准库》 - Nicolai Josuttis
- 第13章: 字符串类
-
《C++编程规范》 - Herb Sutter
- 第84条: 使用类型安全的字符串
10.2 在线资源
-
C++ Reference
- https://en.cppreference.com/w/cpp/string/basic_string
-
C++ Core Guidelines
- https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines
-
Stack Overflow常见问题
- 如何高效拼接多个字符串
- string与char*的性能比较
- 处理UTF-8编码的最佳实践
10.3 进阶主题
-
自定义分配器
- 为string实现内存池分配器
-
与正则表达式集成
- 使用regex进行复杂模式匹配
-
国际化和本地化
- 处理多语言文本
- 使用ICU库进行高级文本处理
-
并行字符串处理
- 使用并行算法处理大文本
在实际项目中,string类的选择和使用应该基于具体需求。对于性能关键的应用,可以考虑以下替代方案:
- 使用string_view减少拷贝
- 对于连接操作频繁的场景,考虑rope数据结构
- 特殊需求下可以使用第三方字符串库(如Facebook的folly::fbstring)
