C++ 标准库 string 类详解
一、为什么学习 string 类?
在 C 语言中,字符串是以 char* 形式存在的字符数组,需要手动管理内存和终止符。而在 C++ 中,std::string 类封装了这些细节,提供了更安全、便捷的字符串处理能力。
掌握 string 类不仅是日常开发的基础,也是面试中的高频考点。常见的面试题包括字符串转换整数、字符串相加等算法题,以及考察底层实现的模拟实现问题。
二、C++ 标准库中的 string 类
在使用 string 类时,必须包含 <string> 头文件并使用 using namespace std;。
2.1)auto 和范围 for
auto 关键字
auto 的核心功能是让编译器通过初始值来推导变量的类型。这意味着使用 auto 时,变量必须初始化。
auto x = 10; // x 被推导为 int
auto y = 3.14; // y 被推导为 double
auto ptr = &x; // ptr 被推导为 int*
用 auto 声明指针类型时,auto 和 auto* 没有区别;但声明引用类型时必须加 &。
int x = 10;
int& ref = x;
auto c = ref; // c 的类型是 int (引用被丢弃,发生值拷贝)
auto& d = ref; // d 的类型是 int& (显式声明为引用)
d = 30; // x 变成了 30
c = 20; // x 依然是 10,因为 c 是独立变量
注意,在同一行声明多个变量时,它们必须是相同的类型。auto 不能作为函数参数(C++17 以前),建议谨慎用作返回值。
范围 for 循环
C++11 引入了基于范围的 for 循环,语法结构如下:
for(declaration : range) {
// 循环体
}
- 第一部分 (
declaration): 用于存放当前迭代到的元素。 - 第二部分 (
range): 要遍历的范围(如数组、容器)。
示例:
#include <iostream>
#include <string>
#include <vector>
using namespace std;
int main() {
int array[] = {1, 2, 3, 4, 5};
// C++98 的遍历方式
for(int i = 0; i < sizeof(array)/sizeof(array[0]); ++i) {
array[i] *= 2;
}
// C++11 的遍历方式
for(auto& e : array) e *= 2; // 使用引用修改原始数据
for(auto e : array) cout << e << " ";
string str("hello world");
for(char ch : str) {
cout << ch << " ";
}
cout << endl;
return 0;
}
范围 for 不仅能作用于数组,也能作用于容器对象(如 std::string)。对于 string,循环会自动调用 begin() 和 end(),无需手动管理迭代器。
2.2)string 类的常用接口
1)构造方法
| 构造函数 | 功能说明 |
|---|---|
string() | 构造空的 string 类对象 |
string(const char* s) | 用 C-string 来构造 string 类对象 |
string(size_t n, char c) | 构造包含 n 个字符 c 的对象 |
string(const string& s) | 拷贝构造函数 |
#include <iostream>
#include <string>
using namespace std;
int main() {
string s1; // 空字符串
string s2("Hello World"); // 由字符串常量构造
string s3(10, '*'); // 10 个星号
string s4(s2); // 拷贝构造
return 0;
}
2)容量操作
| 函数名称 | 功能说明 |
|---|---|
size() / length() | 返回字符串有效字符长度 |
capacity() | 返回空间总大小 |
empty() | 检测字符串是否为空串 |
clear() | 清空有效字符 |
reserve() | 为字符串预留空间 |
resize() | 改变有效字符个数 |
注意事项:
size()与length()底层原理相同,为了与其他容器接口保持一致。clear()只清空有效字符,不改变底层空间大小(capacity)。resize(n, c)改变有效字符个数,多出的空间用字符c填充。reserve(res_arg)只为字符串预留空间,不改变有效元素个数;只有当参数大于当前底层空间时才会扩容。
3)访问及遍历操作
| 函数名称 | 功能说明 |
|---|---|
operator[pos] | 下标访问,可修改 |
begin() + end() | 正向迭代器 |
rbegin() + rend() | 反向迭代器 |
范围 for | C++11 新遍历方式 |
#include <iostream>
#include <string>
using namespace std;
int main() {
string s = "Hello";
// 下标访问
cout << "下标为 1 的字符:" << s[1] << endl;
s[0] = 'h';
// 迭代器遍历
cout << "正向迭代器遍历:";
for(string::iterator it = s.begin(); it != s.end(); ++it) {
cout << *it << " ";
}
cout << endl;
// 反向迭代器遍历
cout << "反向迭代器遍历:";
for(string::reverse_iterator rit = s.rbegin(); rit != s.rend(); ++rit) {
cout << *rit << " ";
}
cout << endl;
// 范围 for
cout << "范围 for 遍历:";
for(char c : s) {
cout << c << "-";
}
cout << endl;
return 0;
}
Iterator 小拓展: 最常用的四种组合:
iterator:能读能改,正着走。const_iterator:只能读,正着走。reverse_iterator:能读能改,倒着走。const_reverse_iterator:只能读,倒着走。
4)修改操作
| 函数名称 | 功能说明 |
|---|---|
push_back | 尾插单个字符 |
append | 追加字符串 |
operator+= | 追加字符串或字符 |
c_str() | 返回 C 格式字符串 |
find() + npos | 查找子串 |
substr() | 截取子串 |
#include <iostream>
using namespace std;
int main() {
string s = "Hello";
// push_back
s.push_back('!');
// append
s.append(" Welcome");
// operator+= (最常用)
s += " to C++";
// c_str
const char* cStr = s.c_str();
cout << "C-Style: " << cStr << endl;
// find
size_t pos = s.find("Welcome");
if(pos != string::npos) {
cout << "Found at: " << pos << endl;
}
// substr
string sub = s.substr(7, 7);
cout << "Sub-string: " << sub << endl;
return 0;
}
注意事项:
- 追加字符有三种方式:
push_back(c)、append(n, c)、+=。其中+=最灵活且常用。 - 如果预估要存较多字符,先调用
reserve()预留空间可以减少频繁扩容带来的性能消耗。
5)非成员函数
| 函数 | 功能说明 |
|---|---|
operator+ | 连接字符串(尽量少用,传值返回效率低) |
operator>> | 输入运算符重载 |
operator<< | 输出运算符重载 |
getline() | 获取一行字符串 |
| 关系运算符 | 大小比较 |
string full_name;
cin >> full_name; // 遇到空格停止
gline(cin, full_name); // 读取整行
string str1 = "apple";
string str2 = "banana";
if(str1 < str2) {
cout << "apple 排在 banana 前面" << endl;
}
6)VS 和 g++ 下 string 结构的说明
不同编译器对 string 的内部实现可能不同。以 32 位平台为例:
- VS 下:
string占 28 字节。内部有一个联合体,当字符串长度小于 16 时使用内部固定数组,大于等于 16 时从堆上开辟空间。此外还有保存长度和容量的字段。 - G++ 下:通常采用写时拷贝(COW)机制。
string对象占 4 个字节,包含一个指针指向堆空间,堆空间中存储空间大小、有效长度、引用计数等信息。
2.3)string 类的模拟实现
在面试中,经常要求模拟实现 string 类,重点在于构造、拷贝构造、赋值运算符重载和析构函数。
1)经典的 string 类问题
初学者容易忽略拷贝构造函数,导致浅拷贝问题。
class String {
public:
String(const char* str = nullptr) {
if(nullptr == str) {
assert(false);
return;
}
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
~String() {
if(_str) {
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
void TestString() {
String s1("hello bit!!!");
String s2(s1); // 触发隐式拷贝构造
}
问题分析:
s1创建时在堆上申请内存,_str指向该地址。s2创建时,编译器生成默认拷贝构造函数,执行浅拷贝,将s1._str的地址直接赋给s2._str。- 析构时,
s2先释放内存,随后s1再次释放同一块内存,导致程序崩溃(Double Free)。
解决方案: 手动实现深拷贝的拷贝构造函数。
String(const String& s) {
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
2)浅拷贝 VS 深拷贝
- 传统写法:在赋值运算符重载中,先检查自赋值,再分配新内存,复制数据,释放旧内存。
- 现代写法:利用交换法(Copy-and-Swap)。通过传值参数构造临时对象,然后交换资源。这样既利用了移动语义的效率,又保证了异常安全。
class String {
public:
/*拷贝构造函数*/
String(const String& s) : _str(nullptr) {
String strTmp(s._str);
swap(_str, strTmp._str);
}
/*赋值运算符重载 (传值方式)*/
String& operator=(String s) {
std::swap(_str, s._str);
return *this;
}
/*析构函数*/
~String() {
if(_str) {
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
3)写时拷贝(COW)
什么是写时拷贝? COW 结合了浅拷贝和深拷贝的优点。读操作时共享内存,写操作时才真正分配新内存并复制数据。核心思想是延迟执行。
工作机制:
- 初始状态:进程 A 拥有内存页 Page 1。
- 发生拷贝(如 fork):进程 B 也指向 Page 1,权限设为只读。
- 安全读取:A 和 B 共享数据,无开销。
- 触发写入:B 尝试修改 Page 1,触发缺页异常。
- 内核介入:内核分配新物理内存 Page 2,复制数据,映射给 B,权限恢复为读写。
- 继续执行:B 在 Page 2 上修改,A 的数据保持不变。
应用场景:
- Linux fork():父子进程初始共享内存,修改时才拷贝,极大提升进程创建速度。
- Redis bgsave:主线程 fork 子进程持久化,利用 COW 避免阻塞主线程。
- Java CopyOnWriteArrayList:读多写少场景下的线程安全 List,写时复制数组。
- 容器技术:Docker 镜像分层存储,启动容器时共享底层镜像层,修改时复制到可写层。
优缺点总结:
- 优点:节省内存/磁盘资源,提升性能,并发读安全。
- 缺点:不可预知的延迟(写时触发),写放大开销(如 Java 中修改一个元素需复制整个数组)。
2.4)面试中 string 的一种正确写法
面试实现 String 类时,至少应满足以下要求:
- 支持定义变量、赋值、复制。
- 能用作函数参数及返回类型。
- 能作为标准库容器(如
vector)的元素类型。
2.5)STL 中的 string 类怎么了?
随着 C++ 标准的演进,std::string 的实现也在不断优化。早期版本可能存在性能瓶颈或特定平台的差异,现代编译器通常针对常见场景进行了深度优化(如短字符串优化 SSO)。理解其底层原理有助于我们在实际开发中做出更合适的选择。
结语
std::string 是 C++ 中最常用的工具之一。深入理解其接口、内存管理及实现原理,不仅能写出更高效的代码,也能从容应对各类技术面试。


