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

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

相关推荐
星轨初途5 天前
【C/C++底层修炼】拆解动态内存管理:四大动态内存函数、六大错误与柔性数组
c语言·开发语言·c++·经验分享·笔记·柔性数组
01二进制代码漫游日记19 天前
C语言:柔性数组
柔性数组
我能坚持多久2 个月前
D19—C语言动态内存管理全解:从malloc到柔性数组
c语言·开发语言·柔性数组
我是大咖2 个月前
关于柔性数组的理解
数据结构·算法·柔性数组
Allen_LVyingbo3 个月前
面向“病历生成 + CDI/ICD”多智能体系统的选型策略与落地实践(三)
算法·自然语言处理·性能优化·知识图谱·健康医疗·柔性数组
栈与堆3 个月前
数据结构篇(1) - 5000字细嗦什么是数组!!!
java·开发语言·数据结构·python·算法·leetcode·柔性数组
yuanmenghao3 个月前
自动驾驶中间件iceoryx - 内存与 Chunk 管理(一)
c++·vscode·算法·链表·中间件·自动驾驶·柔性数组
山上三树3 个月前
柔性数组(C语言)
c语言·开发语言·柔性数组
黎雁·泠崖3 个月前
C 语言动态内存管理高阶:柔性数组特性 + 程序内存区域划分全解
c语言·开发语言·柔性数组