【C++】string的深入学习与模拟实现

news2024/12/23 12:52:53

在这里插入图片描述

🚀write in front🚀
📜所属专栏:初阶数据结构
🛰️博客主页:睿睿的博客主页
🛰️代码仓库:🎉VS2022_C语言仓库
🎡您的点赞、关注、收藏、评论,是对我最大的激励和支持!!!
关注我,关注我,关注我你们将会看到更多的优质内容!!

在这里插入图片描述

文章目录

  • 前言
  • 一.string的成员变量:
  • 二.string的成员函数:
    • 1.构造函数:
    • 2.析构函数:
    • 3.赋值运算符重载:
    • 4.对于深拷贝浅拷贝问题的说明与解决方案:
  • 三.string的迭代器
  • 四.string的容量操作
  • 五.string的元素访问
  • 六.string的修饰操作:
    • push_back插入字符:
    • append插入字符串:
    • +=重载:
    • insert插入数据:
    • erase删除数据:
  • 七.string的字符串操作
    • find查找字符位置:
    • substr返回所寻子串
    • c_str返回字符串数组
  • 八.string的非成员函数重载
    • 比较运算符重载:
    • 流输出运算符:
    • 流输入运算符:
  • 总结

前言

  在C++中,字符串是一种非常常见的数据类型,用于存储和操作文本信息。标准库中的string类提供了强大的字符串操作功能,但是了解其背后的实现原理对于深入理解和灵活运用字符串类至关重要。在本篇博客中,我们将一起学习标准库中string类的实现原理,并模拟实现自己的string类。

一.string的成员变量:

在这里插入图片描述
在标准库里面的string的成员变量看起来很复杂,但是其实本质就是一个顺序表,下面是对于该部分的模拟实现:

namespace zxr
{
	class bit
	{
	private:
		size_t _size;
		size_t _capacity;
		char* _str;
	};
};	

在这里我们要注意的是不同编译器下的成员变量会有微微的不同,比如vs下面,当成员长度小于16时,就会通过一个16个字节大小的数组来存储,当大于16时才是用堆来存储。
在这里插入图片描述

二.string的成员函数:

在这里插入图片描述

1.构造函数:

  构造函数有很多的重载:
在这里插入图片描述
其中最常见的是如下:

void Teststring()
{
 string s1; // 构造空的string类对象s1
 string s2("hello bit"); // 用C格式字符串构造string类对象s2
 string s3(s2); // 拷贝构造s3
}

下面我们来模拟实现一个构造函数

string(const char* str = "")
	{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
	}

这一个构造函数是一个默认的构造函数,将传进来的c字符串存入顺序表中,并且没有初始化的string类也能够进行初始化。

  有了构造函数还是远远不够的,对于复制构造函数,如果我们不通过自己写的深拷贝而调用编译器自己的浅拷贝,在析构的时候就会析构两次,产生错误:
在这里插入图片描述

所以我们要自己实现复制构造函数

string(const string& s)
		{
			_str = new char[s._capacity + 1];
			//strcpy(_str, s._str);
			memcpy(_str, s._str, s._size + 1);
			_size = s._size;
			_capacity = s._capacity;
		}

2.析构函数:

在这里插入图片描述

~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

3.赋值运算符重载:

在这里插入图片描述

  对于赋值运算符重载有两种方法实现:

  • 常规方法:
string& operator=(const string& s)
		{
			if (this != &s)
			{
				char* tmp = new char[s._capacity + 1];
				memcpy(tmp, s._str, s._size+1);
				delete[] _str;
				_str = tmp;

				_size = s._size;
				_capacity = s._capacity;
			}

			return *this;
		}
  • 简单方法:
void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
string& operator=(string tmp)
		{
			swap(tmp);
			return *this;
		}

在这里我们调用了库里面的swap函数,注意看,我们的tmp是一个形参,并没有用引用(很重要的一点),在出函数以后就会调用析构函数自动销毁,所以我们就可以放心的使用swap函数对其进行操作啦。

4.对于深拷贝浅拷贝问题的说明与解决方案:

  我们之所以要深拷贝,原因就是对于在堆上面开辟的空间,如果我们单纯使用浅拷贝就会产生下面的问题

  1. 析构两次
  2. 一个对象修改会影响另一个

在实际的编译器里会通过引用计数的方式来解决:

在这里插入图片描述
大概过程如下:

  • 构造函数中创建类的新对象时,初始化引用计数为1;
  • 拷贝构造函数复制指针,并使相应的引用计数增加1;
  • 赋值操作减少左操作数所值对象的引用计数,增加右操作数所指对象的引用计数;
  • 析构函数使引用计数减少1,并且当引用计数为1时,释放指针说指向的对象;

  简单的说,就是对象实例化之后,如果出现了复制构造,那么这个类的计数就会+1,如果这个对象要析构,先检查引用计数的次数是否等于1,不等于1就不释放这个空间,让引用计数-1,如果等于1在析构。

  对于要修改的对象,我们就要通过深拷贝构造出一个新对象,然后将引用计数-1就可以了!
在这里插入图片描述
gcc测试如下:
在这里插入图片描述

三.string的迭代器

  对于string类型,其迭代器就是指针。
在这里插入图片描述
所以我们可以通过重命名的方式来写出迭代器的类型:

typedef char* iterator;
		typedef const char* const_iterator;

下面我们模拟最简单beginend
在这里插入图片描述
对于begin(),模拟如下:

		iterator begin()
		{
			return _str;
		}
		const_iterator begin() const
		{
			return _str;
		}

对于end(),模拟如下:
在这里插入图片描述

		iterator end()
		{
			return _str + _size;
		}


		const_iterator end() const
		{
			return _str + _size;
		}

一个是只读一个是可读可写,这里就不必多说。

在就是迭代器的使用,其实当我们写好了迭代器,我们auto的语法糖也可以直接用了

//zxr::string::const_iterator cit = s3.begin();
	auto cit = s3.begin();
	while (cit != s3.end())
	{
		//*cit += 1;

		cout << *cit << " ";
		++cit;
	}
	cout << endl;

	zxr::string::iterator it = s1.begin();
	while (it != s1.end())
	{
		*it += 1;

		cout << *it << " ";
		++it;
	}
	cout << endl;

	for (auto ch : s1)
	{
		cout << ch << " ";
	}
	cout << endl;

其实auto的底层也就是通过迭代器来遍历string.

四.string的容量操作

6
这几个容量操作的函数非常好模拟实现,我们就说几个注意事项就可以了。

	返回数组大小size
		size_t size() const
		{
			return _size;
		}
		
	对string对象预留空间,注意:是空间,size大小没有改变
			void reserve(size_t n)
		{
			if (n > _capacity)
			{
				cout << "reserve()->" << n << endl;

				char* tmp = new char[n + 1];
				//strcpy(tmp, _str);
				memcpy(tmp, _str, _size+1);

				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
		}
		
	对string对象预留空间并将size也增加了,是上面的升级版,size和capacity大小都改变了
		void resize(size_t n, char ch = '\0')
		{
			if (n < _size)
			{
				_size = n;
				_str[_size] = '\0';
			}
			else
			{
				reserve(n);

				for (size_t i = _size; i < n; i++)
				{
					_str[i] = ch;
				}

				_size = n;
				_str[_size] = '\0';
			}
		}
		
	将string中有效字符清空,不改变底层空间大小。
		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}

注意事项:

  1. size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
  2. clear()只是将string中有效字符清空,不改变底层空间大小。
  3. resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
  4. reserve:为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。

五.string的元素访问

在这里插入图片描述
这里元素访问,我们最熟悉的就是[]
在这里插入图片描述
模拟实现非常简单,不必多说:

		char& operator[](size_t pos)
		{
			assert(pos < _size);

			return _str[pos];
		}

		const char& operator[](size_t pos) const 
		{
			assert(pos < _size);

			return _str[pos];
		}

六.string的修饰操作:

在这里插入图片描述
这里的模拟实现就很需要大家的基本功了。

push_back插入字符:

在这里插入图片描述

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

			_str[_size] = ch;

			++_size;
			_str[_size] = '\0';
		}

append插入字符串:

在这里插入图片描述

		void append(const char* str)
		{
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				// 至少扩容到_size + len
				reserve(_size+len);
			}

			//strcpy(_str + _size, str);
			memcpy(_str + _size, str, len+1);

			_size += len;
		}

+=重载:

在这里插入图片描述

		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}

insert插入数据:

在这里插入图片描述

		void insert(size_t pos, size_t n, char ch)
		{
			assert(pos <= _size);

			if (_size +n > _capacity)
			{
				// 至少扩容到_size + len
				reserve(_size + n);
			}
			// 添加注释最好
			// 防止越界
			size_t end = _size;
			while (end >= pos && end != npos)
			{
				_str[end + n] = _str[end];
				--end;
			}

			for (size_t i = 0; i < n; i++)
			{
				_str[pos + i] = ch;
			}

			_size += n;
		}

		void insert(size_t pos, const char* str)
		{
			assert(pos <= _size);

			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				// 至少扩容到_size + len
				reserve(_size + len);
			}

			// 添加注释最好
			//防止越界
			size_t end = _size;
			while (end >= pos && end != npos)
			{
				_str[end + len] = _str[end];
				--end;
			}

			for (size_t i = 0; i < len; i++)
			{
				_str[pos + i] = str[i];
			}

			_size += len;
		}

erase删除数据:

在这里插入图片描述

		void erase(size_t pos, size_t len = npos)
		{
			assert(pos <= _size);

			if (len == npos || pos + len >= _size)
			{
				//_str[pos] = '\0';
				_size = pos;
				_str[_size] = '\0';
			}
			else
			{
				size_t end = pos + len;
				while (end <= _size)
				{
					_str[pos++] = _str[end++];
				}
				_size -= len;
			}
		}

七.string的字符串操作

在这里插入图片描述

find查找字符位置:

返回该字符的下标

		size_t find(char ch, size_t pos = 0)
		{
			assert(pos < _size);

			for (size_t i = pos; i < _size; i++)
			{
				if (_str[i] == ch)
				{
					return i;
				}
			}

			return npos;
		}

		size_t find(const char* str , size_t pos = 0)
		{
			assert(pos < _size);

			const char* ptr = strstr(_str + pos, str);
			if (ptr)
			{
				return ptr - _str;
			}
			else
			{
				return npos;
			}
		}

substr返回所寻子串

		string substr(size_t pos = 0, size_t len = npos)
		{
			assert(pos < _size);

			size_t n = len;
			if (len == npos || pos + len > _size)
			{
				n = _size - pos;
			}

			string tmp;
			tmp.reserve(n);
			for (size_t i = pos; i < pos + n; i++)
			{
				tmp += _str[i];
			}
			return tmp;
		}

c_str返回字符串数组

返回字符串数组的时候使用,适用于printf打印。


		const char* c_str() const
		{
			return _str;
		}

八.string的非成员函数重载

在这里插入图片描述

比较运算符重载:

bool operator<(const string& s) const
		{
			int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);
			return ret == 0 ? _size < s._size : ret < 0;
		}

		bool operator==(const string& s) const
		{
			return _size == s._size 
				&& memcmp(_str, s._str, _size) == 0;
		}

		bool operator<=(const string& s) const
		{
			return *this < s || *this == s;
		}

		bool operator>(const string& s) const
		{
			return !(*this <= s);
		}

		bool operator>=(const string& s) const
		{
			return !(*this < s);
		}

		bool operator!=(const string& s) const
		{
			return !(*this == s);
		}

流输出运算符:

在这里插入图片描述

ostream反拷贝,所以用引用。
ostream& operator<<(ostream& out, const string& s)
	{
		/*for (size_t i = 0; i < s.size(); i++)
		{
			out << s[i];
		}*/

		for (auto ch : s)
		{
			out << ch;
		}

		return out;
	}

  在这里我们可以明显看出在打印string对象时和字符串数组的区别,字符串数组打印时以’\0’为结束,而string对象以迭代器的end结束,所以string里面如果存在’\0’也没关系。这就是为什么上面的拷贝要用memcpy而不是strcpy!

流输入运算符:

在这里插入图片描述

istream& operator>>(istream& in, string& s)
	{
		s.clear();
		//处理掉之前对象里的内容
		char ch = in.get();
		// 处理前缓冲区前面的空格或者换行
		while (ch == ' ' || ch == '\n')
		{
			ch = in.get();
		}

		//in >> ch;
		char buff[128];
		int i = 0;

		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 127)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}

			//in >> ch;
			ch = in.get();
		}

		if (i != 0)
		{
			buff[i] = '\0';
			s += buff;
		}

		return in;
	}
};

这里由于我们不知道我们要输入多少字符,不能预先开好空间,所以只能一个字符一个字符的输入。但是我们也不能通过cin>>ch来输入字符,因为cin只有读到空格换行符的时候才能读取成功一个,对于一段完整的字符串,我们要使用该类里面的另一个函数get()来读取才行。该函数输入一个字符就读取,符合该功能。
在这里插入图片描述
对于中间开的buff数组,只是为了增加效率,不然每输入一个字符就扩容一次,效率低下。

总结

  这就是string类的深入学习与模拟实现啦!如果在使用方面还有上面疑惑的话可以去官网查看或者私信我c++使用手册,想看模拟实现相关代码的可以去我的giteestring模拟实现上看!如果本篇文章有上面错误的话请私信我哟!

  更新不易,辛苦各位小伙伴们动动小手,👍三连走一走💕💕 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!

参考文献:https://legacy.cplusplus.com/

专栏订阅:
每日一题
C语言学习
算法
智力题
初阶数据结构
Linux学习
C++学习
更新不易,辛苦各位小伙伴们动动小手,👍三连走一走💕💕 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python 模块 ddt 数据驱动测试

简介 ddt 提供了一种方便的方法来实现数据驱动测试&#xff08;Data-Driven Testing&#xff09;。数据驱动测试是一种测试方法&#xff0c;通过将测试数据与测试逻辑分开&#xff0c;可以使用不同的数据集来运行相同的测试用例。这样可以提高测试的灵活性和可维护性&#xff0…

SQL SERVER ANALYSIS SERVICES决策树、聚类、关联规则挖掘分析电商购物网站的用户行为数据...

全文链接&#xff1a;http://tecdat.cn/?p32118 假如你有一个购物类的网站&#xff0c;那么你如何给你的客户来推荐产品呢&#xff1f;&#xff08;点击文末“阅读原文”获取完整文档、数据&#xff09; 相关视频 这个功能在很多电商类网站都有&#xff0c;那么&#xff0c;通…

Flink CEP (一)原理及概念

目录 1.Flink CEP 原理 2.Flink API开发 2.1 模式 pattern 2.2 模式 pattern属性 2.3 模式间的关系 1.Flink CEP 原理 Flink CEP内部是用NFA&#xff08;非确定有限自动机&#xff09;来实现的&#xff0c;由点和边组成的一个状态图&#xff0c;以一个初始状态作为起点&am…

Unity进阶-消息框架的理论知识与实际操作学习笔记

文章目录 Unity进阶-消息框架的理论知识与实际操作学习笔记 Unity进阶-消息框架的理论知识与实际操作学习笔记 笔记来源课程&#xff1a;https://study.163.com/course/courseMain.htm?courseId1212756805&_trace_c_p_k2_8c8d7393c43b400d89ae94ab037586fc 这种框架其实…

实现锂电池形状的数据可视化css+js

1.效果图 2.需求根据后端返回数据改变里面的高度 HTML&#xff1a; <div class"dianchichi"><div class"limian" id"divElementId"></div></div> css: .dianchichi {width: 84px;height: 146px;display: flex;justify-…

Two Days wpf 分享 分页组件

迟来的wpf分享。 目录 一、序言 二、前期准备 三、前端界面 四、后台代码部分 1、先定义些变量后面使用 2、先是按钮事件代码。 首页按钮 上一页按钮 下一页按钮 末尾按钮 画每页显示等数据 每页显示多少条 判断是否为数字的事件 分页数字的点击触发事件 跳转到…

Docker安装Nexus并配置Maven私服

1 准备工作 1 服务器已安装docker, docker各命令无报错 2 通过dockerhub查看nexus的版本信息&#xff0c;此次使用的镜像为&#xff1a;sonatype/nexus3&#xff0c;可以看到latest版本更前的的是3.58.0&#xff0c;我们这次就使用这个版本的nexus3. 2 开始安装 # 下载镜像 do…

springcloudAlibaba之springboot如何加载nacos配置文件

配置文件想必大家都很熟悉&#xff0c;无论什么架构 都离不开配置&#xff0c;虽然spring boot已经大大简化了配置&#xff0c;但如果服务很多 环境也好几个&#xff0c;管理配置起来还是很麻烦&#xff0c;并且每次改完配置都需要重启服务&#xff0c;nacos config出现就解决了…

【JavaEE】Servlet常用的API

目录 前言 一、HttpServlet类 1、Servlet的生命周期 ✨tomcat的两个端口 ✨设置告诉浏览器使用那种字符集解析响应 ✨Java中Unicode和utf8字符集的使用 二、HttpServletRequest类 1、获取请求的信息 2、 前端给后端传递数据的三种方式 2.1、通过query string传递 2.2…

unity--2d( A*寻路)

目录 一.网格式寻路 1.创建一个A*寻路脚本&#xff0c;命名为"AStarPathfinding.cs"。 2.创建一个人物控制的脚本&#xff0c;命名为"CharacterController2D.cs"。 3.创建一个游戏管理脚本&#xff0c;命名为"GameManager.cs"。 二.UGUI下的…

《零基础入门学习Python》第063讲:论一只爬虫的自我修养11:Scrapy框架之初窥门径

上一节课我们好不容易装好了 Scrapy&#xff0c;今天我们就来学习如何用好它&#xff0c;有些同学可能会有些疑惑&#xff0c;既然我们懂得了Python编写爬虫的技巧&#xff0c;那要这个所谓的爬虫框架又有什么用呢&#xff1f;其实啊&#xff0c;你懂得Python写爬虫的代码&…

基于半监督算法的工业图像缺陷检测方法:MemSeg

来源&#xff1a;投稿 作者&#xff1a;橡皮 编辑&#xff1a;学姐 论文&#xff1a;https://arxiv.org/ftp/arxiv/papers/2205/2205.00908.pdf 代码&#xff1a;https://github.com/TooTouch/MemSeg 主要贡献 提出了一个精心设计的异常模拟策略&#xff0c;用于模型的自监督…

缓存雪崩问题及解决思路

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机&#xff0c;导致大量请求到达数据库&#xff0c;带来巨大压力。为了解决这个问题&#xff0c;我们可以采取以下几种方案。 1. 给不同的Key的TTL添加随机值 在设置缓存的过期时间&#xff08;TTL&#xff09;时…

PhpStudy靶场首页管理

PhpStudy靶场首页管理 一、源码一二、源码二三、源码三四、源码四 一、源码一 index.html <!DOCTYPE html> <html><head><meta charset"UTF-8"><title>靶场访问首页</title><style>body {background-color: #f2f2f2;colo…

一个月学通Python(二十三):RESTful架构和DRF入门

专栏介绍 结合自身经验和内部资料总结的Python教程&#xff0c;每天3-5章&#xff0c;最短1个月就能全方位的完成Python的学习并进行实战开发&#xff0c;学完了定能成为大佬&#xff01;加油吧&#xff01;卷起来&#xff01; 全部文章请访问专栏&#xff1a;《Python全栈教…

【字符流】案例:集合到文件(改进版)

案例&#xff1a;集合到文件&#xff08;改进版&#xff09; 1.需求&#xff1a; 把ArrayList集合中的学生数据写入到文本文件。要求&#xff1a;每一个学生对象的数据作为文件中的一行数据 ​ 格式&#xff1a;学号&#xff0c;姓名&#xff0c;年龄&#xff0c;居住地 2.思…

python与深度学习(五):CNN和手写数字识别

目录 1. 说明2. 卷积运算3. 填充4. 池化5. 卷积神经网络实战-手写数字识别的CNN模型5.1 导入相关库5.2 加载数据5.3 数据预处理5.4 数据处理5.5 构建网络模型5.6 模型编译5.7 模型训练、保存和评价5.8 模型测试5.9 模型训练结果的可视化 6. 手写数字识别的CNN模型可视化结果图7…

HideSeeker论文阅读

文章目录 3.1 Overview of Our System HideSeeker3.2 Visual Information Extraction3.3 Relation Graph Learning3.4 Hidden Object Inference 4 EVALUATIONS4.7 Summary 6 DISCUSSIONS AND CONCLUSION 3.1 Overview of Our System HideSeeker 我们设计了一种名为“HideSeeke…

【Selenium+Pytest+allure报告生成自动化测试框架】附带项目源码和项目部署文档

目录 前言 【文章末尾给大家留下了大量的福利】 测试框架简介 首先管理时间 添加配置文件 conf.py config.ini 读取配置文件 记录操作日志 简单理解POM模型 简单学习元素定位 管理页面元素 封装Selenium基类 创建页面对象 简单了解Pytest pytest.ini 编写测试…

保护数字世界的壁垒

随着科技的不断发展和互联网的普及&#xff0c;我们的生活日益依赖于数字化的世界。然而&#xff0c;随之而来的是网络安全威胁的不断增加。网络攻击、数据泄露和身份盗窃等问题已经成为我们所面临的现实。因此&#xff0c;网络安全变得尤为重要&#xff0c;我们需要采取措施来…