C++ string 类详解
在 C++ 中,std::string 是处理文本的核心组件。相比 C 语言的字符数组,它提供了更安全的内存管理、丰富的接口以及自动扩容能力。本文将系统梳理 string 类的常用接口、底层原理及面试中的常见考点。
一、基础语法辅助
在使用 string 之前,了解两个 C++11 特性会让代码更简洁。
1. auto 关键字
auto 让编译器根据初始值推导变量类型。使用 auto 时变量必须初始化。
auto x = 10; // 推导为 int
auto y = 3.14; // 推导为 double
auto ptr = &x; // 推导为 int*
注意:同一行声明多个变量时,它们必须是相同类型。例如 auto a = 10, b = 20; 正确,但 auto a = 10, b = 3.14; 会报错。
2. 范围 for 循环
基于范围的 for 循环简化了容器和数组的遍历。
int arr[] = {1, 2, 3};
for (int x : arr) {
cout << x << " ";
}
string str = "hello";
for (char c : str) {
cout << c << " ";
}
若需修改元素,建议使用引用:for (auto& e : arr)。
二、string 类常用接口
1. 构造方式
| 构造函数 | 说明 |
|---|---|
string() | 构造空字符串 |
string(const char* s) | 用 C 风格字符串构造 |
string(size_t n, char c) | 构造包含 n 个字符 c 的字符串 |
string(const string& s) | 拷贝构造 |
string s1; // 空串
string s2("Hello"); // C-string 构造
string s3(5, '*'); // *****
string s4(s2); // 拷贝构造
2. 容量操作
size()/length(): 返回有效字符长度,两者底层原理一致。capacity(): 返回当前分配的总空间大小。empty(): 判断是否为空。clear(): 清空有效字符,但不释放底层空间(capacity 不变)。resize(n, c): 调整有效字符个数,超出部分用 c 填充。reserve(n): 预留空间,不改变有效字符数,仅当 n 大于当前 capacity 时扩容。
string s = "hello";
s.reserve(100); // 预留空间,size 仍为 5
s.resize(10, 'x'); // helloxxxxx
3. 访问与遍历
operator[]: 下标访问,可读写。begin()/end(): 获取迭代器,用于正向遍历。rbegin()/rend(): 反向迭代器。- 范围 for: C++11 推荐方式。
string s = "Hello";
// 下标访问
s[0] = 'h';
// 迭代器遍历
for (auto it = s.begin(); it != s.end(); ++it) {
cout << *it << " ";
}
// 范围 for
for (char c : s) {
cout << c << " ";
}
4. 修改操作
push_back(c): 尾插单个字符。append(str): 追加字符串。operator+=: 最常用的追加方式。c_str(): 返回 C 风格字符串指针,用于兼容旧接口。find(): 查找子串位置,未找到返回string::npos。substr(pos, len): 截取子串。
string s = "Hello";
s.push_back('!'); // Hello!
s += " World"; // Hello! World
const char* cStr = s.c_str();
size_t pos = s.find("World");
string sub = s.substr(6, 5);
5. 非成员函数
operator+: 连接字符串,注意传值返回可能涉及深拷贝,性能略低。operator>>/operator<<: 输入输出重载。getline(cin, s): 读取整行字符串(含空格)。- 关系运算符:支持字典序比较。
三、string 结构差异
不同编译器下 string 内部结构略有不同,以 32 位平台为例:
- VS 编译器: 通常占用 28 字节。短字符串(<16 字符)使用内部缓冲(SSO),长字符串才分配堆内存。
- G++ 编译器: 早期版本常采用写时拷贝(COW)机制,对象本身较小,通过引用计数管理共享内存。
四、模拟实现与内存安全
面试中常要求手写 String 类,核心在于资源管理。
1. 浅拷贝与深拷贝
默认拷贝构造函数执行浅拷贝,即直接复制指针地址。这会导致析构时重复释放同一块内存,引发崩溃。
// 错误示例:浅拷贝
class String {
public:
String(const char* str) {
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
~String() { delete[] _str; }
private:
char* _str;
};
// 测试
String s1("hello");
String s2(s1); // s2._str 指向与 s1 相同的地址
解决方案:手动实现深拷贝,为新对象分配独立内存。
String(const String& s) {
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
2. 赋值运算符重载
需处理自赋值情况,并释放原有资源。
String& operator=(const String& s) {
if (this != &s) {
delete[] _str;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
return *this;
}
现代 C++ 推荐使用拷贝交换惯用法,利用移动语义或临时对象减少开销。
3. 写时拷贝(Copy-on-Write, COW)
COW 是一种优化策略,结合了浅拷贝的速度和深拷贝的安全性。
- 读操作:共享内存,无额外开销。
- 写操作:触发时复制数据,确保互不影响。
应用场景:
- Linux
fork()系统调用:父子进程共享内存页,修改时才复制。 - Redis
bgsave:子进程持久化时共享父进程内存视图。 - Java
CopyOnWriteArrayList:读多写少场景下的线程安全 List。
五、总结
掌握 string 类不仅意味着会使用 API,更要理解其背后的内存管理机制。从浅拷贝陷阱到 COW 优化,这些知识点在面试和实际开发中都至关重要。建议结合源码阅读,深入体会标准库的设计思想。


