C++ string类详解:常用接口、字符串操作与模拟实现

C++ string 类详解:常用接口、字符串操作与模拟实现

文章目录

  • [C++ `string` 类详解:常用接口、字符串操作与模拟实现](#C++ string 类详解:常用接口、字符串操作与模拟实现)
    • [1 `string` 类为什么重要](#1 string 类为什么重要)
  • [2 `auto` 与范围 `for`](#2 auto 与范围 for)
    • [2.1 `auto`](#2.1 auto)
      • 指针
      • 引用
      • [同一条语句中的多个 `auto`](#同一条语句中的多个 auto)
      • [`auto` 的限制](#auto 的限制)
  • [3 范围 `for`](#3 范围 for)
  • [4 `string` 的构造](#4 string 的构造)
    • [4.1 默认构造](#4.1 默认构造)
    • [4.2 使用 C 风格字符串构造](#4.2 使用 C 风格字符串构造)
    • [4.3 构造指定数量的字符](#4.3 构造指定数量的字符)
    • [4.4 拷贝构造](#4.4 拷贝构造)
  • [5 `string` 的容量操作](#5 string 的容量操作)
    • [5.1 `size()`](#5.1 size())
    • [5.2 `length()`](#5.2 length())
    • [5.3 `capacity()`](#5.3 capacity())
    • [5.4 `empty()`](#5.4 empty())
    • [5.5 `clear()`](#5.5 clear())
  • [6 `reserve()`](#6 reserve())
  • [7 `resize()`](#7 resize())
  • [8 `string` 的访问与遍历](#8 string 的访问与遍历)
    • [8.1 `operator\[\]`](#8.1 operator[])
    • [8.2 迭代器遍历](#8.2 迭代器遍历)
    • [8.3 反向迭代器](#8.3 反向迭代器)
    • [8.4 范围 `for`](#8.4 范围 for)
  • [9 `string` 的修改操作](#9 string 的修改操作)
    • [9.1 `push_back()`](#9.1 push_back())
    • [9.2 `append()`](#9.2 append())
    • [9.3 `operator+=`](#9.3 operator+=)
  • [10 `c_str()`](#10 c_str())
  • [11 `find()` 与 `npos`](#11 find() 与 npos)
    • [11.1 指定开始位置](#11.1 指定开始位置)
    • [11.2 `rfind()`](#11.2 rfind())
  • [12 `substr()`](#12 substr())
  • [13 `string` 的输入输出](#13 string 的输入输出)
    • [13.1 `operator>>`](#13.1 operator>>)
    • [13.2 `getline()`](#13.2 getline())
  • [14 `string` 的比较](#14 string 的比较)
  • [15 `operator+` 为什么不建议大量使用](#15 operator+ 为什么不建议大量使用)
  • [16 `string` 的底层结构](#16 string 的底层结构)
    • [16.1 小字符串优化](#16.1 小字符串优化)
    • [16.2 不同实现不能混为一谈](#16.2 不同实现不能混为一谈)
  • [17 `string` 高频算法思想](#17 string 高频算法思想)
  • [18 仅反转字母](#18 仅反转字母)
  • [19 找字符串中第一个只出现一次的字符](#19 找字符串中第一个只出现一次的字符)
  • [20 最后一个单词的长度](#20 最后一个单词的长度)
  • [21 判断回文字符串](#21 判断回文字符串)
  • [22 字符串相加](#22 字符串相加)
  • [23 `string` 模拟实现](#23 string 模拟实现)
  • [24 一个存在问题的 `String`](#24 一个存在问题的 String)
  • [25 浅拷贝](#25 浅拷贝)
  • [26 浅拷贝为什么会导致崩溃](#26 浅拷贝为什么会导致崩溃)
  • [27 深拷贝](#27 深拷贝)
  • [28 传统方式实现深拷贝](#28 传统方式实现深拷贝)
    • [28.1 拷贝构造](#28.1 拷贝构造)
    • [28.2 赋值运算符](#28.2 赋值运算符)
    • [28.3 析构函数](#28.3 析构函数)
  • [29 现代写法:利用 `swap`](#29 现代写法:利用 swap)
  • [30 写时拷贝](#30 写时拷贝)
    • [30.1 引用计数](#30.1 引用计数)
    • [30.2 写操作时复制](#30.2 写操作时复制)
  • [31 `string` 学习中的核心联系](#31 string 学习中的核心联系)

1 string 类为什么重要

C 语言中的字符串本质上是以 '\0' 结尾的字符数组

例如

cpp 复制代码
char str[] = "hello";

内存中实际保存的是

text 复制代码
h e l l o \0

C 语言提供了 strlen strcpy strcmp strcat 等字符串函数进行操作

这种方式存在两个比较明显的问题

第一,字符串本身和操作字符串的函数是分离的

第二,字符数组的空间需要程序员自己管理,稍不注意就容易发生越界

C++ 提供了 string 类,将字符串和相关操作封装到一个类中

cpp 复制代码
#include <string>
using namespace std;

之后可以直接通过对象调用字符串相关操作

cpp 复制代码
string s = "hello";

cout << s.size() << endl;
cout << s[0] << endl;

在实际 C++ 编程以及 OJ 题目中,字符串处理通常直接使用 string


2 auto 与范围 for

这两个语法并不是 string 本身的内容,但是在使用 string 时非常常见

2.1 auto

C++11 中,auto 可以让编译器根据初始化表达式自动推导变量类型

cpp 复制代码
int a = 10;

auto b = a;
auto c = 'a';

编译器会根据右边的表达式推导

text 复制代码
b -> int
c -> char

auto 声明的变量必须初始化

cpp 复制代码
auto a = 10;   // 正确

auto b;        // 错误

因为编译器必须根据初始化表达式推导类型

指针

cpp 复制代码
int x = 10;

auto y = &x;
auto* z = &x;

这两种写法得到的都是 int*

也就是说

cpp 复制代码
auto y = &x;

等价于

cpp 复制代码
int* y = &x;

引用

引用必须使用 &

cpp 复制代码
int x = 10;

auto& y = x;

此时 y 是 x 的引用

如果写成

cpp 复制代码
auto y = x;

那么得到的是一个新的 int 变量,而不是引用

同一条语句中的多个 auto

同一条声明中的多个变量必须推导出相同类型

cpp 复制代码
auto a = 1;
auto b = 2;

可以

但是

cpp 复制代码
auto a = 1, b = 2.0;

不可以

因为 a 推导为 int,b 推导为 double

auto 的限制

auto 不能直接作为普通函数参数

cpp 复制代码
void func(auto a)
{
}

这种写法不成立

auto 可以作为函数返回值

cpp 复制代码
auto func()
{
    return 10;
}

但实际编程中通常需要根据情况使用,不要为了使用 auto 而使用 auto

auto 也不能直接用来声明数组

cpp 复制代码
auto arr[] = {1, 2, 3};

这种写法不成立


3 范围 for

传统遍历数组需要自己控制下标

cpp 复制代码
int arr[] = {1, 2, 3, 4, 5};

for (int i = 0; i < 5; ++i)
{
    cout << arr[i] << endl;
}

C++11 提供了范围 for

cpp 复制代码
for (auto e : arr)
{
    cout << e << endl;
}

基本形式是

cpp 复制代码
for (元素变量 : 被遍历对象)
{
    // 循环体
}

例如遍历 string

cpp 复制代码
string s = "hello";

for (auto ch : s)
{
    cout << ch << endl;
}

每次循环都会依次取出字符串中的一个字符

如果需要修改原字符串中的字符,可以使用引用

cpp 复制代码
for (auto& ch : s)
{
    ch = 'A';
}

这里的关键区别是

cpp 复制代码
auto ch

得到的是元素的副本

而

cpp 复制代码
auto& ch

得到的是元素本身的引用

范围 for 本质上仍然可以理解成通过迭代器完成遍历


4 string 的构造

4.1 默认构造

cpp 复制代码
string s;

创建一个空字符串

text 复制代码
s = ""

4.2 使用 C 风格字符串构造

cpp 复制代码
string s("hello");

也可以写成

cpp 复制代码
string s = "hello";

本质上都是根据 C 风格字符串构造 string


4.3 构造指定数量的字符

cpp 复制代码
string s(5, 'a');

结果

text 复制代码
aaaaa

也就是构造一个包含 5 个 'a' 的字符串


4.4 拷贝构造

cpp 复制代码
string s1("hello");
string s2(s1);

或者

cpp 复制代码
string s2 = s1;

此时 s2 是根据 s1 构造出来的 string 对象


5 string 的容量操作

常用接口主要有

cpp 复制代码
size()
length()
capacity()
empty()
clear()
reserve()
resize()

5.1 size()

获取字符串有效字符的数量

cpp 复制代码
string s = "hello";

cout << s.size() << endl;

结果为

text 复制代码
5

注意

size() 计算的是有效字符数量,不包括字符串结尾意义上的 '\0'


5.2 length()

cpp 复制代码
string s = "hello";

cout << s.length() << endl;

结果同样是

text 复制代码
5

size() 和 length() 的底层实现原理相同

之所以同时存在,是因为 STL 中其他容器通常使用 size() 表示元素数量

因此平时使用 size() 更常见


5.3 capacity()

cpp 复制代码
string s = "hello";

cout << s.capacity() << endl;

capacity() 表示当前底层空间能够容纳多少有效字符

它和 size() 不一样

例如

text 复制代码
size     -> 当前有多少字符
capacity -> 当前空间最多能够容纳多少字符

可以理解为

text 复制代码
size <= capacity

5.4 empty()

判断字符串是否为空

cpp 复制代码
string s;

if (s.empty())
{
    cout << "empty" << endl;
}

如果字符串中没有有效字符,则返回 true

否则返回 false


5.5 clear()

清空字符串中的有效字符

cpp 复制代码
string s = "hello";

s.clear();

此时

cpp 复制代码
s.size() == 0

但是要注意

clear() 只是清空有效字符,不会主动释放底层容量

也就是说

text 复制代码
size 可能变成 0
capacity 通常保持不变

这也是为什么之后再次向字符串中添加数据时,可以继续利用之前申请的空间


6 reserve()

reserve() 用来提前申请空间

例如能够大概确定字符串最终会存放很多字符

cpp 复制代码
string s;

s.reserve(1000);

可以提前为字符串预留空间

重要的一点是

cpp 复制代码
reserve()

只改变容量,不改变有效字符数量

例如

cpp 复制代码
string s = "hello";

s.reserve(1000);

此时

text 复制代码
size 仍然是 5
capacity 至少能够容纳相应数量的字符

如果 reserve() 传入的值小于当前容量,一般不会导致容量缩小


7 resize()

resize() 用来改变字符串中有效字符的数量

例如

cpp 复制代码
string s = "hello";

s.resize(3);

结果

text 复制代码
hel

如果新的长度比原来小

text 复制代码
有效字符减少
容量通常不会因此减少

如果新的长度比原来大

cpp 复制代码
string s = "hello";

s.resize(8);

会增加有效字符

使用

cpp 复制代码
resize(n)

增加出来的字符使用 '\0' 填充

也可以指定填充字符

cpp 复制代码
s.resize(8, 'x');

则会使用 'x' 填充新增部分

因此

cpp 复制代码
resize(10)

和

cpp 复制代码
resize(10, 'a')

的区别主要就在新增字符的填充方式


8 string 的访问与遍历

8.1 operator[]

通过下标访问字符串中的字符

cpp 复制代码
string s = "hello";

cout << s[0] << endl;

输出

text 复制代码
h

可以通过下标修改字符

cpp 复制代码
s[0] = 'H';

结果

text 复制代码
Hello

8.2 迭代器遍历

cpp 复制代码
string s = "hello";

string::iterator it = s.begin();

while (it != s.end())
{
    cout << *it << endl;
    ++it;
}

其中

cpp 复制代码
begin()

指向第一个字符

cpp 复制代码
end()

指向最后一个字符的下一个位置

因此遍历通常写成

cpp 复制代码
for (auto it = s.begin(); it != s.end(); ++it)
{
    cout << *it << endl;
}

8.3 反向迭代器

对应的反向迭代器是

cpp 复制代码
rbegin()
rend()

例如

cpp 复制代码
for (auto it = s.rbegin(); it != s.rend(); ++it)
{
    cout << *it << endl;
}

可以从字符串尾部向前遍历


8.4 范围 for

最简单的写法通常是

cpp 复制代码
for (auto ch : s)
{
    cout << ch;
}

如果需要修改字符串

cpp 复制代码
for (auto& ch : s)
{
    ch = 'x';
}

9 string 的修改操作

9.1 push_back()

在字符串尾部添加一个字符

cpp 复制代码
string s = "hello";

s.push_back('!');

结果

text 复制代码
hello!

9.2 append()

在字符串末尾追加内容

cpp 复制代码
string s = "hello";

s.append(" world");

结果

text 复制代码
hello world

也可以追加指定数量的字符

cpp 复制代码
s.append(3, 'a');

9.3 operator+=

实际使用中非常常见

cpp 复制代码
string s = "hello";

s += '!';
s += " world";

最终

text 复制代码
hello! world

+= 既可以追加单个字符,也可以追加字符串

所以实际编程中通常非常方便


10 c_str()

string 可以通过 c_str() 获取 C 风格字符串

cpp 复制代码
string s = "hello";

const char* str = s.c_str();

得到的是一个指向字符串内部字符数组的指针

它常用于需要 C 风格字符串的函数或接口

例如

cpp 复制代码
printf("%s\n", s.c_str());

这里不能直接把 string 当成普通的 char*

需要通过

cpp 复制代码
s.c_str()

取得 C 风格字符串


11 find() 与 npos

find() 用于查找字符或者字符串

例如

cpp 复制代码
string s = "hello world";

size_t pos = s.find('o');

返回找到的位置

字符串下标从 0 开始

如果没有找到,则返回

cpp 复制代码
string::npos

因此通常这样判断

cpp 复制代码
size_t pos = s.find('x');

if (pos == string::npos)
{
    cout << "not found" << endl;
}

npos 可以理解为

text 复制代码
没有找到

11.1 指定开始位置

cpp 复制代码
s.find('o', 5);

表示从下标 5 开始向后查找


11.2 rfind()

rfind() 与 find() 相似,但是搜索方向不同

cpp 复制代码
s.rfind('o');

从后往前寻找

因此

text 复制代码
find  -> 找第一个符合条件的位置
rfind -> 从后面开始找

12 substr()

substr() 用于截取子字符串

cpp 复制代码
string s = "hello world";

string sub = s.substr(6, 5);

得到

text 复制代码
world

基本形式

cpp 复制代码
s.substr(pos, len)

表示从 pos 开始,截取 len 个字符

例如

cpp 复制代码
string s = "abcdef";

string sub = s.substr(2, 3);

得到

text 复制代码
cde

13 string 的输入输出

13.1 operator>>

cpp 复制代码
string s;

cin >> s;

这种方式遇到空格会停止

例如输入

text 复制代码
hello world

最终

text 复制代码
s = "hello"

因为 >> 默认以空白字符作为分隔


13.2 getline()

如果需要读取整行字符串

cpp 复制代码
string s;

getline(cin, s);

输入

text 复制代码
hello world

那么

text 复制代码
s = "hello world"

所以处理包含空格的字符串时,经常使用 getline()


14 string 的比较

string 支持直接进行关系运算

例如

cpp 复制代码
string a = "abc";
string b = "abd";

if (a < b)
{
    cout << "a < b" << endl;
}

字符串比较按照字符的字典序进行

因此可以直接使用

cpp 复制代码
==
!=
<
>
<=
>=

这比 C 语言中的 strcmp() 使用起来更加直接


15 operator+ 为什么不建议大量使用

可以使用

cpp 复制代码
string c = a + b;

但是需要注意效率问题

operator+ 通常会产生新的字符串对象,并涉及返回对象以及相关资源处理

如果在循环中大量使用类似操作

cpp 复制代码
string s;

for (...)
{
    s = s + x;
}

可能产生较多额外开销

相比之下

cpp 复制代码
s += x;

通常更加直接

因此实际编程中字符串追加通常优先考虑 +=

如果能够提前知道大概需要多少空间,还可以配合

cpp 复制代码
reserve()

减少扩容带来的额外开销


16 string 的底层结构

不同编译器和标准库实现中的 string 内部结构并不完全相同

有些地方的结构是在特定平台和实现下观察得到的,不能把它当成 C++ 标准规定的固定布局

一个非常重要的思想是

string 的本质仍然是对一块字符存储空间的封装


16.1 小字符串优化

某些实现会采用小字符串优化

也就是字符串很短的时候,直接利用 string 对象内部的固定空间保存字符,而不是去堆上申请空间

例如书中的 VS 实现中

text 复制代码
字符串较短
    ↓
直接使用对象内部空间

字符串较长
    ↓
从堆上申请空间

这样做的目的主要是减少短字符串频繁进行动态内存申请带来的开销


16.2 不同实现不能混为一谈

某些旧版 G++ 实现中的写时拷贝机制

其中多个 string 对象可以共同使用同一份底层资源

同时通过引用计数记录资源的使用者数量

但具体 STL 实现可能随着编译器版本发生变化

因此学习这里时,更重要的是理解

text 复制代码
string
↓
管理字符资源
↓
需要处理空间申请
↓
需要处理拷贝
↓
需要处理资源释放

而不是死记某个版本的对象大小


17 string 高频算法思想

字符串题目中经常出现的核心思路包括

text 复制代码
双指针
字符统计
字符串遍历
模拟
逆序
查找
截取

18 仅反转字母

例如字符串

text 复制代码
a-bC-dEf-ghIj

要求只反转字母,其他字符位置保持不变

可以使用左右双指针

text 复制代码
begin →                    ← end

左边遇到非字母就向右移动

右边遇到非字母就向左移动

当两边都找到字母之后交换

核心代码

cpp 复制代码
while (begin < end)
{
    while (begin < end && !isLetter(S[begin]))
        ++begin;

    while (begin < end && !isLetter(S[end]))
        --end;

    swap(S[begin], S[end]);

    ++begin;
    --end;
}

判断字母可以自己写函数

cpp 复制代码
bool isLetter(char ch)
{
    if (ch >= 'a' && ch <= 'z')
        return true;

    if (ch >= 'A' && ch <= 'Z')
        return true;

    return false;
}

这种题最重要的是理解双指针如何跳过无效字符


19 找字符串中第一个只出现一次的字符

典型思路是

text 复制代码
第一次遍历
统计每个字符出现次数

第二次遍历
按照原字符串顺序寻找出现次数为 1 的字符

例如

cpp 复制代码
int count[256] = {0};

for (int i = 0; i < s.size(); ++i)
{
    count[s[i]]++;
}

for (int i = 0; i < s.size(); ++i)
{
    if (count[s[i]] == 1)
        return i;
}

return -1;

这里使用数组统计字符出现次数

因为字符可以直接作为数组下标使用

核心思想并不是 string 本身,而是

text 复制代码
统计 + 再遍历

20 最后一个单词的长度

假设输入

text 复制代码
hello world

最后一个单词是

text 复制代码
world

长度为

text 复制代码
5

可以使用

cpp 复制代码
getline(cin, line);

读取整行

然后使用

cpp 复制代码
rfind(' ')

寻找最后一个空格

cpp 复制代码
size_t pos = line.rfind(' ');

cout << line.size() - pos - 1 << endl;

这里要注意

如果题目允许字符串末尾存在空格,就需要进一步处理末尾空格


21 判断回文字符串

回文的特点是

text 复制代码
正着读和反着读相同

例如

text 复制代码
abcba

可以使用左右双指针

text 复制代码
begin →        ← end

比较

cpp 复制代码
s[begin]
s[end]

如果不同,直接返回 false

如果相同

cpp 复制代码
++begin;
--end;

继续向中间移动

如果题目要求忽略非字母数字字符,那么左右指针遇到这些字符时直接跳过

cpp 复制代码
while (begin < end && !isLetterOrNumber(s[begin]))
    ++begin;

while (begin < end && !isLetterOrNumber(s[end]))
    --end;

这种题的核心仍然是

text 复制代码
双指针 + 条件跳过 + 对称比较

22 字符串相加

两个非常大的数字不能直接使用普通整数类型保存

例如

text 复制代码
num1 = "123456789123456789"
num2 = "987654321987654321"

可以直接按照小学竖式加法的方式进行

从最后一位开始

text 复制代码
个位
↓
十位
↓
百位
↓
...

核心代码思想

cpp 复制代码
int end1 = num1.size() - 1;
int end2 = num2.size() - 1;

int next = 0;

while (end1 >= 0 || end2 >= 0)
{
    int value1 = 0;
    int value2 = 0;

    if (end1 >= 0)
        value1 = num1[end1--] - '0';

    if (end2 >= 0)
        value2 = num2[end2--] - '0';

    int value = value1 + value2 + next;

    if (value > 9)
    {
        next = 1;
        value -= 10;
    }
    else
    {
        next = 0;
    }

    result += value + '0';
}

由于我们是从低位向高位计算,而字符串最终要求从高位到低位排列,因此最后需要

cpp 复制代码
reverse(result.begin(), result.end());

这里有一个非常值得记住的转换

cpp 复制代码
num1[end1] - '0'

把数字字符转换成真正的数字

而

cpp 复制代码
value + '0'

把数字转换成对应的字符


23 string 模拟实现

学习 string 模拟实现的核心,并不是重新写一个完整 STL,而是理解

text 复制代码
构造函数
拷贝构造
赋值运算符
析构函数
动态内存管理
浅拷贝
深拷贝
写时拷贝

这些内容和 C++ 类的资源管理密切相关


24 一个存在问题的 String

例如

cpp 复制代码
class String
{
public:
    String(const char* str = "")
    {
        _str = new char[strlen(str) + 1];
        strcpy(_str, str);
    }

    ~String()
    {
        delete[] _str;
        _str = nullptr;
    }

private:
    char* _str;
};

表面上看起来没有问题

构造的时候申请空间

析构的时候释放空间

但是

cpp 复制代码
String s1("hello");
String s2(s1);

就会出现严重问题


25 浅拷贝

如果没有自己实现拷贝构造函数

编译器会生成默认拷贝构造

默认拷贝本质上是成员逐字节拷贝

于是

text 复制代码
s1
 ↓
+----------+
| 0x1000   |──────→ "hello"
+----------+

s2
 ↓
+----------+
| 0x1000   |──────→ "hello"
+----------+

两个对象中的 _str 指针保存的是同一个地址

也就是说

text 复制代码
s1._str == s2._str

两个对象共同管理同一块堆空间

这就是浅拷贝带来的资源管理问题


26 浅拷贝为什么会导致崩溃

假设

cpp 复制代码
String s1("hello");
String s2(s1);

最终

text 复制代码
s1 ─┐
    ├──→ 同一块堆空间
s2 ─┘

当 s2 析构时

cpp 复制代码
delete[] _str;

堆空间被释放

但是 s1._str 仍然保存着原来的地址

此时这个指针已经成为悬空指针

等 s1 再析构

cpp 复制代码
delete[] _str;

就可能对同一块空间再次释放

最终产生

text 复制代码
double free

等一系列严重问题

所以只要一个类自己管理动态资源,就必须认真考虑拷贝行为


27 深拷贝

深拷贝的思想非常简单

text 复制代码
每个对象拥有自己独立的资源

例如

text 复制代码
s1 ───→ "hello"

s2 ───→ "hello"

虽然两个字符串内容相同

但是底层空间不同

因此一个对象销毁时不会影响另一个对象


28 传统方式实现深拷贝

构造函数

cpp 复制代码
String(const char* str = "")
{
    if (nullptr == str)
    {
        assert(false);
        return;
    }

    _str = new char[strlen(str) + 1];
    strcpy(_str, str);
}

这里

cpp 复制代码
strlen(str) + 1

中的 +1 非常重要

因为还需要保存字符串结尾的

cpp 复制代码
'\0'

28.1 拷贝构造

cpp 复制代码
String(const String& s)
    : _str(new char[strlen(s._str) + 1])
{
    strcpy(_str, s._str);
}

这里重新申请了一块空间

然后把原字符串内容复制过去

因此两个对象拥有独立的资源


28.2 赋值运算符

cpp 复制代码
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;
}

这里必须注意

cpp 复制代码
if (this != &s)

它是在防止自己给自己赋值

例如

cpp 复制代码
s1 = s1;

如果没有这个判断,可能在复制数据之前就把原来的资源释放掉


28.3 析构函数

cpp 复制代码
~String()
{
    if (_str)
    {
        delete[] _str;
        _str = nullptr;
    }
}

负责释放构造函数申请的动态空间

这三个函数需要联系起来理解

text 复制代码
构造函数
    ↓
申请资源

拷贝构造
    ↓
复制资源

赋值运算符
    ↓
替换资源

析构函数
    ↓
释放资源

这就是典型的资源管理问题


29 现代写法:利用 swap

还可以利用临时对象和 swap 简化资源管理

拷贝构造可以写成

cpp 复制代码
String(const String& s)
    : _str(nullptr)
{
    String strTmp(s._str);
    swap(_str, strTmp._str);
}

赋值运算符可以写成

cpp 复制代码
String& operator=(String s)
{
    swap(_str, s._str);

    return *this;
}

这种写法的关键是

text 复制代码
先构造一个临时对象
↓
让临时对象拥有新的资源
↓
swap
↓
当前对象获得新资源
↓
临时对象获得旧资源
↓
临时对象析构
↓
自动释放旧资源

这样能够利用析构函数自动完成旧资源释放


30 写时拷贝

写时拷贝的核心思想是

text 复制代码
能共享就先共享
真正修改时再复制

它建立在浅拷贝的基础上,同时增加引用计数

例如有两个对象共同使用一块资源

text 复制代码
s1 ─┐
    ├──→ 同一份资源
s2 ─┘

引用计数 = 2

如果再增加一个对象

text 复制代码
s1 ─┐
s2 ─┼──→ 同一份资源
s3 ─┘

引用计数 = 3

30.1 引用计数

引用计数用于记录当前有多少个对象使用这份资源

基本过程

text 复制代码
创建资源
↓
引用计数 = 1

又有对象使用
↓
引用计数++

对象销毁
↓
引用计数--

引用计数归零
↓
释放资源

这样多个对象可以共享一份资源


30.2 写操作时复制

假设

text 复制代码
s1 ─┐
    ├──→ "hello"
s2 ─┘

此时两个对象共享资源

如果只是读取

cpp 复制代码
cout << s1;

没有必要复制

但如果

cpp 复制代码
s1[0] = 'H';

此时不能直接修改共享资源

因为这样会同时影响 s2

所以需要先复制一份

text 复制代码
修改前

s1 ─┐
    ├──→ "hello"
s2 ─┘


修改 s1

s1 ───→ "Hello"

s2 ───→ "hello"

这就是写时拷贝

不过现代 C++ 标准库实现中并不能简单地认为 std::string 一定使用这种方式,具体实现由标准库决定


31 string 学习中的核心联系

string 看起来只是一个字符串类,但实际上把很多 C++ 核心知识联系起来了

text 复制代码
string
│
├── 类与对象
│
├── 构造函数
│
├── 拷贝构造
│
├── 运算符重载
│
├── 动态内存管理
│
├── 深拷贝
│
├── 浅拷贝
│
├── 析构函数
│
├── 迭代器
│
├── 范围 for
│
├── auto
│
└── STL

而字符串算法本身最常见的思路主要集中在

text 复制代码
双指针
字符统计
字符串模拟
查找
截取
逆序

掌握这些之后,绝大多数基础字符串题都可以从这些思路出发进行分析

08.string 08.string

相关推荐
李游Leo1 小时前
HarmonyOS 7 + Spatial Recon Kit-C++:3DGS 高斯参数的非有限值隔离与可渲染性门禁【鸿蒙心迹】
开发语言·c++·3d·harmonyos
CHEEVEN_QY1 小时前
新能源汽车电池托盘FSW量产的工艺稳定性控制
java·前端·汽车
惊讶的猫1 小时前
MCP-server
java
忆挽篱笙歌1 小时前
gcc,g++
linux
ZealSinger1 小时前
Boot4升级PostConstruct不跑了
java·spring boot·spring·升级迁移
Code_Solitude1 小时前
C语言:关于二维数组的作业总结
java·c语言·前端
Wang's Blog1 小时前
Java 项目实战: 外卖平台优化-Nginx六种负载均衡策略对比与选型
java·nginx·负载均衡
时间的拾荒人1 小时前
Qt 文件操作详解:从基础类到读写实战
开发语言·qt·面试
Wang's Blog2 小时前
Java 项目实战: 外卖平台优化-YApi接口管理平台与文档导入导出
java·开发语言·yapi