C++ 标准库 string 类详解与模拟实现
为什么学习 string 类
在 C 语言中,字符串是以 \0 结尾的字符集合。虽然标准库提供了一些 str 系列函数,但这些函数与字符串本身是分离的,不太符合面向对象(OOP)的思想。更重要的是,底层空间需要用户自己管理,容易出现越界访问等安全隐患。
C++ 引入 string 类正是为了解决这些问题,它封装了内存管理、大小计算等操作,让开发者能更安全、高效地处理文本。
基础语法与工具
auto 关键字与范围 for
auto 关键字
在 C++11 之前,auto 表示自动存储期,意义不大。C++11 重新定义了它:auto 不再是一个存储类型指示符,而是作为类型推导指示符。编译器会在编译期根据初始化表达式推导变量类型。
- 指针与引用:用
auto声明指针时,auto和auto*没有区别;但声明引用时必须加&。 - 多变量声明:同一行声明多个变量时,它们必须是相同的类型,否则编译器会报错。
- 限制:
auto不能作为函数参数,可以做返回值但需谨慎使用;不能直接用来声明数组。
#include <iostream>
#include <typeinfo>
using namespace std;
int fun1() {
return 1;
}
void func2(auto a) {} // 编译报错,auto 不能做参数
auto func3() {
return 3; // 可以返回 auto,但建议谨慎使用
}
int main() {
int a = 1;
auto b = a;
auto c = 'a';
auto d = fun1();
// auto e; // 编译报错,必须具有初始值设定项
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
int x = 1;
auto y = &x;
auto *z = &x;
auto &m = x;
// auto cc = 3, dd = 4.0; // 编译报错,类型不一致
return 0;
}
范围 for 循环
C++11 引入了基于范围的 for 循环,简化了集合遍历。括号由冒号分为两部分:迭代变量和被迭代范围。它会自动处理迭代器,无需手动判断结束条件。
#include <iostream>
#include <string>
using namespace std;
int main() {
int arr[] = {1, 2, 3, 4, 5};
// C++98 写法
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
arr[i]++;
cout << arr[i] << " ";
}
cout << endl;
// C++11 写法
for (auto &e : arr) { // 引用修改原数组
e++;
cout << e << " ";
}
cout << endl;
string str("hello world");
for (auto c : str) {
cout << c << " ";
}
cout << endl;
return 0;
}
string 类常用接口
1. 构造方式
| 函数名称 | 功能说明 |
|---|---|
string() | 构造空的 string 对象 |
string(const char* s) | 用 C 风格字符串构造 |
string(size_t n, char c) | 包含 n 个字符 c |
string(const string& s) | 拷贝构造 |
void Test() {
string s1; // 空字符串
string s2("hello world"); // C 字符串构造
string s3(s2); // 拷贝构造
}
2. 容量操作
| 函数名称 | 功能说明 |
|---|---|
size() / length() | 返回有效字符长度 |
capacity() | 返回总空间大小 |
empty() | 检测是否为空串 |
clear() | 清空有效字符,不释放空间 |
reserve() | 预留空间,不改变有效元素个数 |
resize() | 调整有效字符个数,可指定填充字符 |
注意:
size()与length()原理相同,为了与其他容器接口一致,通常用size()。clear()只清空内容,底层空间不变。resize(n)扩容时用\0填充,resize(n, c)用字符c填充。reserve()若参数小于当前容量,则不改变容量。
3. 访问及遍历
| 函数名称 | 功能说明 |
|---|---|
operator[] | 返回指定位置字符 |
begin() + end() | 获取首尾迭代器 |
rbegin() + rend() | 获取反向迭代器 |
| 范围 for | C++11 简洁遍历方式 |
4. 修改操作
| 函数名称 | 功能说明 |
|---|---|
push_back() | 尾部插入字符 |
append() | 追加字符串 |
operator+= | 追加字符串或字符 |
c_str() | 返回 C 风格字符串指针 |
find() / rfind() | 查找字符/子串位置 |
substr() | 截取子串 |
提示:
- 尾部追加字符时,
s.push_back(c)、s.append(1, c)和s += 'c'效果类似,+=最常用。- 若能预估字符数量,先用
reserve()预留空间可减少内存重分配开销。
5. 非成员函数
| 函数名称 | 功能说明 |
|---|---|
operator+ | 连接字符串(传值效率低,尽量少用) |
operator>> | 输入运算符重载 |
operator<< | 输出运算符重载 |
getline() | 获取一行字符串 |
| 关系运算符 | 大小比较 |
底层结构差异
不同编译器下 string 的内部实现有所不同,以 32 位平台为例:
VS 下的结构
VS 的 string 占用 28 字节。采用短字符串优化(SSO):
- 当字符串长度小于 16 时,使用内部固定字符数组存放,无需堆分配。
- 当长度大于等于 16 时,从堆上开辟空间。
- 结构体包含联合体、长度字段、容量字段及其他指针信息。
g++ 下的结构
g++ 的 string 通常采用写时拷贝(Copy-On-Write),对象本身仅占 4 字节(一个指针)。
- 指针指向堆上的控制块,包含:空间总大小、有效长度、引用计数。
- 这种设计在多线程环境下需注意线程安全问题。
string 类的模拟实现
面试中常要求手写 string 类,核心在于掌握资源管理规则(Rule of Three/Five)。
浅拷贝问题
如果未显式定义拷贝构造函数和赋值运算符,编译器会生成默认的浅拷贝。这会导致两个对象共享同一块堆内存,析构时发生重复释放,引发程序崩溃。
class String {
public:
String(const char* str = "") {
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;
};
// 测试代码中若进行 String s2(s1),将导致浅拷贝错误
深拷贝解决方案
每个对象应拥有独立的资源副本。
传统写法
显式实现拷贝构造函数、赋值运算符重载和析构函数。
class String {
public:
String(const char* str = "") {
if (nullptr == str) {
assert(false);
return;
}
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
// 拷贝构造
String(const String& s) : _str(new char[strlen(s._str) + 1]) {
strcpy(_str, s._str);
}
// 赋值运算符重载
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;
}
~String() {
if (_str) {
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
现代写法(异常安全)
利用临时对象和 swap 提高效率,减少内存泄漏风险。
class String {
public:
String(const char* str = "") {
if (nullptr == str) {
assert(false);
return;
}
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
// 拷贝构造
String(const String& s) : _str(new char[strlen(s._str) + 1]) {
strcpy(_str, s._str);
}
// 赋值运算符重载 (右值引用版本)
String& operator=(String& s) {
swap(_str, s._str);
return *this;
}
// 赋值运算符重载 (常量引用版本)
String& operator=(const String& s) {
if (this != &s) {
String temp(s);
swap(_str, temp._str);
}
return *this;
}
~String() {
if (_str) {
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};


