C++STL的string模拟实现

文章目录

  • 前言
  • string的成员变量
  • 成员函数
    • 构造函数
    • 拷贝构造
    • 赋值重载
  • 模拟实现string各种接口
    • print
    • 迭代器
      • 普通迭代器
      • const迭代器
    • string比较大小
    • push_back
    • insert 和 erase
      • insert
      • erase
    • reserve和resize
      • reserve
      • resize
    • swap
    • find
    • cout和cin
      • cout
      • cin

前言

今天要讲string的底层实现,通过自己来实现string,我们对string的理解才能更加的深刻。
我们对string其实既熟悉又陌生,熟悉sting其实就是字符串,陌生是在于管理字符串这样一个类。

string的成员变量

namespace but
{
	class string
	{
	private:
		char* _str;
		size_t _capaicty;
		size_t _size;
	};
}

我们为了避免自己定义的string于库里面的傻傻分不清,这里我们自己用了一个命名空间把自己写的string封装起来。

成员函数

构造函数

namespace but
{
	class string
	{
	public:
		string()
			:_str(nullptr),
			_capaicty(0),
			_size(0)
		{}
		string(const char* str)
			:_str(str),
			_capaicty(strlen(str)),
			_size(strlen(str))//容量不包括'\0'
		{}
	private:
		const char* _str;//加上const,防止写构造函数时,权限放大编译不通过
		size_t _capaicty;
		size_t _size;
	};
}

简简单单写了上面的构造函数,其实这里面存在两个问题,下面我们通过一些使用来看一下。
第一个问题。

写个c_str,思考一下为什么程序会崩?

const char* c_str()
{
	return _str;
}
string s1;
string s2("hello world");
cout << s1.c_str() << endl;//上述代码都是写在类里面
cout << s2.c_str() << endl;

流插入是自动识别类型,它识别出const char*, 然后去解引用,然后遇到‘\0’结束,这样空指针的问题就暴露出来了。

继续看第二个问题

const char& operator[](size_t pos)//按照之前写的构造函数,必须加上const
{
	assert(pos < _size);
	return _str[pos];
}

这里面有个很坑的问题,我们是呆会是要修改pos位置的字符,并且如果空间不够还需要扩容,比如+=;那这里就变得非常矛盾。

这是什么原因呢?

string s2("hello world")

s2在常量区无法修改,扩容也无法扩。

如何解决这两个问题呢?
其实根源还是在于初始化列表,我们大多数情况下都是推荐把所有成员变量直接放到初始化列表初始化,这里比较特殊。
其次,我们要想修改pos位置的字符,还想扩容,在初始化的时候空间就不能直接赋值过去,最好new出来。

那经过修改之后,我们的代码

namespace but
{
	class string
	{
	public:
		string()
			:_str(new char[1]),//要解决第一个问题,这里就不能是空
			_capacity(0),
			_size(0)
		{
			_str[0] = '\0';
		}
		string(const char* str)
			:_capacity(strlen(str))
		{
			_size = _capacity;//没有必要重复用strlen,strlen是o(N)的接口
			_str = new char[_capacity + 1];//扩容的时候应该+1,包括\0
			strcpy(_str, str);
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
			_capacity =_size= 0;
		}
		const char* c_str()
		{
			return _str;
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
	private:
	    char* _str;//加上const,防止写构造函数时,权限放大编译不通过
		size_t _capacity;
		size_t _size;
	};
	void test_string1()
	{
		string s1;
		string s2("hello world");
		cout << s1.c_str() << endl;
		cout << s2.c_str() << endl;
		s2[0]++;
		cout << s2.c_str() << endl;
	}
}

至此,把上面的问题都解决了。
在这里插入图片描述

拷贝构造还可以继续优化一下,优化成只有一个全缺省的构造函数。

//string(const char* str = nullptr)  //不可以,等下strlen解引用会崩
//string(const char* str = '\0')//不可以,类型不匹配
//string(const char* str = "\0")//可以
string(const char* str = "")//可以
	:_size(strlen(str))
{
	_capaicty = _size == 0 ? 3 : _size;
	_str = new char[_capaicty + 1];
	strcpy(_str, str);
}

拷贝构造

void test_string2()
{
	string s2("hello world");

	string s3(s2);
	cout << s2.c_str() << endl;
	cout << s3.c_str() << endl;
}

我们之前说过拷贝构造是默认成员函数,我们不写,编译器会自动生成一个,对自定义类型不做处理,对内置类型做值拷贝或浅拷贝。那我们看一下自动生成的拷贝构造。
在这里插入图片描述
这个是经典的值拷贝或浅拷贝问题,我们之前也讲过,接下来既然有一个具体的场景,就用调试带大家看一下。
在这里插入图片描述
看两个地址完全一摸一样。

这样会带来两个问题。
1.一个修改影响另外一个。
2.同一块空间会析构两次。

那我们需要自己写一个深拷贝的拷贝构造,怎么写呢?

//拷贝构造也有初始化列表
string(const string& s)
			:_size(s._size)
			, _capaicty(s._capaicty)
		{
			_str = new char[s._capaicty + 1];
			strcpy(_str, s._str);
		}

赋值重载

赋值重载和拷贝构造也一摸一样,我们不写的话,编译器自动生成的会出问题。
写成这样,那就考虑的太不全面了

string& operator=(const string& s)
{
	_size = s._size;
	_capacity = s._capacity;
	_str = new char[s._capacity + 1];
	strcpy(_str, s._str);
	return *this;
}

我们知道拷贝构造是一块已经存在的空间给另一块还没存在的空间。
而赋值重载是两块都已经存在的空间,所以赋值重载还需要从空间的角度去分析问题。

从空间大小考虑,总共有三种情况
在这里插入图片描述

但是存在一个问题,如果s3空间特别大,s1又非常小,把s1直接赋值过去,s3就会浪费很多空间,所以比较好的方式就是再开一块空间。
我们库里面的string实现不会这么麻烦,直接把旧的空间释放掉,开一块一样大的空间。

还要处理自己给自己赋值,以免造成不必要的麻烦。

string& operator=(const string& s)
		{
			if (this != &s)
			{
				//这种写法稍微不好一点
				//抛异常的时候会把s1给破坏掉
				/*delete[] _str;
				_str = new char[s._capaicty + 1];
				strcpy(_str, s._str);
				_size = s._size;
				_capaicty = s._capaicty;*/

				char* tmp = new char[s._capaicty + 1];
				strcpy(tmp, s._str);
				delete[] _str;
				_str = tmp;

				_size = s._size;
				_capaicty = s._capaicty;
			}

模拟实现string各种接口

print

这里为什么报错?
在这里插入图片描述

这也涉及到我们之前讲过的。** cosnt成员变量不能调用非const成员函数,这样会权限放大。**

在这里插入图片描述
紧接着这里报错又怎么解决?
这说明我们需要两个【】,一个是给const对象调用的,不允许修改。
一个是给普通对象调用的,可以修改。它们构成函数重载,因为它们函数名相同参数不一样。
虽然普通对象也可以调用const成员函数,但是编译器非常聪明,他会调用最匹配的哪个。

迭代器

遍历的方式我们还可以用迭代器,这里我们再写一个迭代器

普通迭代器

在这里插入图片描述
要实现一个迭代器其实不难。

我们支持了迭代器,其实也就支持了范围for

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

const迭代器

const迭代器能不能修改?
可以修改,只是指向的内容不能修改。

string::const_iterator it = s1.begin();
while (it != s1.end())
{
	//*it = 'x';//不能修改,只能读不能改
	++it;
}
cout << endl;

反向迭代器这里先不讲,后面再讲,要用一个适配器来实现。

string比较大小

怎样比较大小?
比较ascll值,一个一个比。

// 不修改成员变量数据的函数,最好都加上const
		bool operator>(const string& s) const
		{
			return strcmp(_str, s._str) > 0;
		}

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

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

		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);
		}

push_back

空间不够扩容的时候不能用realloc,那就和c++交叉了,容易出问题。

void push_back(char ch)
{
	if (_size + 1 > _capaicty)
	{
		reserve(_capaicty * 2);
	}
	_str[_size] = ch;
	++_size;

	_str[_size] = '\0';
}

void append(const char* str)
{
	size_t len = strlen(str);
	if (_size+len > _capaicty)
	{
		reserve(_size + len);
	}

	strcpy(_str + _size, str);
	//strcat(_str, str);//为什么不用strcat?strcat很挫自己要去找\0,\0就在size位置,能不用就不用
	_size += len;
}

在这里插入图片描述

我们喜欢使用的还是+=,直接复用push_back;

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

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

凡是你的扩容,析构上代码崩了,一般都是内存问题。

insert 和 erase

问个小小的问题,静态成员变量能不能给缺省值?
不能,因为缺省值是给初始化列表用的。静态列表不是在初始列表初始化的。
它属于整个类,不是属于某个对象。

insert

插入字符
insert有个巨坑给大家看一下下面的代码?
在这里插入图片描述

在这里插入图片描述
程序运行结果。

调试的时候发现这样,扯淡了。
在这里插入图片描述
因为end的类型是size_t;

void insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_size + 1 > _capacity)
	{
		reserve(2 * _capacity);
	}
	//int end=_size;//这样也不行,会发生类型转换,一般有符号转化为无符号。
	//改pos也不好,pos的类型一般规定都是size_t
	size_t end = _size;
	//while(end>=pos(int))//强转也不推荐
	//while (end >= pos)
	//{
	//	_str[end + 1] = _str[end];
	//	--end;
	//}
	size_t end = _size + 1;
	while (end > pos)
	{
		_str[end] = _str[end-1];
		--end;
	}

	_str[pos] = ch;
	++_size;
}

我们最好的解决思路,巧妙的避开了小于0;
在这里插入图片描述
在这里插入图片描述

插入字符串

一定要画图,不然很容易出错。

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

	size_t len = strlen(str);

	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}

	// 挪动数据
	size_t end = _size + len;
	while (end > pos + len - 1)//强烈不建议用大于等于
	{
		_str[end] = _str[end - len];
		--end;
	}
	
	//这个比较简单,完美避开了循环结束条件的难题
	/*size_t end = _size;
	for (size_t i = 0; i < _size + 1; ++i)
	{
		_str[end + len] = _str[end];
		--end;
	}*/

	// 拷贝插入
	strncpy(_str + pos, str, len);
	_size += len;

	return *this;

}

erase

erase比较简单,从pos位置删除数据就可以了。

我们浅浅分析一下所有的情况
在这里插入图片描述

erase也是不考虑缩容的。

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

	if (len == npos || pos + len >= _size)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		strcpy(_str + pos, _str + pos + len);//不需要考虑覆盖的问题,所以可以直接用strcpy
		_size -= len;
	}
	return *this;
	}

白盒测试,把三种情况都验证一遍
在这里插入图片描述

reserve和resize

reserve

看一下我们之前写的扩容有什么问题?
在这里插入图片描述

它是没有考虑缩容的。继续看这样子就报错了。
在这里插入图片描述
为什么报错呢?
strcp的时候越界了。

简单修改一下代码就变成这样了。

void reserve(size_t n)
{
	if (n > _capacity)
	{
		char* tmp = new char[n + 1];
		strcpy(tmp, _str);
		delete[] _str;
		_str = tmp;

		_capacity = n;
	}
}

resize

resize缩容吗?
不缩容。缩荣的代价还是很大的,首先是异地缩,先开另一块空间,然后把数据拷贝过去,接着把之前的空间释放掉。
待会插入数据空间不够又要扩容,这样就很麻烦。

接下来实现resize,我们得分情况讨论,以及明白resize功能上的一些细节。
在这里插入图片描述

void resize(size_t n, char ch = '\0')
{
	if (n < _size)
	{
		// 删除数据--保留前n个
		_size = n;
		_str[_size] = '\0';
	}
	else if (n > _size)
	{
		if (n > _capacity)
		{
			reserve(n);
		}
		//如果调用系统的接口,我们可以用memset
		size_t i = _size;
		while (i < n)
		{
			_str[i] = ch;
			++i;
		}

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

swap

我们实现 一下swap,其实就知道库里面的swap和类里面的效率差距有多大

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

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);
			char* p = strstr(_str + pos, str);
			if (p == nullptr)
			{
				return npos;
			}
			else
			{
				return p - _str;
			}
		}

cout和cin

现在我有一个问题,cout和cin必须实现成友元函数?这句话对不对
不对,我们可以写一些函数来访问私有成员变量。

cout

首先,我们实现cout, 它不是成员函数。

能不能直接这样搞?
在这里插入图片描述
我们之前说过c_str()和cout是有区别的,它们最大的区别就是c_str()打印时是遇到\0终止,cout是根据size来打印的。

ostream& operator<<(ostream& out, const string& s)
{
	for (auto ch : s)
	{
		out << ch;
	}
	return out;
}

cin

在这里插入图片描述
这样为什么不行?
调试一下就知道了,空格和换行不会进入缓冲区。为什么?
它会认为你输入的时候多个字符之间的间隔。

我们可以改成这样
在这里插入图片描述
仔细看一下上面的代码,功能是完善了但还有什么弊端。
在这里插入图片描述

在这里插入图片描述
这是没有把之前的数据清理掉。

还有一个问题有个流插入的数据比较长,那它会影响效率,那有没有什么方法能解决这个问题?
开小了不够,开多了浪费。这里有一个参考方式。
相当于换成字符串,可以这样理解。

istream& operator>>(istream& in, string& s)
{
	s.clear();

	char ch = in.get();
	char buff[128];
	size_t i = 0;
	while (ch != ' ' && ch != '\n')
	{
		buff[i++] = ch;
		if (i == 127)
		{
			buff[127] = '\0';
			s += buff;
			i = 0;
		}

		ch = in.get();
	}
	//防止还有数据没有+=进去
	if (i != 0)
	{
		buff[i] = '\0';
		s += buff;
	}

	return in;
}

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

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

相关文章

docker---资源控制

docker的资源控制 对容器使用宿主机的资源进行限制。 三种控制方向&#xff1a;CPU 内存 磁盘I/O docker使用linux自带的功能cgroup&#xff1b;control groups是linux内核系统提供的一种可以限制记录&#xff0c;隔离进程所使用的物理资源机制。 docker借助此…

每日一练 | 华为认证真题练习Day145

1、一台路由器通过RIP、OSPF和静态路由都学习到了到达同一目的地址的路由。默认情况下&#xff0c;VRP将最终选择通过哪种协议学习到的路由&#xff1f; A. 三种协议学习到的路由都选择 B. 静态路由 C. OSPF D. RIP 2、如果网络管理员没有配置骨干区域&#xff0c;则路由器…

el-table 表格多选(后端接口搜索分页)实现已选中的记忆功能。实现表格数据和已选数据(前端分页)动态同步更新。

实现效果&#xff1a;&#xff08;可拉代码下来看&#xff1a;vue-demo: vueDemo&#xff09; 左侧表格为点击查询调用接口查询出来的数据&#xff0c;右侧表格为左侧表格所有选择的数据&#xff0c;由前端实现分页。 两个el-table勾选数据联动更新 实现逻辑&#xff1a; el-…

MQTT协议对比TCP网络性能测试模拟弱网测试

MQTT正常外网压测数据---时延diff/ms如下图&#xff1a; MQTT弱网外网压测数据 TCP正常外网压测数据 TCP弱网外网压测数据 结论&#xff1a; 在弱网场景下&#xff0c;MQTT和TCP的网络性能表现会有所不同。下面是它们在弱网环境中的对比&#xff1a; 连接建立&#xff1a;M…

华清远见嵌入式学习——QT——作业2

作业要求&#xff1a; 代码运行效果图&#xff1a; 登录失败 和 最小化 和 取消登录 登录成功 和 X号退出 代码&#xff1a; ①&#xff1a;头文件 #ifndef LOGIN_H #define LOGIN_H#include <QMainWindow> #include <QLineEdit> //行编辑器类 #include…

Rust测试字符串的移动,Move

代码创建了一个结构体&#xff0c;结构体有test1 字符串&#xff0c;还有指向字符串的指针。一共创建了两个。 然后我们使用swap 函数 交换两个结构体内存的内容。 最后如上图。相同的地址&#xff0c;变成了另外结构体的内容。注意看指针部分&#xff0c;还是指向原来的地址…

想转行IT,有前途吗?

作为一个在工程领域工作了三年的人&#xff0c;我深知转行到 IT&#xff0c;尤其是网络安全领域&#xff0c;不是一件轻松的事。我的经历或许能为你提供一些启示。 在我之前的工作中&#xff0c;虽然工作量大、压力重&#xff0c;但总觉得缺少了某种成就感和动力。我意识到&a…

Flutter代码补全

有的时候属性不经常使用&#xff0c;就想不起来该用啥&#xff0c;只有点点印象&#xff1b;只能用代码补全功能&#xff0c;但我用了AS的默认操作发下并不好使&#xff0c;估计是快捷键冲突了。刚开始是不是下面的效果&#xff1a;这肯定不是我们想要的。 不怕&#xff0c;接下…

XML是什么

XML是是什么&#xff1f; XML&#xff08;Extensible Markup Language&#xff09;&#xff0c;中文是可扩展标记语言&#xff0c;是标准通用标记语言的子集。它是一种标记语言&#xff0c;用于标记电子文档&#xff0c;使其结构化。 XML可以用来标记数据&#xff0c;定义数据…

代码随想录算法训练营第五十九天【单调栈part2】 | 503.下一个更大元素II、42. 接雨水

503.下一个更大元素II 题目链接 力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 求解思路 重点在如何处理循环数组。 方案一&#xff1a; 直接将两个数组拼接在一起&#xff0c;然后使用单调栈求下一个最大值。 方案二&#xff1a; 在遍历的过…

提醒事项日历同步怎么设置?可实时同步日历的提醒事项工具

随着生活节奏的加快&#xff0c;我们每天都需要处理许多琐碎的事务。为了不忘记重要的事情&#xff0c;很多人选择使用提醒事项工具来帮助自己。然而&#xff0c;市场上的提醒事项工具五花八门&#xff0c;有些并不具备日历月视图功能&#xff0c;也无法与手机日历同步&#xf…

【Linux系统编程】项目自动化构建工具make/Makefile

介绍&#xff1a; make和Makefile是用于编译和构建C/C程序的工具和文件。Makefile是一个文本文件&#xff0c;其中包含了编译和构建程序所需的规则和指令。它告诉make工具如何根据源代码文件生成可执行文件&#xff0c;里面保存的是依赖关系和依赖方法。make是一个命令行工具&a…

paddleocr文字识别变迁

数据挖掘 v3 UIM&#xff1a;无标注数据挖掘方案 UIM&#xff08;Unlabeled Images Mining&#xff09;是一种非常简单的无标注数据挖掘方案。核心思想是利用高精度的文本识别大模型对无标注数据进行预测&#xff0c;获取伪标签&#xff0c;并且选择预测置信度高的样本作为训…

如何提升软文推广效果?这三招大部分人都不知道

内容为王的时代下不少企业都把软文推广作为宣传的主要手段&#xff0c;但不是每一次软文推广的效果都会如意。今天媒介盒子就来和大家分享提升软文推广效果的小诀窍&#xff0c;让企业宣传事半功倍。 一、以质取胜 虽然软文营销是一个长期积累的过程&#xff0c;但不代表数量决…

鸿蒙4.0开发笔记之ArkTS语法基础之数据传递与共享详细讲解(十八)

文章目录 一、路由数据传递&#xff08;router&#xff09;1、路由数据传递定义2、路由数据传递使用方法3、数据传递两个页面的效果 二、页面间数据共享&#xff08;EntryAbility&#xff09;1、定义2、实现案例3、避坑点 三、数据传递练习 一、路由数据传递&#xff08;router…

梳理一下嵌入式和单片机之间的关系

一定有很多人都听说过嵌入式和单片机&#xff0c;但在刚开始接触时&#xff0c;不知道大家有没有听说过嵌入式就是单片机这样的说法&#xff0c;其实嵌入式和单片机还是有区别的。单片机与嵌入式到底有什么关系&#xff1f; 下面我们就来说说嵌入式和单片机之间的联系和区别吧…

信息可视化在数字孪生中的应用:打造直观决策支持系统

在当今的数字化时代&#xff0c;数字孪生和信息可视化已成为推动各行业发展的重要力量。数字孪生为物理世界提供了一个虚拟的副本&#xff0c;而信息可视化则将复杂的数据以易于理解的方式呈现出来。两者之间的关系密切&#xff0c;相辅相成&#xff0c;为决策者提供了更全面、…

如何解决el-table中动态添加固定列时出现的行错位

问题描述 在使用el-table组件时&#xff0c;我们有时需要根据用户的操作动态地添加或删除一些固定列&#xff0c;例如操作列或选择列。但是&#xff0c;当我们使用v-if指令来控制固定列的显示或隐藏时&#xff0c;可能会出现表格的行错位的问题&#xff0c;即固定列和非固定列…

定时器TIM HAL库+cubeMX(上)

定时器时钟源APB1 36MHz 一.基本定时器 1.基本框图 2.溢出时间计算 3.配置定时器步骤 TIM_HandleTypeDef g_timx_handle;/* 定时器中断初始化函数 */ void btim_timx_int_init(uint16_t arr, uint16_t psc) {g_timx_handle.Instance TIM6;g_timx_handle.Init.Prescaler p…

3D Web轻量引擎HOOPS Communicator如何实现对大模型的渲染支持?

除了读取轻松外&#xff0c;HOOPS Communicator对超大模型的支持效果也非常好&#xff0c;它可以支持30GB的包含70万个零件和3.5亿个三角面的Catia装配模型&#xff01; 那么它是如何来实现对大模型的支持呢&#xff1f; 我们将从以下几个方面与大家分享&#xff1a;最低帧率…