【 C++ 入门】Cyber骇客的 流式文本序列处理器 —— 【 string 类】万字大文带你从0学好C++的string类!

【 C++ 入门】Cyber骇客的 流式文本序列处理器 —— 【 string 类】万字大文带你从0学好C++的string类!



⚡ CYBER_PROFILE ⚡
/// SYSTEM READY ///


[WARNING]: DETECTING HIGH ENERGY

🌊 🌉 🌊 心手合一 · 水到渠成

分隔符
>>> ACCESS TERMINAL <<<
[ 🦾 作者主页 ][ 🔥 C语言核心 ]
[ 💾 编程百度 ][ 📡 代码仓库 ]

---------------------------------------
Running Process: 100% | Latency: 0ms


索引与导读

一、为什么学习 string类 ?

C语言中的字符串

🔗Lucy的空间骇客裂缝:C语言字符与字符串函数
🔗Lucy的空间骇客裂缝:C语言字符串

字符串面试题

🔗Lucy的空间骇客裂缝:字符串转化为整数
🔗Lucy的空间骇客裂缝:字符串相加

二、C++ 标准库中的 string 类

🔗Lucy的空间骇客裂缝:C++ string类
  • 在使用string类时,必须包含#include头文件以及using namespace std;

2.1)auto和范围for

  • auto关键字
  1. 核心功能:auto的核心功能是
    让编译器通过初始值来推导变量的类型。这意味着使用 auto 时,变量必须初始化
auto x =10;// x 被推导为 intauto y =3.14;// y 被推导为 doubleauto ptr =&x;// ptr 被推导为 int*
  1. auto 声明指针类型时,用 autoauto* 没有任何区别,但用 auto 声明引用类型时则必须加 &
int x =10;int* p =&x;auto a = p;// a 被推导为 int*auto* b = p;// b 被推导为 int*// 结论:在这种情况下,a 和 b 的类型完全一致
int x =10;int& ref = x;// ref 是 x 的引用auto c = ref;// c 的类型是 int (注意:引用被丢弃了!这里是值拷贝)auto& d = ref;// d 的类型是 int& (显式声明为引用) c =20;// x 依然是 10,因为 c 是独立变量 d =30;// x 变成了 30,因为 d 是 x 的别名
  1. 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量
/*正确用法:类型一致*/auto a =10, b =20;// 正确:a 是 int,b 也是 intauto c =1.5, d =3.14;// 正确:c 是 double,d 也是 double// 注意:即使一个是值,一个是引用/指针,只要“基础类型”一致即可int x =0;auto i = x,&j = x;// 正确:基础类型都是 int/*错误用法:类型不一致*/// 错误示例:编译器会报错auto a =10, b =3.14;// 报错原因:编译器对 a 推导为 int,但对 b 推导为 double// 编译器实际上只对第一个类型(a)进行推导,然后尝试用 int 定义 b,导致类型冲突auto m =5,*p =&m;// 正确:基础类型都是 intauto n =5, q =3.14;// 错误:基础类型不同
  1. auto不能作为函数的参数,可以做返回值,但是建议谨慎使用
// 错误示例 (C++17 及以前)voidfunc(auto x){...}// 正确方案:模板template<typenameT>voidfunc(T x){...}
// 错误示例:推导冲突autocheck(int i){if(i >0)return1;// 推导为 intelsereturn3.14;// 错误!与之前的 int 冲突}
  1. auto不能直接用来声明数组
int arr[]={1,2,3};// 正确:传统的数组声明auto a[]={1,2,3};// 错误!编译器无法推导出 a 是一个数组类型auto b[3]={1,2,3};// 错误!即使指定了长度也不行

推导为指针

int nums[]={10,20,30};auto p = nums;// 正确:p 的类型被推导为 int*,而不是 int[3]/*同行声明一致性*/int nums[]={1,2};auto p = nums, x =10;// 正确:p 是 int*,x 是 int,基础类型一致auto p = nums, y =3.14;// 错误:基础类型冲突(int vs double)

如果你真的想推导数组
如果你希望保留数组的“大小信息”而不是让它退化成指针,可以使用引用:

int nums[]={1,2,3};auto& refArr = nums;// 正确:refArr 的类型是 int(&)[3]

  • for关键字
    • 对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误
    • 因此 C++11 中引入了基于范围的 for 循环
      for 循环后的括号由**冒号“:”**分为两部分:
      • 第一部分是范围内用于迭代的变量
      • 第二部分则表示被迭代的范围自动迭代自动取数据自动判断结束
  1. 语法结构
for(declaration : range){// 循环体}
  • 第一部分 (declaration): 用于存放当前迭代到的元素
  • 第二部分 (range): 要遍历的范围(如数组、std::vectorstd::string 等)
  1. 代码示例
int arr[]={1,2,3,4,5};for(int x : arr){ cout << x <<" ";// 自动取数据,自动判断结束}
  1. 范围 for 可以作用到数组和容器对象上进行遍历
  • 数组 (Arrays):指具有明确大小的静态数组。
  • 容器对象 (Containers):指标准库中的容器(如 std::vectorstd::liststd::mapstd::setstd::string 等)。
  1. 使用示例
#include<iostream>#include<string>#include<map>usingnamespace std;intmain(){int array[]={1,2,3,4,5};// C++98的遍历for(int i =0; i <sizeof(array)/sizeof(array[0]);++i){ array[i]*=2;}for(int i =0; i <sizeof(array)/sizeof(array[0]);++i){ cout << array[i]<< endl;}// C++11的遍历for(auto& e : array) e *=2;for(auto e : array) cout << e <<" "<< endl; string str("hello world");for(auto ch : str){ cout << ch <<" ";} cout << endl;return0;}

╔═█▓▒░ CODE CORE 🔥

┌─────────────┐
│ 代码关键点 │使用引用修改数据
└─────────────┘

// C++11的遍历for(auto& e : array) e *=2;
  • auto& ee 是当前元素的引用。使用引用是因为我们需要修改 array 中的原始值
  • ::分隔符,左边是变量,右边是范围(即数组 array)。

╔═█▓▒░ CODE CORE 🔥

┌─────────────┐
│ 代码关键点 │自动取数据并打印
└─────────────┘

for(auto e : array) cout << e <<" "<< endl;
  • auto e:编译器自动推导出e的类型为 int。每一轮循环,它会自动从 array 中取出下一个元素赋值给 e

╔═█▓▒░ CODE CORE 🔥

┌─────────────┐
│ 代码关键点 │作用于容器对象(string)
└─────────────┘


范围for不仅能作用于数组,也能作用于容器对象

string str("hello world");for(auto ch : str){ cout << ch <<" ";} cout << endl;
  • 对象str 是一个 std::string 容器。
  • 过程:循环会自动调用 str.begin()str.end(),依次提取字符并打印,无需手动管理迭代器或下标。

2.2)string类的常用接口

🚩1)string类的常用构造

构造函数功能说明
string()(重点)构造空的 string 类对象,即空字符串
string(const char* s)(重点)用 C-string 来构造 string 类对象
string(size_t n, char c)string 类对象中包含 n 个字符 c
string(const string& s)(重点)拷贝构造函数
#include<iostream>#include<string>// 包含 string 类的定义usingnamespace std;intmain(){// 1. string() (重点)// 构造空的 string 对象 string s1; cout <<"s1 (空字符串): ["<< s1 <<"]"<< endl;// 2. string(const char* s) (重点)// 用 C-string (字符串常量) 来构造对象 string s2("Hello World"); cout <<"s2 (由字符串常量构造): "<< s2 << endl;// 3. string(size_t n, char c)// 构造包含 n 个字符 c 的对象 string s3(10,'*'); cout <<"s3 (10个星号): "<< s3 << endl;// 4. string(const string& s) (重点)// 拷贝构造函数:用已有的 string 对象 s2 来初始化 s4 string s4(s2); cout <<"s4 (拷贝自s2): "<< s4 << endl;return0;}

🚩2)string类对象的容量操作

函数名称功能说明
size(重点)返回字符串有效字符长度
length返回字符串有效字符长度
capacity返回空间总大小
empty(重点)检测字符串是否为空串,是返回 true,否则返回 false
clear(重点)清空有效字符
reserve(重点)为字符串预留空间
resize(重点)将有效字符的个数改成 n 个,多出的空间用字符 c 填充
❗注意事项
1)size() 与 length()底层原理相同

size() 是为了与其他容器接口保持一致

string s ="hello"; cout <<"size: "<< s.size()<< endl;// 输出 5 cout <<"length: "<< s.length()<< endl;// 输出 5
2)clear() 是清空有效字符,但不改变底层空间大小(capacity)
3)resize(size_t n, char c)改变有效字符个数
s ="hello";// 如果 n 大于当前大小,用字符 'x' 填充 s.resize(10,'x'); cout <<"resize(10, 'x'): "<< s << endl;// helloxxxxx// 如果 n 小于当前大小,会发生截断,但底层总空间不变 s.resize(3); cout <<"resize(3): "<< s << endl;// hel
4)reserve(size_t res_arg) 为 string 预留空间,不改变有效元素个数;只有当参数大于当前底层空间时才会扩容
s.reserve(100); cout <<"reserve(100)后大小: "<< s.size()<< endl;// 依然是 3

🚩3)string类对象的访问及遍历操作

函数名称功能说明
operator[pos](重点)operator是字符串名,返回 pos 位置的字符,const string 类对象调用
begin + endbegin 获取一个字符的迭代器 + end 获取最后一个字符下一个位置的迭代器
rbegin + rendrbegin 获取一个字符的迭代器 + rend 获取最后一个字符下一个位置的迭代器
范围 forC++11 支持更简洁的范围 for 的新遍历方式
#include<iostream>#include<string>usingnamespace std;intmain(){ string s ="Hello";// 1. operator[] (下标访问)// 像数组一样访问 pos 位置的字符 cout <<"下标为1的字符: "<< s[1]<< endl;// 'e' s[0]='h';// 也可以修改// 2. 使用迭代器遍历 (begin + end)// begin 获取第一个字符位置,end 获取最后一个字符下一个位置 cout <<"正向迭代器遍历: ";for(string::iterator it = s.begin(); it != s.end();++it){ cout <<*it <<" ";} cout << endl;// 3. 反向迭代器遍历 (rbegin + rend) cout <<"反向迭代器遍历: ";for(string::reverse_iterator rit = s.rbegin(); rit != s.rend();++rit){ cout <<*rit <<" ";// 输出 o l l e h} cout << endl;// 4. 范围 for (C++11 遍历新方式) cout <<"范围 for 遍历: ";for(char c : s){ cout << c <<"-";} cout << endl;return0;}

╔═█▓▒░ CODE CORE 🔥

┌─────────────┐
│ 代码关键点 │string::iterator it = s.begin()
└─────────────┘

  • string:: 作用域限定符:告诉编译器:我要找 string 类里面定义的工具。
  • iterator 类型名:这是一个专门用来遍历字符串的“迭代器类型”。
  • it 变量名:你给这个“光标”取的名字(iterator 的缩写)。
  • s.begin() 初始赋值:调用字符串 s 的函数,把光标放在第一个字符的位置。

╔═█▓▒░ CODE CORE 🔥

┌─────────────┐
│ 代码关键点 │string::reverse_iterator rit = s.rbegin()
└─────────────┘

  • string::reverse_iterator:这是一个专门用于从后往前读字符串的类型。
  • rit:变量名,通常是 reverse iterator 的缩写。
  • s.rbegin():这里的 r 代表 reverse(反向)。这个函数返回指向字符串最后一个有效字符的指针。
iterator小拓展

对于 string 来说,你最常用的就是这四种组合:

  1. iterator:能读能改,正着走。
  2. const_iterator:只能读,正着走。
  3. reverse_iterator:能读能改,倒着走。
  4. const_reverse_iterator:只能读,倒着走。

🚩4)string类对象的修改操作

函数名称功能说明
push_back在字符串后尾插字符 c
append在字符串后追加一个字符串
operator+=(重点)在字符串后追加字符串 str
c_str(重点)返回 C 格式字符串
find + npos(重点)从字符串 pos 位置开始往后找字符 c,返回该字符在字符串中的位置
rfind从字符串 pos 位置开始往前找字符 c,返回该字符在字符串中的位置
substrstr 中从 pos 位置开始,截取 n 个字符,然后将其返回
#include<iostream>usingnamespace std;intmain(){// 0. 初始化 string s ="Hello";// 1. push_back: 只能插入单个字符 s.push_back('!');// s 变为 "Hello!"// 2. append: 追加字符串 s.append(" Welcome");// s 变为 "Hello! Welcome"// 3. operator+= (重点): 最常用的追加方式 s +=" to C++";// s 变为 "Hello! Welcome to C++"// 4. c_str (重点): 返回 C 风格字符串 (const char*)// 方便配合 printf 或旧的 C 函数使用constchar* cStr = s.c_str(); cout <<"C-Style: "<< cStr << endl;// 5. find + npos (重点): 查找子串// string::npos 是一个特殊值,表示“没找到” size_t pos = s.find("Welcome");if(pos != string::npos){ cout <<"Found 'Welcome' at: "<< pos << endl;}// 6. rfind: 从右向左找// 比如找最后一个空格的位置 size_t last_space = s.rfind(' '); cout <<"Last space at: "<< last_space << endl;// 7. substr: 截取子串// 参数为:(起始位置, 截取长度) string sub = s.substr(7,7);// 截取 "Welcome" cout <<"Sub-string: "<< sub << endl;return0;}
❗注意事项
1)追加字符的三种方式
char c ='!';// 方式 A: 使用 push_back(c)// 只能插入单个字符 s.push_back(c);// 方式 B: 使用 append(n, c)// 表示追加 n 个字符 c s.append(1, c);// 方式 C: 使用 += (最常用)// 既可以连接单个字符,也可以连接字符串 s += c;// 连接字符 s +=" Hello";// 连接字符串

2)使用 reserve 预留空间
string s;// 如果预估要存 50 个字符,先预留空间可以减少频繁扩容带来的性能消耗 s.reserve(50);
手动new VS reserve


🚩5)string类非成员函数

函数功能说明
operator+尽量少用,因为传值返回,导致深拷贝效率低
operator>>(重点)输入运算符重载
operator<<(重点)输出运算符重载
getline(重点)获取一行字符串
relational operators(重点)大小比较
🔗Lucy的空间骇客裂缝:运算符重载
string full_name; cout <<"请输入你的全名: ";// 参数:输入流,存储变量getline(cin, full_name); cout <<"你好, "<< full_name << endl;
string str1 ="apple"; string str2 ="banana";if(str1 < str2){ cout <<"apple 在字典中排在 banana 前面"<< endl;}if(str1 =="apple"){ cout <<"字符串相等"<< endl;}

🚩6)vs和g++下string结构的说明

注意: 下述结构是在32位平台下进行验证,32位平台下指针占4个字节

VS 下 string 的结构

string 总共占 28 个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义 string 中字符串的存储空间:

  • 当字符串长度小于 16 时,使用内部固定的字符数组来存放
  • 当字符串长度大于等于 16 时,从堆上开辟空间

这种设计也是有一定道理的,大多数情况下字符串的长度都小于 16,那 string 对象创建好之后,内部已经有了 16 个字符数组的固定空间,不需要通过堆创建,效率高

其次:还有一个 size_t 字段保存字符串长度,一个 size_t 字段保存从堆上开辟空间总的容量

最后:还有一个指针做一些其他事情

故总共占 16 + 4 + 4 + 4 = 28 个字节

G++ 下 string 的结构

G++ 下,string 是通过写时拷贝实现的,string 对象总共占 4 个字节,内部只包含了一个指针,该指针将来指向一块堆空间,内部包含了如下字段:

  • 空间总大小
  • 字符串有效长度
  • 引用计数
  • 指向堆空间的指针,用来存储字符串

💪牛刀小试

🔗Lucy的空间骇客裂缝:string算法题

2.3)string类的模拟实现

上面已经对 string 类进行了简单的介绍,大家只要能够正常使用即可。在面试中,面试官总喜欢让学生自己来模拟实现 string 类,最主要是实现 string 类的构造、拷贝构造、赋值运算符重载以及析构函数

  • 大家看下以下 string 类的实现是否有问题?

为了和标准库区分,此处使用 String

classString{public:String(constchar* str =""){if(nullptr== str){assert(false);return;} _str =newchar[strlen(str)+1];strcpy(_str, str);}~String(){if(_str){delete[] _str; _str =nullptr;}}private:char* _str;};// 测试voidTestString(){ String s1("hello bit!!!"); String s2(s1);}

这段代码展示了一个初学者在模拟实现 C++String 类时经常会遇到的一个经典问题:浅拷贝导致的崩溃

虽然你定义了构造函数和析构函数,但你漏掉了一个非常关键的角色:拷贝构造函数

String s1("hello bit!!!"); String s2(s1);// 这里触发了隐式的拷贝构造

█▓▒░█▓▒░█▓▒░█▓▒░ CODE CORE 🔥

  1. s1 的创建:调用构造函数,在堆上申请了一块内存存储 "hello bit!!!"_str 指向这块地址(假设是 0x123
  2. s2 的创建:因为你没有自己写拷贝构造函数,编译器会为你生成一个默认的。默认拷贝构造函数执行的是浅拷贝****,它只是简单地把 s1._str 的值(地址 0x123)赋值给了 s2._str
  3. 析构时的灾难
    • TestString 结束时,s2 先析构,调用 delete[] _str,释放了 0x123
    • 随后 s1 析构,它也尝试调用 delete[] _str 去释放 0x123
    • 结果:同一块内存被释放了两次,程序直接崩溃

在这里插入图片描述

解决方案
你需要手动实现拷贝构造函数,为新对象申请独立的内存空间

String(const String& s){ _str =newchar[strlen(s._str)+1];strcpy(_str, s._str);}

🚩1)经典的string类问题

1)构造函数的缺省值设置非常关键
String(constchar* str ="\0")//为什么错?
  • 逻辑冗余: 在 C/C++ 中,双引号括起来的字符串字面量(如 “”)结尾自带一个隐式的 \0。如果你写 “\0”,实际上这个字符串在内存中包含了两个 \0(一个是显式写的,一个是编译器自动补的)
String(constchar* str =nullptr) 为什么错? 
  • 解引用空指针
    string 类的构造函数内部通常会调用 strlen(str) 来计算传入字符串的长度。
  • 底层崩溃
    strlen 函数的原理是持续向后读取内存直到遇到 \0。如果 strnullptr(空指针),
    strlen 试图访问地址为 0 的内存,这会导致非法访问(Segmentation Fault)。

🚩2)浅拷贝 VS 深拷贝

🔗Lucy的空间骇客裂缝:深拷贝与浅拷贝

传统 vs 现代 的string类
1)传统
classString{public:String(constchar* str =""){if(nullptr== str){assert(false);return;} _str =newchar[strlen(str)+1];//分配空间,+1用于存放'\0'strcpy(_str, str);}String(const String& s):_str(newchar[strlen(s._str)+1]){strcpy(_str, s._str);}//赋值运算符重载/*避免s1 = s2出现浅拷贝问题*/ String&operator=(const String& s){if(this!=&s){char* Pstr =newchar[strlen(s._str)+1];strcpy(Pstr, s._str);delete[] _str; _str = Pstr;}return*this;// 支持链式赋值}~String(){if(_str){delete[] _str; _str =nullptr;}}
2)现代
classString{public:/*构造函数*/String(constchar* str =""){if(str !=nullptr){ _str =newchar[strlen(str)+1];strcpy(_str, str);}else{assert(false);return;}}/*拷贝构造函数*/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;// s 是局部副本,函数结束时自动释放旧资源}/*另一种赋值实现 (传引用方式)*///String& operator=(const String& s) {// if(this != &s) { // 必须做自赋值检测// String strTmp(s); // 拷贝构造出临时对象// std::swap(_str, strTmp._str);// }// return *this;//}/*析构函数*/~String(){if(_str){delete[] _str; _str =nullptr;}}private:char* _str;};

╔═█▓▒░ CODE CORE 🔥

┌─────────────┐
│ 代码关键点 │String strTmp(s._str);
└─────────────┘

╔═█▓▒░ CODE CORE 🔥

┌─────────────┐
│ 代码关键点 │swap(_str, strTmp._str);
└─────────────┘


调用了 String 类的另一个构造函数(通常是 String(const char* str)
strTmp 对象在被创建时,已经完美地做好了内存申请和数据拷贝工作(意味着你向编译器保证不会修改 s

交换指针swap(_str, strTmp._str);


🚩3)写时拷贝

一、 什么是写时拷贝(COW)?

在了解 COW 之前,我们先回顾一下传统的两种数据拷贝方式:

  1. 浅拷贝(Shallow Copy): 多个指针指向同一块内存。优点是速度快、省内存;缺点是一旦某个指针修改了数据,其他指针看到的数据也会跟着变,不安全。
  2. 深拷贝(Deep Copy): 重新分配一块同样大小的内存,把数据原封不动地搬过去。优点是绝对安全,互不影响;缺点是极其消耗时间和内存资源。

写时拷贝(Copy-on-Write)其实就是一种结合了浅拷贝和深拷贝优点的“懒汉(Lazy)”策略:

  • 读操作时: 大家共享同一块内存(类似浅拷贝),不产生任何额外的开销。
  • 写操作时: 当有人试图修改这块共享内存的数据时,系统才会真正分配一块新内存,把原来的数据复制过来,然后再进行修改(类似深拷贝)。

核心思想只有四个字:延迟执行。 只要你没去改它,大家就一直共享;只有在迫不得已(要修改)的时候,才去真正做拷贝的动作。


二、 写时拷贝的工作机制

我们以内存页(Page)为例,看看 COW 在操作系统中是如何运转的:

  1. 初始状态: 进程 A 拥有一块内存页 Page 1。
  2. 发生拷贝(如 fork 进程): 此时需要拷贝出一个进程 B。系统不会真的去复制 Page 1,而是让进程 A 和 B 的页表都指向 Page 1,并将 Page 1 的权限设置为 只读(Read-Only)
  3. 安全读取: 只要 A 和 B 只是读取数据,系统相安无事。
  4. 触发写入: 假设进程 B 要修改 Page 1 的数据。此时 CPU 发现 Page 1 是只读的,就会触发一个 缺页异常(Page Fault)
  5. 内核介入(写时拷贝): 操作系统内核捕获到异常,发现这其实是一个 COW 页面。于是内核会默默分配一块新的物理内存(Page 2),把 Page 1 的数据复制到 Page 2 中,将进程 B 的页表映射到 Page 2,并将两者的权限都恢复为 可读写(Read-Write)
  6. 继续执行: 进程 B 的写入操作在 Page 2 上顺利完成,进程 A 的数据依然在 Page 1 上保持原样。

三、 写时拷贝的经典应用场景

COW 思想的应用极其广泛,以下是几个面试常考、工作中也常遇到的经典场景:

1. Linux 中的 fork() 系统调用

在早期的 Unix 系统中,fork() 创建子进程时会把父进程的所有内存空间全部深拷贝一份。但大多数情况下,子进程马上就会调用 exec() 去执行全新的程序,之前费时费力拷贝过来的内存完全浪费了。
引入 COW 后,fork() 瞬间变得轻量级:父子进程初始共享全部物理内存。只有当某一方试图修改数据(如修改变量)时,才会针对那一小块特定的内存页进行拷贝。这极大地提升了进程创建的速度,并节省了内存。

2. Redis 的 bgsave 持久化

Redis 是单线程的,如果在主线程里把庞大的内存数据写入磁盘(RDB 镜像),会严重阻塞正常的客户端请求。
Redis 的做法是:调用 fork() 创建一个子进程去专门负责写磁盘。得益于操作系统的 COW 机制,子进程在创建瞬间就拥有了和父进程一模一样的内存视图,而且速度极快。父进程依然可以毫无阻塞地处理客户端的新请求,只有被修改的数据页才会发生实际的内存拷贝。

3. Java 中的 CopyOnWriteArrayList

这是 Java 并发包(java.util.concurrent)中提供的一个线程安全的 List。
它的内部实现原理是:任何读操作都不加锁,直接读取底层数组;而一旦有写操作(如 addsetremove),就会先将原数组复制一份出一个新数组,在新数组上进行修改,修改完成后,再把原数组的引用指向新数组。

  • 适用场景: 读多写少的并发场景(如黑白名单、系统配置缓存等)。
4. 容器技术与存储(Docker / ZFS)

Docker 镜像的分层存储(如 OverlayFS、AUFS)也深度依赖了 COW。当我们启动一个容器时,底层镜像层是只读的。只有当我们在容器内修改或新建文件时,系统才会把该文件从底层镜像层复制到最上面的可写层(Container Layer)进行修改。这使得成百上千个容器可以共享同一份底层镜像,极大地节省了磁盘空间。


四、 写时拷贝的优缺点总结
🌟 优点:
  1. 极大地节省内存/磁盘资源: 只有在发生修改时才去分配资源,未修改的部分永远共享。
  2. 显著提升性能: 避免了不必要的、耗时的初始化全量拷贝操作。
  3. 并发读安全: 在某些语言级别实现中(如 Java),由于原始数据不可变,天然支持无锁的并发读取。
⚠️ 缺点:
  1. 不可预知的延迟: 拷贝动作被延迟到了写操作触发的瞬间。如果数据量大,突然触发的大量写操作会导致系统出现短暂的卡顿(比如 Redis 在写密集的场景下进行 bgsave,容易发生频繁的 Page Fault 导致性能抖动)。
  2. 写放大开销: 在 Java 的 CopyOnWriteArrayList 中,哪怕只修改数组中的一个元素,也要把整个数组复制一遍,写操作的开销极大。

2.4)面试中string的一种正确写法

C++ 的一个常见面试题是让你实现一个String类,限于时间,不可能要求具备std::string的功能,但至少要求能正确管理资源。具体来说:

1. 能像 int 类型那样定义变量,并且支持赋值、复制

2. 能用作函数的参数类型及返回类型

3. 能用作标准库容器的元素类型,即 vector/list/dequevalue_type

(用作 std::mapkey_type 是更进一步的要求,本文从略)

🔗Lucy的空间骇客裂缝:面试中C++的写法

2.5)STL 中的 string 类怎么了?

🔗Lucy的空间骇客裂缝

💻结尾— 核心连接协议

警告:🌠🌠正在接入底层技术矩阵。如果你已成功破解学习中的逻辑断层,请执行以下指令序列以同步数据:🌠🌠


【📡】 建立深度链接:关注本终端。在赛博丛林中深耕底层架构,从原始代码到进阶协议,同步见证每一次系统升级。

【⚡】 能量过载分发:执行点赞操作。通过高带宽分发,让优质模组在信息流中高亮显示,赋予知识跨维度的传播力。

【💾】 离线缓存核心:将本页加入收藏。把这些高频实战逻辑存入你的离线存储器,在遭遇系统崩溃或需要离线检索时,实现瞬时读取。

【💬】 协议加密解密:评论区留下你的散列码。分享你曾遭遇的代码冲突或系统漏洞(那些年踩过的坑),通过交互式编译共同绕过技术陷阱。

【🛰️】 信号频率投票:通过投票发射你的选择。你的每一次点击都在重新定义矩阵的进化方向,决定下一个被全量拆解的技术节点。


在这里插入图片描述

Read more

【MySQL筑基篇】新手必看:聚簇索引、非聚簇索引与回表,一篇扫清盲区

【MySQL筑基篇】新手必看:聚簇索引、非聚簇索引与回表,一篇扫清盲区

🍃 予枫:个人主页 📚 个人专栏: 《Java 从入门到起飞》《读研码农的干货日常》 💻 Debug 这个世界,Return 更好的自己! 引言 做后端开发的同学,大概率都听过“索引优化”,也用过主键索引提升查询速度。但你真的懂索引吗?日常开发中,不少同学遇到查询卡顿就盲目加索引,结果反而导致数据增删改效率下降;还有人疑惑,为什么同样是索引,主键查询秒出结果,普通索引查询却要慢半拍?除了主键,还有哪些核心索引类型?为什么有的查询不用“绕路”,有的却要额外“回表”?今天咱们从数据库物理存储的底层逻辑出发,逐字拆解聚簇索引与非聚簇索引的核心概念,帮你夯实索引入门基础,为后续吃透B+树结构、搞定索引优化铺路~ 建议点赞收藏,避免后续需要时找不到! 文章目录 * 引言 * 一、索引基础:不止是“快速查找” * 1.1 为什么需要关注物理存储? * 二、聚簇索引:数据与索引“合二为一”

By Ne0inhk
Spring Boot AOP(三) 通知执行链源码解析

Spring Boot AOP(三) 通知执行链源码解析

博主社群介绍: ① 群内初中生、高中生、本科生、研究生、博士生遍布,可互相学习,交流困惑。 ② 热榜top10的常客也在群里,也有数不清的万粉大佬,可以交流写作技巧,上榜经验,涨粉秘籍。 ③ 群内也有职场精英,大厂大佬,跨国企业主管,可交流技术、面试、找工作的经验。 进群免费赠送写作秘籍一份,助你由写作小白晋升为创作大佬,进群赠送ZEEKLOG评论防封脚本,送真活跃粉丝,助你提升文章热度。 群公告里还有全网大赛约稿汇总/博客提效工具集/ZEEKLOG自动化运营脚本 有兴趣的加文末联系方式,备注自己的ZEEKLOG昵称,拉你进群,互相学习共同进步。 文章目录 * Spring Boot AOP(三) 通知执行链源码解析 * 1. 执行链概述 * 核心概念 * 2. Advisor 链与通知统一处理 * Mermaid 流程图:Advisor 链构建 * 3. MethodInterceptor 执行流程

By Ne0inhk
OpenClaw多设备协同:手机+电脑分布式节点,跨端任务自动化

OpenClaw多设备协同:手机+电脑分布式节点,跨端任务自动化

文章目录 * 当"用手机修电脑"不再是段子 * 架构揭秘:Gateway是大脑,Nodes是手脚 * 动手实战:把你的手机变成AI的外挂设备 * 第一步:确认Gateway处于"远程模式" * 第二步:手机端配对流程 * 第三步:验证节点能力 * 场景实战:那些只有多设备协同才能干成的活儿 * 场景一:移动端触发,PC端执行(Mobile-to-Desktop) * 场景二:PC端决策,移动端采集(Desktop-to-Mobile) * 场景三:多节点并行任务(Swarm模式) * 技术原理:MCP协议让万物互联成为可能 * 避坑指南:别让你的分布式系统变成"分布死"系统 * 网络连通性是第一要义 * 权限管理要精细 * 电池与性能考虑 * 未来展望:从"多设备"到&

By Ne0inhk
Flutter for OpenHarmony:result_dart 告别 try-catch,让错误处理像 Rust 一样优雅(Result 类型模式) 深度解析与鸿蒙适配指南

Flutter for OpenHarmony:result_dart 告别 try-catch,让错误处理像 Rust 一样优雅(Result 类型模式) 深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 在 Dart 中,异常(Exception)是隐式的,你看不到函数签名上有 throws 标记。这不仅导致调用者经常忘记捕获异常,还使代码充斥着难以追踪的 try...catch 块。 受 Rust 语言的启发,result_dart 引入了 Result<Success, Failure> 类型。它将成功值和错误值都包装在同一个对象中,强制你在编译期就处理可能发生的错误,从而写出极度健壮的代码。 在传统的面向对象编程(OOP)中,异常(Exception) 是处理错误的默认机制。我们习惯了: try{final user =awaitgetUser();// ...}catch(e){// 处理错误} 但这带来了一个巨大的问题:

By Ne0inhk