自学内容网 自学内容网

C++(手写Mystring|柔性数组、引用计数与写时拷贝的核心用法)

OK,今天又是被难到的一天,开啃!我就不信拿不下你,大家一起上!!!——上代码!

//柔性数组
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:为什么要把一个简单的字符串,写得这么绕、这么复杂?

首先——如果让你写一个最简单的字符串类,你会怎么写?
我大概率会这样写:

class MyString {
private:
    char* data;  // 直接存字符串
    int len;
};

拷贝的时候:

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

整个类的结构:

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——申请空间函数?

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 写时复制函数?

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——扩容函数?

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不是指针吗,为什么可以存大小?

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是什么意思?

Mystring(Mystring&& other) : pstr(other.pstr) {
    other.pstr = nullptr;
}

pstr(other.pstr)

左边是新对象的指针,other.pstr的pstr是别人的指针,意思就是把别人的地址给我

other.pstr=nullptr;

再把别人的指针置空

Question10:运行速度和这个系数有什么关系?

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:赋值重载函数中的拷贝交换函数的内核?

Mystring& operator=(const Mystring& other) {
if (this != &other) {
Mystring(other).swap(*this);
}
return *this;
}

作用:把 other 字符串赋值给当前对象(this)

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!❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀


原文地址:https://blog.csdn.net/2301_79997776/article/details/159613906

免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!