【STL】string的模拟实现

目录

前言

构造函数

析构函数

迭代器

拷贝构造和赋值

深浅拷贝问题

传统写法

现代写法

 插入函数

reserve() 

push_back()

append()

+=操作 

insert()

erase() 

流插入和流提取 

 流插入

流提取

运算符重载

其它函数实现

 []重载

resize() 

find()  

 substr() 

完整代码展示


前言

本文通过对STL中的string进行模拟实现来了解string的底层实现,我们模拟实现的目的并不是为了写出一个更好的string,而是为了学习这底层的基本实现,实际上库里面实现的细节比这复杂,还得是大佬。

关于string的基本用法可以参考这个网站cplusplus.com/reference/string/string/

构造函数

 

在实现构造函数时,我们可以实现一个无参的构造和一个有参的构造,但是对于无参的构造我们可以省略,而直接在有参中使用缺省值代替无参的情况。由于在对无参的对象进行初始化时,还要考虑加上'\0',所以我们的缺省值用一个空串代替即可。在使用初始化列表时,考虑到初始化列表的初始化顺序需要和变量声明的顺序一致,在我们后序想要加一些变量时不太方便,所以这里就没有使用初始化列表。

//构造函数
MyString(const char* str = "")
{_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];strcpy(_str, str);
}

当使用strlen函数给成员变量初始化时,我们只需要给一个成员变量使用strlen,其余的使用其赋值即可,因为每次使用strlen也是O(N)的消耗的。在我们初始化new一块空间时必需还考虑到末尾的'\0',也就是上述代码中_capacity + 1的原因。还有别忘了初始化完后的最后一步进行拷贝。

析构函数

//析构函数
~MyString()
{delete[] _str;_str = nullptr;_size = _capacity = 0;
}

对new出来的空间需要delete掉,并且置空,防止野指针。

迭代器

 迭代器还是很好实现的,不要被吓到了,它其实是个纸老虎,本质是一个char*的指针

//迭代器
typedef char* iterator;
typedef 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;
}

拷贝构造和赋值

深浅拷贝问题

如果我们不写拷贝构造,编译器会默认帮我们生成一个,但是编译器帮我们生成的是一个浅拷贝,例如在下图中,当用s构造s1时,编译器会调用默认的拷贝构造,最终导致的问题是,s和s1共用一块内存空间,在调用析构函数时,同一块空间会被释放两次而引起程序崩溃。这种就是浅拷贝带来的问题。

浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。

那么要解决这个问题就要用到深拷贝,也就是要开辟一块新空间,让s和s1都有属于自己的一块空间。

 深拷贝:给每个对象独立分配资源,保证多个对象之间不会因为引共享资源而造成多次释放带来的程序崩溃问题。

传统写法

    //拷贝构造——传统写法MyString(const MyString& s):_str(new char[s._capacity + 1]),_size(s._size),_capacity(s._capacity){strcpy(_str, s._str);}//赋值 s = s2——传统写法MyString& operator=(const MyString& s){if (this != &s){char* tmp = new char[s._capacity + 1];strcpy(tmp, s._str);delete[] _str;_str = tmp;_size = s._size;_capacity = s._capacity;}return *this;}

在拷贝构造中我们需要开辟一块空间,之后调用strcpy将内容拷到新开的空间中去即可。在开辟空间时始终记得+1留给'\0'。

在传统写法中实现赋值时要先判断一下自己赋值给自己的情况(不然将空间delete掉之后就会使程序崩溃),并且对于原来的空间需要delete掉之后重新指向新开好的空间。

现代写法

    void swap(MyString& tmp){::swap(_str, tmp._str);::swap(_size, tmp._size);::swap(_capacity, tmp._capacity);}//拷贝构造——现代写法MyString(const MyString& s):_str(nullptr), _size(0), _capacity(0){MyString tmp(s._str);swap(tmp);}//赋值 s = s2——现代写法MyString& operator=(const MyString& s){MyString tmp(s);swap(tmp);return *this;}

传统写法和现代写法效率上差不多,但是对比与传统写法,现代写法显得更简便,定义出来的tmp会调用构造函数来初始化,然后tmp就像打工人一样,将自己辛苦打拼的成果全交给了老板。其实上面的赋值还能在简化,把参数也考虑进去。

MyString& operator=(MyString s)
{swap(s);return *this;
}

插入函数

reserve() 

在插入数据时要考虑到空间够不够的问题,所以先来实现一下reserve(),其实实现起来也很简单,当空间不够了就要重新开一块空间,开好后把原来的内容拷贝到新开好的空间中去,然后再释放掉原来的空间,最后再修改_capacity大小。

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

push_back()

    void push_back(char ch){if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size++] = ch;_str[_size] = '\0';//一定要把'\0'加上}

push_back的实现和链表就很像了,插入时先判断一下空间够不够,不够就要扩容(要考虑一开始没有给初始值的情况),最后不要忘了加上'\0'。 

append()

    void append(const char* str){size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}strcpy(_str + _size, str);_size += len;}void append(const MyString& s){append(s._str);}void append(size_t n, char ch){reserve(_size + n);for (int i = 0; i < n; i++){push_back(ch);}}

 append和push_back类似,但是要注意的时扩容时要把插入的字符串的长度考虑进去,然后在尾部插入字符串,最后再修改一下_size大小。

+=操作 

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

insert()

    MyString& insert(size_t pos, char ch){assert(pos <= _size);if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}//挪动数据size_t end = _size + 1;while (end > pos){_str[end] = _str[end - 1];end--;}_str[pos] = ch;_size++;return *this;}MyString& 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){_str[end] = _str[end - len];end--;}strncpy(_str + pos, str, len);_size += len;return *this;}

这里的两个insert构成重载,先来讲解第一个在任意位置插入一个字符

比如下图中在 l 位置插入一个o,先要判断一下这块空间的大小支不支持我们插入,不支持的话就要扩容。显然下面的空间不支持我们插入,那么就需要我们扩容了。

扩完容后我们要挪动数据,将pos位置空出来,以便让我们插入,当我们挪动数据时要从后往前挪动,防止我们的数据被覆盖掉。如下所示

那么在任意位置插入字符串也是类似的,在插入是要先判断一下空间够不够,这里的判断机制和append一样,要把待插入字符串的长度考虑进去。之后挪动数据时也要将待插入字符串的长度考虑进去(end == _size + len这样才能空出len个位置出来),将位置空出来以便将待插入字符串插入进去。

在这里我为了方便就没把'\0'画出来了

在插入字符串时,我们可以用个strncpy就不用我们手动再去插入了。

最后不要忘了修改size的大小

insert实现完后,上面的push_back和append只要调用,就能实现相应的效果。

void push_back(char ch)
{insert(_size, ch);
}void append(const char* str)
{insert(_size, str);
}

erase() 

在库里面的erase还有个npos,其实npos的本质就是个-1,我们在自己实现的string类中加上即可。注意静态变量加上了const之后就可以在类中初始化啦,这是C++给做的特殊处理。

 

void 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);_size -= len;}
}

在erase中我们要注意,如果len的大小超过了[pos,end]这个区间的大小或者pos==npos,我们直接在pos位置加上'\0'就行了,否则我们调用strcpy将数据覆盖掉就行了,不过要注意拷贝的范围问题。最后修改一下size就行了。

流插入和流提取 

 流插入

ostream& operator<<(ostream& out, const MyString& s)
{for (size_t i = 0; i < s.size(); i++){out << s[i];}return out;
}

流插入中其实还是调用了库里面的内置类型,ostream里面就实现了,将字符一个一个插入进ostream,这个字符串有多长就会输出多长。 

流提取

在流提取中,对于已经有数据的应该先进行一个清理的操作,那么很容易就会写出下面的代码 

	void clear(){_str[0] = '\0';_size = 0;}
	istream& operator>>(istream & in, MyString & s){char ch;in >> ch;while (ch != ' ' && ch != '\n'){s += ch;in >> ch;}return in;}

这样写就会导致一个问题,这个循环不会停下来,因为它会把空格和换行当成是分隔符,这样就陷入了死循环 

 

解决方法:用库里面的in.get()函数来对字符进行提取就能解决这个问题

效率问题:一个一个的字符插入,空间不够了就要扩容,这样就要扩容好多次,非常影响效率。

改进:我们可以用个类似于缓冲区的方式,先将提取到的字符弄进缓冲区,如果这个缓冲区满了,再一次性将缓冲区中的数据输入到instream中,这样就能有效解决了扩容带来的效率问题

istream& operator>>(istream& in, MyString& s)
{s.clear();char ch;ch = in.get();const size_t N = 64;char buff[N];size_t i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == N - 1){buff[i] = '\0';s += buff;i = 0;}ch = in.get();}buff[i] = '\0';s += buff;return in;
}

运算符重载

这里只要实现了两个就行,让其它运算符复用即可。偷个懒,使用strcmp函数就能完成如下操作。

	bool operator > (const mystring& s)const{return strcmp(_str, s._str) > 0;}bool operator == (const mystring& s)const{return strcmp(_str, s._str) == 0;}bool operator >= (const mystring& s)const{return *this > s || *this == s;}bool operator <= (const mystring& s)const{return !(*this > s);}bool operator < (const mystring& s)const{return !(*this >= s);}bool operator != (const mystring& s)const{return !(*this == s);}

其它函数实现

 []重载

重载了[]就可以支持下标访问了

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

resize() 

主要是实现对开辟的空间进行初始化,要注意的是如果需要开辟的容量小于原来的,那么就需要删除数据,删除数据的操作也很简单,直接在末尾加'\0' ,如果需要开辟的容量大于原来的,那么就需要插入数据给这块新空间中未被用到的做初始化

	void resize(size_t n, char ch = '\0'){if (n > _size){//插入数据reserve(n);for (size_t i = _size; i < n; i++){_str[i] = ch;}_str[n] = '\0';}else{//删除数据_str[n] = '\0';_size = n;}}

find()  

	size_t find(char ch, size_t pos = 0)const{assert(pos < _size);for (size_t i = pos; i < _size; i++){if (ch == _str[i]){return i;}}return npos;}size_t find(const char* sub, size_t pos = 0)const{assert(sub);assert(pos < _size);const char* ptr = strstr(_str + pos, sub);if (ptr == nullptr){return npos;}else{return ptr - _str;//通过指针-指针的方式来获取下标}}

 substr() 

substr用来提取子串,在实现的过程中,需要对提取的真实长度进行判断,如果len太大直接将后面的所有字符串提取完。 

	MyString substr(size_t pos, size_t len = npos)const{assert(pos < _size);size_t realLen = len;if (len == npos || len + pos >= _size){realLen = _size - pos;}MyString str;for (size_t i = 0; i < realLen; i++){str += _str[i + pos];}return str;}

库里面还有很多函数如果有兴趣可以自己去动手实现一下哦 

完整代码展示

string的模拟实现


今天的分享就到这里,如果有写的不好或者不对的地方,还望指出,谢谢!!! 

 

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Matlab|计及电池储能寿命损耗的微电网经济调度

目录 1 主要内容 储能寿命模型 负荷需求响应 2 部分代码 3 程序结果 4 下载链接 1 主要内容 该程序参考文献《考虑寿命损耗的微网电池储能容量优化配置》模型&#xff0c;以购售电成本、燃料成本和储能寿命损耗成本三者之和为目标函数&#xff0c;创新考虑储能寿命损耗约…

基于 YAML 接口自动化测试框架设计

在设计自动化测试框架的时候&#xff0c;我们会经常将测试数据保存在外部的文件&#xff08;如Excel、YAML、CSV&#xff09;&#xff0c;或者数据库中&#xff0c;实现脚本与数据解耦&#xff0c;方便后期维护。目前非常多的自动化测试框架采用通过Excel或者YAML文件直接编写测…

python 中判断文件、目录是否存在的方法

判断目录是否存在并创建目录 一、实现上传文件功能二、判断目录是否存在的办法2.1、使用os模块2.1.1、判断目录是否存在2.1.2、os.makedirs()&#xff1a;递归创建目录 2.2、使用pathlib模块2.2.1、path.exist()判断目录是否存在2.2.1、path.mkdir()&#xff1a;创建目录 2.3、…

力扣刷题Days25-45. 跳跃游戏 II(js)

目录 1&#xff0c;题目 2&#xff0c;代码 贪心算法正向查找 3&#xff0c;学习 解题思路 具体代码处理 数组遍历的最后边界的处理&#xff1a; 1&#xff0c;题目 给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向…

程序结构

oracle从入门到总裁:​​​​​​https://blog.csdn.net/weixin_67859959/article/details/135209645 程序结构 PL/SQL程序的基本单元是语句块&#xff0c;所有的 PL/SQL程序都是由语句块组成&#xff0c;语句块之间可以相互嵌套&#xff0c;每个语句块完成特定的功能。 一个…

html页面使用@for(){},@if(){},利用jquery 获取当前class在列表中的下标

基于以前的项目进行修改优化&#xff0c;前端代码根据List元素在html里进行遍历显示 原先的代码&#xff1a; 其中&#xff0c;noticeGuide.Id是标识noticeGuide的唯一值&#xff0c;但是不是从0开始的【是数据库自增字段】 但是在页面初始化加载的时候&#xff0c;我们只想…

NO9 蓝桥杯单片机实践之串口通信的使用

1 回顾 串口通信的代码编写结构还是与中断一样&#xff0c;不同的是&#xff1a; 初始中断函数条件涉及到串口通信相关的寄存器和定时器1相关的寄存器&#xff08;定时器1用于产生波特率&#xff09;&#xff0c;但初始条件中的中断寄存器只考虑串口通信而不考虑定时器1。 vo…

基于SSM的高校推免报名(有报告)。Javaee项目。ssm项目。

演示视频&#xff1a; 基于SSM的高校推免报名&#xff08;有报告&#xff09;。Javaee项目。ssm项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c;通过Spring Spri…

图解 python 的赋值,浅拷贝,深拷贝

上面的图中&#xff0c;我们将箭头连线看作是引用。 如果你只是简单是 b a&#xff0c;实际上两者的引用是一样的&#xff0c;相当于 b 只是 a 的另外一个名字&#xff0c;不管是对 a 或者 b 内的可变元素还是不可变元素修改&#xff0c;打印 a, b 两者都是一样的。 但是如果…

PHP+MySQL开发组合:智慧同城便民信息小程序源码系统 带完整的安装代码包以及安装部署教程

当前&#xff0c;城市生活的节奏日益加快&#xff0c;人们对各类便民信息的需求也愈发迫切。无论是寻找家政服务、二手交易&#xff0c;还是发布租房、求职信息&#xff0c;一个高效、便捷的信息平台显得尤为重要。传统的信息发布方式往往存在信息更新不及时、查找困难等问题&a…

Jenkins安装 Linux 更换镜像 安装插件

Jenkins安装 Linux 更换镜像 安装插件 前言 下面叙述了三种jenkins安装的方式,jenkins安装之前必须有java环境因为他是java写的… yum安装只能安装最新版本的jenkins,但是jenkins是java写的所以他强依赖java版本,当你的服务器的java版本与jenkins版本冲突时还需要给jenkins重…

攻防世界misc_pic_again

是个png文件 你用binwalk &#xff0c;010&#xff0c;和图片分离都没有什么信息 正确做法—————— 在StegSolve里面 点Ana——Exrtact 选上这三个&#xff0c;你就可以发现 这个二进制是个zip &#xff08;具体原理我不知道&#xff0c;只能说这是新题型&#xff0c…

书生浦语大模型实战营第一课笔记

书生浦语大模型全链路开源体系 课程笔记大模型的发展趋势InternLM2的主要亮点模型到应用的典型流程全链路的开源工具 InternLM2技术报告笔记大型语言模型的发展InternEvoModel Structure训练数据 课程笔记 第一节课主要对大模型进行介绍&#xff0c;特别是书生浦语大模型的发展…

前端学习<二>CSS基础——04-CSS选择器:伪类

伪类&#xff08;伪类选择器&#xff09; 伪类&#xff1a;同一个标签&#xff0c;根据其不同的种状态&#xff0c;有不同的样式。这就叫做“伪类”。伪类用冒号来表示。 比如div是属于box类&#xff0c;这一点很明确&#xff0c;就是属于box类。但是a属于什么类&#xff1f;…

Transformer的前世今生 day09(Transformer的框架概述)

前情提要 编码器-解码器结构 如果将一个模型分为两块&#xff1a;编码器和解码器那么编码器-解码器结构为&#xff1a;编码器负责处理输入&#xff0c;解码器负责生成输出流程&#xff1a;我们先将输入送入编码器层&#xff0c;得到一个中间状态state&#xff0c;并送入解码器…

时序预测 | Matlab实现BiTCN-BiLSTM双向时间卷积神经网络结合双向长短期记忆神经网络时间序列预测

时序预测 | Matlab实现BiTCN-BiLSTM双向时间卷积神经网络结合双向长短期记忆神经网络时间序列预测 目录 时序预测 | Matlab实现BiTCN-BiLSTM双向时间卷积神经网络结合双向长短期记忆神经网络时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现BiTCN…

AI智能分析网关V4如何使用GB28181注册到EasyCVR平台?具体步骤是什么?

旭帆科技的智能分析网关V4内含近40种智能分析算法&#xff0c;包括人体、车辆、消防、环境卫生、异常检测等等&#xff0c;在消防安全、生产安全、行为检测等场景应用十分广泛。如常见的智慧工地、智慧校园、智慧景区、智慧城管等等&#xff0c;还支持抓拍、记录、告警、语音对…

rabbitmq集群问题排查

blowcode-test-redis04、blowcode-test-redis05、blowcode-test-redis06 这3个节点搭建的rabbitmq集群&#xff0c;04是主节点。 某次分别观察3个节点的管理页面&#xff0c;先都只能看到自己的节点是正常的绿色状态&#xff0c;猜测节点都各自为政了。 下图是05节点成功加入0…

MySQL 高级语句(二)

一、子查询 1.1 相同表子查询 1.2 不同表/多表子查询 1.3 子查询的应用 1.3.1 语法 1.3.2 insert 子查询 1.3.3 update 子查询 1.3.4 delete 子查询 1.4 exists 关键字 1.4.1 true 1.4.2 false 1.5 as别名 二、视图 2.1 视图和表的区别和联系 2.1.1 区别 2.1.2 …

策略路由-IP-Link-路由协议简介

策略路由 策略路由和路由策略的不同 1.策略路由的操作对象是数据包&#xff0c;在路由表已经产生的情况下&#xff0c;不按照路由表进行转发&#xff0c;而是根据需要&#xff0c;依照某种策略改变数据包的转发路径 2.路由策略的操作对象是路由信息。路由策略的主要实现了路…