**OK,今天又是被难到的一天,开啃!我就不信拿不下你,大家一起上!!!------**上代码!
cpp
//柔性数组
class Mystring {
public:
struct StrNode {
int ref;//对象个数=>代表有多少个对象持有
int slen;//字符串长度,不算'\0'
int capa;//申请的字符串空间
char data[];
};
private:
StrNode* pstr;
static const size_t kInitSize = 128;//静态常量初始化数组长度
static const size_t kHeadSize = sizeof(StrNode);//结构体开头大小
static StrNode* getNode(size_t len)
{
len = (len < kInitSize) ? kInitSize : len;
size_t total = kHeadSize + len;
StrNode* newstr = (StrNode*)calloc(total, sizeof(char));
if (nullptr == newstr) {
exit(EXIT_FAILURE);
}
newstr->capa = len - 1;
return newstr;
}
static void freeNode(StrNode* p) {
free(p);
}
static StrNode* writeCopy(StrNode* pstr, size_t newcap) {
newcap = pstr->capa > newcap ? (pstr->capa+1) : newcap;
StrNode* newNode = getNode(newcap);
newNode->ref = 1;
newNode->slen = pstr->slen;
strcpy(newNode->data, pstr->data);
pstr->ref -= 1;
return newNode;
}
static StrNode* expansionNode(StrNode* pstr, size_t newcap) {
StrNode* newNode = getNode(newcap);
newNode->ref = 1;
newNode->slen = pstr->slen;
strcpy(newNode->data, pstr->data);
freeNode(pstr);
return newNode;
}
public:
Mystring(const char* sp = nullptr) :pstr(nullptr) {
if (sp != nullptr && *sp != '\0') {
int len = strlen(sp);
pstr = getNode(strlen(sp) + 1);
pstr->ref = 1;
pstr->slen = len;
strcpy(pstr->data, sp);
}
}
~Mystring() {
if (pstr != nullptr && --pstr->ref == 0) {
freeNode(pstr);
}
pstr = nullptr;
}
Mystring(const Mystring& other) :pstr(other.pstr) {//拷贝
if (this->pstr != nullptr) {
this->pstr->ref += 1;
}
}
Mystring(Mystring&& other) :pstr(other.pstr) {//移动构造
other.pstr = nullptr;
}
void reserve(size_t newcap) {
newcap += 1;//多的一位存'\0'
if (nullptr == pstr) {
pstr = getNode(newcap);
pstr->ref = 1;
pstr->slen = 0;
}
else if (pstr->ref > 1) {
pstr = writeCopy(pstr, newcap);
}
else if (pstr->capa + 1 < newcap) {
pstr = expansionNode(pstr, newcap*1.6);//运行速度和这个系数有什么关系?
}
}
StrNode* swap(Mystring& other) {
std::swap(this->pstr, other.pstr);
return this->pstr;
}
Mystring& operator=(const Mystring& other) {
if (this != &other) {
Mystring(other).swap(*this);
}
return *this;
}
Mystring& operator=(Mystring&& other) {
if (this != &other) {
Mystring(std::move(other)).swap(*this);
}
return *this;
}
void Print() {
if (pstr != nullptr) {
cout << "ref:" << pstr->ref << " ";
cout << "slen:" << pstr->slen << " ";
cout << "capa:" << pstr->capa << " ";
cout << "data:" << pstr->data << " ";
}
cout << endl;
}
};
int main() {
Mystring s1("Hello world!");
s1.Print();
s1.reserve(127);
s1.Print();
s1.reserve(128);
s1.Print();
s1.reserve(200);
s1.Print();
Mystring s2;
s2.reserve(100);
s2 = s1;
s2.Print();
Mystring s3("How are you!");
Mystring s4(s3);
s4 = "I am fine!";
s3.reserve(100);
s4.Print();
return 0;
}
我今天学这个的时候,稍微有些懵,同时有很多疑问,现在来捋一下:
Question 1:为什么要把一个简单的字符串,写得这么绕、这么复杂?
首先------如果让你写一个最简单的字符串类,你会怎么写?
我大概率会这样写:
cpp
class MyString {
private:
char* data; // 直接存字符串
int len;
};
拷贝的时候:
cpp
MyString(const MyString& other) {
data = new char[len];
memcpy(data, other.data, len); // 直接把字符串复制一份
}
这就是最朴素的字符串。但它有一个巨大的缺点:拷贝 = 复制整个字符串 = 慢!字符一长,又慢又浪费内存
进而 ------就有了大佬们想出的神方案:不拷贝,共享同一份内存
多个字符串共用同一个内存
s1 → 内存A
s2 → 内存A
s3 → 内存A
这样拷贝起来非常快,只需要指针赋值,速度是 O(1),瞬间完成!
指针赋值 ,简单说就是:把一个 内存地址 存到指针变量里,让指针指向这个地址对应的变量 / 数据。 你可以把指针理解成一张写着地址的纸条,指针赋值就是在纸条上写下地址。
然后------ 那问题来了:大家共用一块内存,有人改了怎么办?
s1 = "hello";
s2 = s1;
s1 = "world"; // s1变了,s2不能跟着变!
所以必须加一个规则: 谁要修改,谁就自己复制一份离开群体! 这就叫: 写时拷贝 Copy On Write(COW)
这个代码,不是在存字符串,而是在管理 "共享内存"!
普通字符串:我自己独占一份
这个字符串:大家共用一份,修改才复制
Question2:整体结构和思路是什么?





最后没有人用了才释放:
s2 析构 → ref=1
s3 析构 → ref=0 → 旧内存真正 free
整个类的结构:
cpp
class Mystring {
struct StrNode; // 真正存字符串的内存块
StrNode* pstr; // 句柄:只指向一块共享内存
// 工具:申请/释放/写时复制/扩容
static getNode();
static freeNode();
static writeCopy();
static expansionNode();
// 构造/析构/拷贝/移动
Mystring();
~Mystring();
Mystring(const Mystring&);
Mystring(Mystring&&);
// 赋值运算符
operator=();
// 扩容
reserve();
// 交换
swap();
// 打印
Print();
};
Question3:结构体 StrNode里面的成员,柔性数组详解?
(1)ref:0 = 没人用,可以释放;1 = 独占;>1 = 共享
(2)slen:strlen 的结果,不包含结尾 \0
(3)capa:容量 = 最多能存多少字符,结构体的开头大小+柔性数组的大小,一定 ≥ slen
(4)data [] 柔性数组:不占结构体大小,和结构体连续内存,高效!
柔性数组 char data[ ];关键特点:
- 不占用结构体内存sizeof(StrNode) 只计算 ref + slen + capa 的大小,data[] 不占空间。
- 和结构体是连续内存结构体 + 字符串数据在同一块连续内存里,一次 malloc/calloc 就能全部申请。
- 效率极高 少一次内存分配 / 释放 内存连续,CPU 缓存命中率更高 没有额外指针开销(不用 char* data)
Question4:静态工具函数1------申请空间函数?
cpp
static StrNode* getNode(size_t len)//申请空间
{
len = (len < kInitSize) ? kInitSize : len;//判断柔性数组的大小
size_t total = kHeadSize + len;//得到总空间的大小
StrNode* newstr = (StrNode*)calloc(total, sizeof(char));//向堆空间申请需要的大小
if (nullptr == newstr) {//判空
exit(EXIT_FAILURE);//为空直接退出
}
newstr->capa = len - 1;//不包括'\0'
return newstr;//返回申请的堆空间的地址
}
- newstr是一个局部变量,为什么可以这样newstr->capa = len - 1?
newstr 确实是局部指针变量存在栈上,但它指向的内存是全局的(堆), 这块内存是 calloc 申请的,不会随函数结束消失,函数最后把指向堆内存的地址返回了,虽然 newstr 这个局部指针死了,但地址被传出去了,外面还能继续用------所以可以放心修改、放心返回使用。
Question5:静态工具函数2------writeCopy 写时复制函数?
cpp
static StrNode* writeCopy(StrNode* pstr, size_t newcap) {//写时复制
newcap = pstr->capa > newcap ? (pstr->capa) : newcap;//确定新空间的大小
StrNode* newNode = getNode(newcap);//给新空间申请新的内存
newNode->ref = 1;//新空间只有一个成员使用
newNode->slen = pstr->slen;//字符串长度和原来相同
strcpy(newNode->data, pstr->data);//字符串内容和原来相同
pstr->ref -= 1;//原来的使用旧空间的人数减1
return newNode;//返回新空间的地址,让人修改访问
}
作用 :当多个字符串共用同一块内存 时(引用计数
ref > 1),一旦要修改内容,必须复制一份新的 ,不能改原来的。这就是写时复制。
Question6:静态工具函数3------扩容函数?
cpp
static StrNode* expansionNode(StrNode* pstr, size_t newcap) {//扩容
StrNode* newNode = getNode(newcap);//定义一个新局部指针,指向一个更大的空间
newNode->ref = 1;//给一个成员用的
newNode->slen = pstr->slen;//将字符串长度复制过去
strcpy(newNode->data, pstr->data);//将字符串内容拷贝到新的里面
freeNode(pstr);//释放旧的空间
return newNode;//返回新空的的地址
}
作用:给只有一个对象在用的字符串,申请更大的空间,把数据挪过去。
Question7:为什么要把这几个静态函数封装起来,为什么在Public里面可以调用封装起来的函数?
(1)因为这些函数是内部工具人 ,只给 Mystring 类自己用,绝对不允许外界随便调用,所以要封装起来,静态函数不依赖对象,这 4 个函数不需要操作对象的成员变量,只做一件事:传入指针 → 处理 → 返回指针
(2)属于 Mystring 类本身,而这些静态工具函数也是类的私有成员。同类成员之间,天然可以互相访问,不需要额外权限。这就是 C++ 类封装的基本规则。出来类就不能用了,没有访问权限
Question8:pstr不是指针吗,为什么可以存大小?
cpp
Mystring(const char* sp = nullptr) :pstr(nullptr) {//构造函数
if (sp != nullptr && *sp != '\0') {//非空指针和指针指向的地址空间里不是空串
int len = strlen(sp);//计算字符串长度
pstr = getNode(strlen(sp) + 1);//将新空间的地址给指针pstr
//初始化
pstr->ref = 1;//使用个数
pstr->slen = len;//长度
strcpy(pstr->data, sp);//字符串内容
}
}
哎呀,宝子,你看看清楚噻!getNode()这个函数的返回值是空间地址,所以pstr存的是新申请空间的地址!
Question9:other.pstr是什么意思?
cpp
Mystring(Mystring&& other) : pstr(other.pstr) {
other.pstr = nullptr;
}
pstr(other.pstr)
左边是新对象的指针,other.pstr的pstr是别人的指针,意思就是把别人的地址给我
other.pstr=nullptr;
再把别人的指针置空
Question10:运行速度和这个系数有什么关系?
cpp
void reserve(size_t newcap) {
newcap += 1;//多的一位存'\0'
if (nullptr == pstr) {
pstr = getNode(newcap);
pstr->ref = 1;
pstr->slen = 0;
}
else if (pstr->ref > 1) {//使用内存的成员人数>2
pstr = writeCopy(pstr, newcap);
}
else if (pstr->capa + 1 < newcap) {//如果本来大小小于扩容大小,就扩容
pstr = expansionNode(pstr, newcap*1.6);//运行速度和这个系数有什么关系?
}
}
- 空对象没有空间为什么还要这句话pstr = getNode(newcap);?
pstr 空对象没有空间 所以必须用 getNode 申请空间 这是空对象第一次拥有内存
你后面想操作字符串:
pstr->ref = 1;
直接崩溃!因为 pstr 是空指针,不能访问 -> 成员。
- 运行速度和这个系数有什么关系?
系数越大 → 扩容次数越少 → 速度越快
系数越小 → 扩容次数越多 → 速度越慢
但系数不能无限大,太大会浪费内存。
假设你要存 1000 个字符
情况 1:系数 = 1.1 倍(很小)
每次只多扩容 10% 100 → 110 → 121 → 133 → ... → 1000
要扩容几十次 每次扩容都要 拷贝整个字符串 巨慢!巨耗时间!
情况 2:系数 = 2 倍(大) 100 → 200 → 400 → 800 → 1600
只扩容 4 次 拷贝次数少 → 速度飞快!
情况 3:系数 = 1.618 倍(黄金比例,STL string 标准)
扩容次数少 内存浪费少 速度 + 空间 平衡最好
核心原理: 扩容 = 内存拷贝 扩容一次 = 把整个字符串复制一遍
系数小扩容频繁 → 大量拷贝 → 速度慢
系数大扩容稀少 → 拷贝很少 → 速度快
为什么 STL 标准 用 1.6 或 1.5,不用 2?
2 倍 速度最快,但浪费内存
1.6 倍 速度几乎一样,但内存浪费少 所以 1.6 是速度与空间的黄金平衡点。
Question11:赋值重载函数中的拷贝交换函数的内核?
cpp
Mystring& operator=(const Mystring& other) {
if (this != &other) {
Mystring(other).swap(*this);
}
return *this;
}
作用:把 other 字符串赋值给当前对象(this)
cpp
Mystring(other).swap(*this);//拷贝交换函数
先拷贝一份 → 交换指针 → 自动释放旧内存
- 第一步:Mystring temp(other);
Mystring(other) 这行创建了一个临时对象 temp
它调用拷贝构造函数,把 other 的内容复制一份。 temp 是新的、独立的对象,它有自己的指针,它和 other 共享内存(ref++)
- 第二步:temp.swap(*this);
swap(*this); 调用 swap 函数,把: 临时对象 temp 的指针,当前对象 this 的指针,两个指针互换!
- 第三步:语句结束,temp 销毁
这行代码执行完后,临时对象 temp 生命周期结束,自动调用析构函数。它带走了 this 原来的旧指针,并把它释放了。
测试结果:

❀❀❀❀❀❀❀❀❀❀❀❀❀You are so good!❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀