【C++】深度解析:用 C++ 模拟实现 String 类,探索其底层实现细节

目录

了解string类

string的内存管理

VS下string的结构

​g++下string的结构

 string的模拟实现

string的构造函数

 浅拷贝

深拷贝

 string的遍历

重载 [] 下标访问

迭代器访问

reserve

resize

 增删查改

push_back()

append和+=

 insert和erase

find

substr

swap 

 流插入和流提取

getline

string其他基本功能


⭐了解string类

1. 字符串是表示字符序列的类
2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
3. string 类是使用 char( 即作为它的字符类型,使用它的默认 char_traits 和分配器类型 ( 关于模板的更多信息,请参阅basic_string)
4. string 类是 basic_string 模板类的一个实例,它使用 char 来实例化 basic_string 模板类,并用 char_traits和allocator 作为 basic_string 的默认参数 ( 根于更多的模板信息请参考 basic_string)
5. 注意,这个类独立于所使用的编码来处理字节 : 如果用来处理多字节或变长字符 ( UTF-8) 的序列,这个类的所有成员( 如长度或大小 ) 以及它的迭代器,将仍然按照字节 ( 而不是实际编码的字符 ) 来操作。
总结:
1. string 是表示字符串的字符串类
2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作 string 的常规操作。
3. string 在底层实际是: basic_string 模板类的别名, typedef basic_string<char, char_traits, allocator>
string;
4. 不能操作多字节或者变长字符的序列。
使用 string 类时,必须包含 #include 头文件以及 using namespace std ;

⭐string的内存管理

✨VS下string的结构

string总共占28个字节 ,内部结构稍微复杂一点,先是 有一个联合体,联合体用来定义 string 中字
符串的存储空间
  • 当字符串长度小于16时,使用内部固定的字符数组来存放
  • 当字符串长度大于等于16时,从堆上开辟空间
union _Bxty
{ // storage for small buffer or pointer to larger one
 value_type _Buf[_BUF_SIZE];
 pointer _Ptr;
 char _Alias[_BUF_SIZE]; // to permit aliasing
} _Bx;
  1. 大多数情况下字符串的长度都小于16,当string对象创建好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。
  2. 还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量
  3. 有一个指针做一些其他事情。
  4. 故总共占16+4+4+4=28个字节。

✨g++下string的结构

g++ 下, string 是通过写时拷贝实现的, string对象总共占4个字节 ,内部只包含了一个指针,该指
针将来指向一块堆空间,内部包含了如下字段:
  • 空间总大小
  • 字符串有效长度
  • 引用计数
  • 指向堆空间的指针,用来存储字符串。
struct _Rep_base
{
 size_type _M_length;
 size_type _M_capacity;
 _Atomic_word _M_refcount;
};

 ⭐string的模拟实现

private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;

✨string的构造函数

// 为了和标准库区分,此处使用String
class String
{
public:

 /*String()
 :_str(new char[1])
 {*_str = '\0';}
 */
 //String(const char* str = "\0") 错误示范
 //String(const char* str = nullptr) 错误示范
 String(const char* str = "")//默认包含 \0
 {
 // 构造String类对象时,如果传递nullptr指针,可以认为程序非法
 if (nullptr == str)
 {
 assert(false);
 return;
 }
 _str = new char[strlen(str) + 1];
 strcpy(_str, str);
 }
 ~String()
 {
 if (_str)
 {
 delete[] _str;
 _str = nullptr;
 }
 }
private:
 char* _str;
};
// 测试
void TestString()
{
 String s1("hello bit!!!");
 String s2(s1);
}

 

上述 String 类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用 s1 s2 时,编译器会调用默认的拷贝构造。最终导致的问题是, s1 s2 共用同一块内存空间,在释放时同一块 空间被释放多次而引起程序崩溃 ,这种拷贝方式,称为 浅拷贝

 📖浅拷贝

浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来 。如果 对象中管理资源 ,最后就会 导致多个对象共 享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为 还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
举个例子,如果一个家庭里面有两个孩子,但是父母只给他们买了一个玩具,如果两个孩子都愿意玩这一个玩具,那就相安无事,否则就会鸡飞狗跳。
要解决这个问题,就可以直接给他们一人买一个玩具,这样各自安逸。
所以,要想解决浅拷贝的问题,可以使用深拷贝的方法, 每个对象都有一份独立的资源,不要和其他对象共享

📖深拷贝

如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。

 ✨string的遍历

📖重载 [] 下标访问

char& operator[](size_t pos)//可读可写
{
	assert(pos < _size);
	return _str[pos];
}
//重载一个const
const char& operator[](size_t pos) const//只读
{
	assert(pos < _size);
	return _str[pos];
}

首先访问之前需要判断pos是否再合法访问之内,即小于等于size,然后直接返回字符串数组中对应的元素。由于存在const对象和非const对象,所以需要写两个重载版本。

📖迭代器访问

//迭代器
		typedef char* iterator;
		typedef const char* const_iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		const_iterator begin()const
		{
			return _str;
		}
		const_iterator end()const
		{
			return _str + _size;
		}

 其实底层就是指针,所以直接返回对应的地址就可以了。

✨reserve

void reserve(size_t n)
{
	if (n > _capacity)
	{
		char* tmp = new char[n+1];
		strcpy(tmp, _str);
		//释放旧空间,指向新空间
		delete[] _str;
		_str = tmp;
		//修改capacity,不用修改size
		_capacity = n;
	}
}

reserve是提前预留部分空间,它接收的空间大小不能比本来就有的容量小,如果n合法,则需要将原数组从旧空间移向一块更大的新空间,并释放掉旧空间。

✨resize

void resize(size_t n,char ch='\0')
{
	if (n <= _size)
	{
		_str[n] = '\0';
		_size = n;
	}
	else
	{
		reserve(n);
		for (int i = _size; i < n; i++)
		{
			_str[i] = ch;
		}
		_str[n] = '\0';
		_size = n;
	}
}

resize是设置字符串的大小,如果n比字符串原来的大小小,则会发生截断;如果比原来的大小大,则会reserve一块n大小的空间。

 ✨增删查改

📖push_back()

void push_back(char ch)
{
	//扩容2倍
	if (_size == _capacity)
	{
		reserve(_capacity==0 ? 4 : 2 * _capacity);
	}
	_str[_size] = ch;
	_size++;
	_str[_size] = '\0';
}

直接尾插就可以,需要先判断空间是否足够,最后更新size的大小。

📖append和+=

void append(const char* str)
{
	//扩容
	//根据追加的字符串的长度扩容
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}
	strcpy(_str + _size, str);
	_size += len;
}
//  +=  
string& operator+=(char ch)
{
	push_back(ch);
	return *this;
} 
string& operator+=(const char* str)
{
	append(str);
	return *this;
}
string& operator+=(const string s)
{
	append(s._str);
	return *this;
}

append和+=都是在字符串的尾部追加字符或者字符串,需要先判断容量是否足够,不够则需要扩容, 根据追加的字符串的长度扩容。

 📖nsert和erase

对于insert,0位置的插入可能产生问题,end是int类型,pos是size_t类型,end变成-1与pos比较时会发生整型提升,所以pos需要先进行强制类型转换;也可以使用另一种解决方法,将end的初始值赋值为size+1,每次使用这种方法后移字符串 _str[end] = _str[end - 1];,则end最后不会变成-1。

//在pos之前插入
//插入字符
void insert(size_t pos,char ch)
{
	assert(pos <= _size);
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	}
	//int end = _size;
	0位置的插入可能产生问题,end变成-1与pos比较时会发生整型提升,所以pos需要先进行强制类型转换
	//while (end >= (int)pos)
	//{
	//	_str[end+1] = _str[end];
	//	end--;
	//}
	//第二种解决方法
	int end = _size + 1;
	while (end > pos)
	{
		_str[end] = _str[end - 1];
		end--;
	}
	_str[pos] = ch;
	_size++;
}
//在pos之前插入
//插入字符串
void insert(size_t pos,const char* str)
{
	assert(pos <= _size);
	int len = strlen(str);
	if (_size +len > _capacity)
	{
		reserve(_size + len+1);
	}
	//int end = _size;
	0位置的插入可能产生问题,end变成-1与pos比较时会发生整型提升,所以pos需要先进行强制类型转换
	//while (end >= (int)pos)
	//{
	//	_str[end+1] = _str[end];
	//	end--;
	//}
	//第二种解决方法
	//在pos之前插入
	int end = _size + len;
	//pos 1 2 end
	while (end > pos+len-1)
	{
		_str[end] = _str[end - len];
		end--;
	}
	strncpy(_str + pos,str,len);
	_size+=len;
}
//释放删除
void erase(size_t pos, size_t len = npos)
{
	assert(pos < _size);
	// pos+len 存在溢出风险
	//if (len == npos || pos + len >= _size)
	if (len == npos ||len >= _size-pos)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		strcpy(_str + pos, _str + pos + len);
		_size -= len;
	}
}

对于erase,需要根据传递的参数的大小来判断需要删除多少个字符。

📖find

//寻找匹配
size_t find(char ch,size_t pos = 0) const
{
	for (size_t i = pos; i < _size; i++)
	{
		if (_str[i] == ch)
			return i;
	}
	return npos;
}
size_t find(const char* sub, size_t pos = 0) const
{
	assert(pos <= _size);
	const char* p=strstr(_str+pos, sub);
	if (p)
	{
		return p - _str;
	}
	else
		return npos;
}

实现方法比较简单,就是普通的暴力查找。 

📖substr

截取子串,需要注意len的大小。

        string substr(size_t pos = 0, size_t len = npos)
		{
			string sub;
			if (len == npos|| len >= _size - pos)
			{
				for (size_t i = pos; i < _size; i++)
				{
					sub += _str[i];
				}
			}
			else
			{
				for (size_t i = pos; i < pos + len; i++)
				{
					sub += _str[i];
				}
			}
			return sub;
		}

✨swap 

众所周知,C++算法库里面存在swap这个函数模板,但是为什么string内部自己也有一个swap呢?

如果用std::swap交换两个string对象,将会发生1次构造和2次赋值,也就是三次深拷贝;

而string内部的swap仅仅只交换成员,代价较小。

//交换
void swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}

 为了符合算法库里面swap的用法,可以再将swap重载成全局函数。

	void swap(string& x, string& y)
	{
		x.swap(y);
	}

✨ 流插入和流提取

//重载成全局是为了调整顺序
	//流插入
	ostream& operator<<(ostream& out, const string& s)
	{
		//这里不需要写成友元函数,因为不需要直接访问私有成员
		for (auto ch:s)
		{
			cout << ch;
		}
		return out;
	}
	//流提取
	//C++ 流插入,流提取可以支持自定义类型使用
	istream& operator>>(istream& in, string& s)
	{
		s.clear();
		char ch;
		char buff[128];
		//in >> ch;//默认把空格当作分隔符、换行,不读取
		ch = in.get();//C++中读取一个字符
		size_t i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 127)
			{
				buff[127] = '\0';
				s += buff;
				i = 0;
			}
			//s += ch;//重复+=,会重复扩容,消耗较大
			ch = in.get();
		}
		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;
	}

因为在这里不需要直接访问类的私有成员,所以流插入和流提取可以不用重载成string类的友元函数。

对于流提取,如果频繁的尾插,会造成频繁扩容。而且C++的扩容和C语言的扩容不一样,C++使用new不能原地扩容,只能异地扩容,异地扩容就会导致新空间的开辟、数据的拷贝、旧空间释放。为了防止频繁扩容,我们可以创建一个可以存储128字节的数组,作为缓冲,如果数组满了,则将这个字符数组追加到s上,如果没慢,但是遇到空格或者换行了也需要追加。

另外由于C++的标准输入流默认把空格和换行当作分隔符,不读取,所以这里要用in.get()来接收字符。

✨getline

基本上可以直接复用流提取的代码。

//读取空格
	istream& getline(istream& in, string& s)
	{
		s.clear();
		char ch;
		//in >> ch;//默认把空格当作分隔符、换行,不读取
		ch = in.get();//C++中读取一个字符
		while ( ch != '\n')
		{
			s += ch;
			ch = in.get();
		}
		return in;
	}

✨string其他基本功能

        size_t size() const
		{
			return _size;
		}
		size_t capacity() const
		{
			return _capacity;
		}
        void clear()
		{
			_size = 0;
			_str[_size] = '\0';
		}
		

重载运算符,要写成全局的函数。

bool operator==(const string& a ,const string& b)
	{
		int ret = strcmp(a.c_str(), b.c_str());
		return ret == 0;
	}
	bool operator<(const string& a, const string& b)
	{
		int ret = strcmp(a.c_str(), b.c_str());
		return ret < 0;
	}
	bool operator<=(const string& s1, const string& s2)
	{
		return  (s1 < s2) || (s1 == s2);
	}
	bool operator>(const string& s1, const string& s2)
	{
		return  !(s1 <= s2);
	}
	bool operator>=(const string& s1, const string& s2)
	{
		return  !(s1 < s2);
	}
	bool operator!=(const string& s1, const string& s2)
	{
		return  !(s1 == s2);
	}

____________________

⭐感谢你的阅读,希望本文能够对你有所帮助。如果你喜欢我的内容,记得点赞关注收藏我的博客,我会继续分享更多的内容。⭐

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/623636.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

一个完美的回到顶部按钮

大家好,我是 Just,这里是「设计师工作日常」,今天给大家写了一个丝滑回到顶部的按钮,原生js实现的,兼容性所有主流浏览器,可在vue中使用,适用于网页、h5等。 最新实例通过公众号「设计师工作日常」发布。 目录 整体效果核心代码html 代码css 部分代码js 部分代码完整代…

网络网络层之(5)IPv6协议

网络网络层之(5)IPv6协议 Author: Once Day Date: 2024年5月12日 一位热衷于Linux学习和开发的菜鸟&#xff0c;试图谱写一场冒险之旅&#xff0c;也许终点只是一场白日梦… 漫漫长路&#xff0c;有人对你微笑过嘛… 全系列文档可参考专栏&#xff1a;通信网络技术_Once-Day…

啥都吃的打字练习软件TL(TypeLetters)

今天给大家安利一款啥都吃的打字练习软件——TL&#xff08;TypeLetters&#xff09;。 为什么说TL啥都吃呢&#xff1f;很简单&#xff0c;因为不管是科技的、还是时事的、或者潮流的、或者热点的&#xff0c;凡是英文的资料TL都能通通吃下去&#xff0c;所以说TL是来者不拒&…

QT C++ widget layout 嵌套 例子2

在上篇文章中描述了实中套虚&#xff08;用setLayout&#xff09;&#xff0c;虚中套实&#xff08;用addWidget&#xff09;。 本文再加1条&#xff0c;虚中套虚&#xff08;用addLayout&#xff09;。 所谓虚中套虚&#xff0c;是layout 套 layout 。 另外用循环代码生成从…

新书速览|MATLAB科技绘图与数据分析

提升你的数据洞察力&#xff0c;用于精确绘图和分析的高级MATLAB技术。 本书内容 《MATLAB科技绘图与数据分析》结合作者多年的数据分析与科研绘图经验&#xff0c;详细讲解MATLAB在科技图表制作与数据分析中的使用方法与技巧。全书分为3部分&#xff0c;共12章&#xff0c;第1…

【鸿蒙+全国产瑞芯微】智慧楼宇解决方案 | 如何实现多场景下智慧化、精细化楼宇管理?

随着数字化、智能化与工作生活的联结日渐紧密&#xff0c;聚焦人性化服务&#xff0c;以数字和科技匹配多重需求&#xff0c;加速商业楼宇智能化转型的脚步&#xff0c;逐步形成智慧楼宇产品矩阵。 方案亮点 01/数字标牌——形象展示 企业文化宣传、公告通知等 播放内容统一远…

blender 制作圆角立方体模型,倒角实现。cocos 使用。导出fbx

图片&#xff1a; 步骤&#xff1a; 1.首先创建一个立方体&#xff0c;这里可以使用默认的立方体。 2.在属性面板选择如“扳手”图标一样的修改器工具。 3.设置数量和段数实现圆角的圆滑效果&#xff0c;没有菱角。 保存导出相关的教程&#xff1a;

object.key()用法

object.key(obj) 一、概念&#xff1a;返回一个由一个给定对象的自身可枚举属性组成的数组。 二、用法&#xff1a; 1、参数为对象&#xff1a;则返回为 对象属性名组成的数组。 let obj {日期&#xff1a;date,姓名&#xff1a;userName,地址:address}console.log(Object.k…

使用Go和JavaScript爬取股吧动态信息的完整指南

引言 在现代金融生态系统中&#xff0c;信息流动的速度和效率对于市场的健康和投资者的成功至关重要。股市信息&#xff0c;特别是来自活跃交流平台如股吧的实时数据&#xff0c;为投资者提供了一个独特的视角&#xff0c;帮助他们洞察市场趋势和投资者情绪。这些信息不仅能够…

Bovine Serum Albumin ELISA kit(牛血清白蛋白)

牛血清白蛋白&#xff08;Bovine serum albumin, BSA&#xff09;是一种提取自牛的血清白蛋白。成熟的牛血清白蛋白含有583个氨基酸。像其他血清白蛋白一样&#xff0c;牛血清白蛋白在毛细血管内提供胶体渗透压&#xff0c;运输脂肪酸、胆红素、矿物质和激素&#xff0c;并在作…

Windows电脑使用Docker安装AList网盘神器并配置公网地址打造私人云存储空间

文章目录 前言1. 使用Docker本地部署Alist1.1 本地部署 Alist1.2 访问并设置Alist1.3 在管理界面添加存储 2. 安装cpolar内网穿透3. 固定Alist公网地址 前言 本文和大家分享如何在Windows系统使用Docker本地部署Alist全平台网盘神器&#xff0c;然后结合cpolar内网穿透工具实现…

论文阅读:The Unreasonable Ineffectiveness of the Deeper Layers 层剪枝与模型嫁接的“双生花”

作者实证研究了针对流行的开放式预训练 LLM 系列的简单层修剪策略&#xff0c;发现在不同的 QA 基准上&#xff0c;直到去掉一大部分&#xff08;最多一半&#xff09;层&#xff08;Transformer 架构&#xff09;后&#xff0c;性能的下降才会降到最低。为了修剪这些模型&…

自然资源-“十四五”规划引领,审批智慧化提升-值得学习

自然资源-“十四五”规划引领&#xff0c;审批智慧化提升-值得学习 2022年1月12日&#xff0c;国务院正式印发了《“十四五”数字经济发展规划》&#xff08;国发〔2021〕29号&#xff09;&#xff0c;从八个方面对“十四五”期间我国数字经济发展做出总体部署。其中第五点要求…

Q1季度电饭煲家电行业线上市场(京东天猫淘宝)销售数据排行榜

鲸参谋监测的2024年Q1季度线上电商平台&#xff08;天猫淘宝京东&#xff09;电饭煲家电销售数据已出炉&#xff01; 今年Q1季度&#xff0c;电饭煲销售成绩不如预期。根据鲸参谋数据显示&#xff0c;今年Q1季度在线上电商平台&#xff08;淘宝天猫京东&#xff09;电饭煲销量…

Qt-FFmpeg开发-打开摄像头直接显示YUYV422图像(12)

Qt-FFmpeg开发-打开摄像头直接显示YUYV422图像&#x1f4c0; 文章目录 Qt-FFmpeg开发-打开摄像头直接显示YUYV422图像&#x1f4c0;1、概述&#x1f4f8;2、实现效果&#x1f4bd;3、主要代码&#x1f50d;4、完整源代码&#x1f4d1; 更多精彩内容&#x1f449;个人内容分类汇…

Android Saving Activity State使用说明和注意事项

1、说明 在管理activity生命周期的简单介绍中提到当一个activity被暂停或停止时&#xff0c;该activity的状态被保留。因为当activity对象被暂停或停止时仍然保留在内存中&#xff0c;所有有关成员的信息和当前的状态仍然可用。这样&#xff0c;用户对该activity所做的任何更改…

全国大学生数学建模竞赛【集训营E题】丨 近5年赛题实现,模拟参赛体验

全国大学生数学建模竞赛E题集训营即将开营 基于Python的近5年E题数学建模基础巩固 近5年E题赛题实现 模拟参赛体验与作品评审

Ansys Zemax|HUD 设计实例

说明 本文介绍了HUD设计实例。 实例说明 规格如下&#xff1a; 显示器尺寸&#xff1a;24*8mm 眼盒尺寸&#xff1a;100*40mm 放大倍率&#xff1a;5 &#xff08;虚像尺寸 120*40mm&#xff09; 虚像距离&#xff1a;1.8m 最终光学系统的整体布局如下图所示。 从HUD发出的…

【Linux取经路】文件中的数据是如何被写进磁盘的?

文章目录 一、操作系统对物理内存的管理1.1 物理内存与磁盘的数据交换1.2 操作系统对物理内存的管理 二、再来看文件打开和写入2.1 文件页缓冲区的引入2.2 向文件中写入的过程 三、结语 一、操作系统对物理内存的管理 1.1 物理内存与磁盘的数据交换 物理内存与磁盘之间的数据…

专业的服贸会服务团队-媒体邀约宣传

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 2024服贸会开展在即&#xff0c;许多企业都做好了的参展的准备&#xff0c;北京麦塔文化提供专业的展览展会服务&#xff0c;下面做个简单介绍。 、会场搭建团队&#xff1a; 负责整个活…