C++ 标准库 string 类详解
一、为什么学习 string 类?
在 C 语言中,字符串是以 char* 形式存在的字符数组,需要手动管理内存和长度,容易引发缓冲区溢出等安全问题。C++ 引入的 std::string 类封装了这些底层细节,提供了安全的动态字符串管理能力。
掌握 string 类不仅是日常开发的基础,也是理解 C++ 资源管理(RAII)、拷贝控制以及 STL 容器设计思想的关键。常见的字符串面试题往往围绕其底层实现、性能优化及边界处理展开。
二、C++ 标准库中的 string 类
使用 string 类前,需包含 <string> 头文件并启用命名空间:
#include <iostream>
#include <string>
using namespace std;
2.1)auto 和范围 for
auto 关键字
auto 的核心功能是让编译器根据初始值推导变量类型。使用时必须初始化。
auto x = 10; // 推导为 int
auto y = 3.14; // 推导为 double
auto ptr = &x; // 推导为 int*
声明指针时 auto 和 auto* 无区别,但声明引用时必须加 &,否则会发生值拷贝:
int x = 10;
int& ref = x;
auto c = ref; // c 是 int 类型,独立变量
auto& d = ref; // d 是 int& 类型,x 的别名
d = 30; // x 变为 30
注意:同一行声明多个变量时,它们必须是相同类型。auto 不能直接用于函数参数或数组声明。
范围 for 循环
C++11 引入了基于范围的 for 循环,简化了集合遍历:
for (declaration : range) {
// 循环体
}
- declaration:当前迭代元素(建议用引用避免拷贝)。
- range:可遍历对象(数组、vector、string 等)。
int arr[] = {1, 2, 3};
for (auto& e : arr) e *= 2; // 修改原数组
string str("hello world");
for (char ch : str) cout << ch << " ";
2.2)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() | 清空有效字符,不释放空间 |
reserve() | 预留空间,不改变有效长度 |
resize() | 调整有效字符个数 |
注意事项:
size()与length()底层实现一致,为了与其他容器接口统一。clear()仅重置有效长度,底层分配的内存(capacity)保留,以便后续复用。resize(n, c)若 n 大于当前大小,用 c 填充;若小于则截断。reserve(n)仅在 n 大于当前 capacity 时扩容,不会改变有效字符数。
string s = "hello";
s.reserve(100); // 空间可能变大,但 size() 仍为 5
s.resize(10, 'x'); // helloxxxxx
3)访问及遍历
| 函数 | 功能说明 |
|---|---|
operator[pos] | 下标访问(非常量版本可修改) |
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 (auto rit = s.rbegin(); rit != s.rend(); ++rit) {
cout << *rit << " ";
}
迭代器类型补充:
iterator:读写,正向。const_iterator:只读,正向。reverse_iterator:读写,反向。const_reverse_iterator:只读,反向。
4)修改操作
| 函数 | 功能说明 |
|---|---|
push_back(c) | 尾插单个字符 |
append(str) | 追加字符串 |
operator+= | 连接字符串/字符 |
c_str() | 返回 C 风格字符串指针 |
find() / rfind() | 查找子串位置 |
substr(pos, len) | 截取子串 |
string s = "Hello";
s.push_back('!'); // Hello!
s.append(" World"); // Hello! World
s += " to C++"; // Hello! World to C++
const char* cStr = s.c_str(); // 兼容 C 函数
size_t pos = s.find("World"); // 查找位置
if (pos != string::npos) {
cout << "Found at: " << pos << endl;
}
string sub = s.substr(7, 6); // World
性能提示:
- 频繁追加字符时,建议先用
reserve()预留空间,减少因扩容导致的内存重新分配开销。 operator+会创建临时对象,大量拼接时效率低于+=或append。
5)非成员函数
operator>>:输入流读取(遇到空格停止)。operator<<:输出流写入。getline(cin, str):读取整行(含空格)。- 关系运算符:支持字典序比较。
string name;
girl(cin, name); // 读取一行
if (name > "Alice") cout << "Name is later";
6)不同编译器下的结构差异
不同编译器的 string 实现略有不同,以 32 位平台为例:
- VS 环境:通常占用 28 字节。内部包含联合体,短字符串(<16 字符)使用内部缓冲,长字符串堆分配。还包含长度、容量字段及辅助指针。
- G++ 环境:通常采用写时拷贝(COW)机制,占用较小(如 4 字节指针),指向堆上包含长度、容量、引用计数及数据的结构。
2.3)string 类的模拟实现
面试中常要求手写 String 类,核心在于正确管理资源。重点考察构造函数、析构函数、拷贝构造函数及赋值运算符重载。
1)浅拷贝与深拷贝问题
初学者常忽略拷贝构造函数,导致默认生成浅拷贝,引发重复释放(Double Free)崩溃。
class String {
public:
String(const char* str = nullptr) {
if (str == nullptr) assert(false);
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
~String() {
delete[] _str;
}
private:
char* _str;
};
// 测试
String s1("hello");
String s2(s1); // 触发默认拷贝构造,浅拷贝
问题分析:
s1 和 s2 的 _str 指向同一块堆内存。当 s2 析构释放内存后,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) {
char* Pstr = new char[strlen(s._str) + 1];
strcpy(Pstr, s._str);
delete[] _str;
_str = Pstr;
}
return *this;
}
现代写法(异常安全): 利用拷贝交换原则(Copy-and-Swap)。
String& operator=(String s) { // 传值,自动调用拷贝构造
swap(_str, s._str); // 交换资源
return *this; // s 析构时释放旧资源
}
此写法不仅代码简洁,还能天然处理自赋值情况,且具备异常安全性。
3)写时拷贝(COW)
概念: 结合浅拷贝的高效与深拷贝的安全。读操作共享内存,写操作时才真正复制数据。
机制:
- 初始状态:多对象共享同一内存页,权限设为只读。
- 触发写入:检测到写操作时,触发缺页异常,内核分配新内存并复制数据。
- 继续执行:修改在新内存上进行,原数据保持不变。
应用场景:
- Linux fork():父子进程共享内存,仅修改时复制,提升进程创建速度。
- Redis bgsave:子进程持久化时共享父进程内存,减少阻塞。
- Java CopyOnWriteArrayList:读多写少场景下的线程安全 List。
- Docker/ZFS:镜像分层存储,容器层修改时复制文件。
优缺点:
- 优点:节省内存,提升读性能,并发读安全。
- 缺点:写操作延迟不可控,存在写放大风险(如 Java 中修改一个元素需复制整个数组)。
2.4)面试中的正确实现要点
实现 String 类需满足以下要求:
- 支持基本赋值与复制。
- 可作为函数参数及返回值。
- 可作为 STL 容器(如 vector)的元素类型。
关键在于遵循'三法则'(Rule of Three):若定义了析构函数,通常也需定义拷贝构造函数和赋值运算符。
2.5)STL string 的现状
现代 C++ 标准库的 string 实现已高度优化,部分版本不再使用传统的 COW 策略(因多线程环境下引用计数维护复杂),转而采用更安全的独占所有权模型。但在理解底层原理时,COW 依然是重要的知识储备。
本文涵盖了 string 类的核心接口、内存管理机制及模拟实现细节。理解这些内容有助于编写更安全、高效的 C++ 代码。

