C++String类的手撕实现

目录

构造函数

提前准备工作:

有参构造

析构函数

c_str

无参构造:

无参和有参的结合

 operater[]的实现

简易版的迭代器

begin

end

 原因:

reserve

思想步骤

获取_capacity 和 _size

测试 

push_back 

思想步骤

append

insert字符

思想步骤:

测验

循环条件的问题

insert字符串

npos

erase

判断删除数据的长度

全删

部分删

最终代码为

 swap

find字符

测试

find字符串

代码

测验

​编辑

substr

代码

测试 

问题提出

问题分析

 问题解答

构建拷贝和赋值

测试 

流插入和提取

流插入

流提取

cin对于空格和换行

get( )

频繁扩容问题

解决办法


构造函数

提前准备工作:

string的成员变量

private:char* _str;size_t _size;size_t _capacity;

需要的头文件

#include<iostream>
#include"string.h"    //它是有个头文件的

有参构造

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

有人可能会走参数列表,单走参数列表的话,在此处是没有特别大的意义,且很容易出错。

这是正确使用参数列表的方法
string(const char* str= ""):_str(new char[_capacity + 1]),_size(strlen(str)),_capacity(_size){strcpy(_str, str);}

 很多会变成出错的原因,就是参数列表没有对应好成员变量的顺序,所以为了避免出现这样的问题就是不用参数列表,直接在函数内定义

析构函数

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

c_str

先用来写出打印出自定义类型的,因为IO流不能输出自定义类型

所以要自己写

const char* c_str(){return _str;
}

无参构造:

string(){_str = nullptr;_size = 0;_capacity = 0;
}

很多朋友可能会写成这个样子,并觉得没有任何问题,那我们就来调用一下

出现了异常,原因就是c_str在返回的时候对空指针进行了解引用的行为

所以报错了

所以要对无参构造怎么搞也要有一个初始值

string():_str(new char[1]),_size(0),_capacity(0)
{_str[0] = '\0';
}

无参和有参的结合

那么这么麻烦,其实可以将有参构造和无参构造合二为一的,在缺省参数上赋予一个空字符串

string(const char* str = ""){_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];strcpy(_str, str);此处的顺序结构也不能乱,当时我就是把_str放到最上面,导致其实_capacity是随机值的时候
就把空间内容赋给_str,最终报错//_str = new char[_capacity + 1];//_size = strlen(str);//_capacity = _size;
}

拷贝构造我们往下点再说 !


 operater[]的实现

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

assert的条件里无需再写 i>=0了,因为参数类型是size_t,所以能很好地避免出现负数的情况


简易版的迭代器

begin

typedef char* iterator;iterator begin(){return _str;
}

end

上面已经typedef,这里就不再写了
iterator end(){return _str + _size;
}
		const_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;}

 

 原因:

因为string是物理线性结构,这个字符的地址的下个地址,就是下一个字符的地址,这是它的底层结构的特性。如果换成物理上不是连续的地址的时候,我们就不能够这样去实现这种迭代器了

比如,链表,树结构


reserve

思想步骤

  1. 如果要新空间的大小大于原空间的大小的话,就要扩容
  2. 创建一个临时对象,来接受新增加的空间
  3. 将原来空间里的数据,拷贝到新空间里面去
  4. 将旧空间释放掉
  5. 将新空间的指针变量交给原来的指针变量
  6. 更新_capacity

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

获取_capacity 和 _size

size_t getCapacity() {return _capacity;
}size_t getSize() {return _size;
}

测试 

void test1() {string s1("hello reverse");cout << "我原来的大小是:" << s1.getSize() << endl;cout << "我原来的空间是:" << s1.getCapacity() << endl << endl;s1.reserve(100);cout << "我现在的大小是:" << s1.getSize() << endl;cout << "我现在的空间是:" << s1.getCapacity() << endl;
}


push_back 

插入数据最核心的点就是扩容, 在push_back之前先完成reserve

开空间时要多开一个,留给\0,因为capacity指的是有效空间,真实的空间是会比他多一个的

思想步骤

  1. 判断空间是否已满

  2. 若已满,则进行扩容,用reserve即可

  3. 将要插入的ch放进对象里的最后一位

  4. ++size

  5. 将原本被ch覆盖掉的'\0'补回去 

void push_back(char ch) {if (_size == _capacity) {size_t newcapacity = _capacity == 0 ? 4 : (_capacity * 2);reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\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;
}

strcpy是能够将'\0'也拷贝进去的


insert字符

思想步骤:

  1. 判断空间是否已满
  2. 给予一个变量,用来记录开始移动的数据
  3. 给予判断条件,开始移动
  4. 放入新数据
  5. ++_size

测验

void test3() {string s1("hello insert");cout << s1.c_str() << endl << endl;s1.insert(0, 'x');cout << s1.c_str() << endl << endl;
}

我们现在来看看第三步,给予判断条件

size_t end = _size;
while (end >= pos){_str[end+1] = _str[end];--end;
}
_str[pos] = ch;

如果是这样的话,能不能成功 ?

肯定不行啦,都这样问了

循环条件的问题

当在0位置插入的时候就挂了

因为end是size_t定义的,当pos为0时,不断--end,当end被减到-1时,就变成了无穷大,所以又变成了>0,陷入了一个死循环

改int也不行

因为当两边的操作符类型不一样的时候,它们会发生类型提升,有符号会向无符号转,所以在比较的时候,会转成无符号进行比较,所以结果还是一样的

所以正确的做法是:避免pos变为-1;

while (end > pos) {_str[end] = _str[end-1];--end;
}

变成这样之后,就要考虑一个重点问题了,'\0'怎么办? 

这里有两种方法

  1. end 从_size 变成 _size + 1,让end - 1 所代表的 '\0' 有去处
  2.  后面给它加一个,_str [_size] = '\0' ;

最终代码为:

void insert(size_t pos, char ch) {assert(pos <= _size);if (_size == _capacity) {size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}size_t end = _size + 1;while (end > pos) {_str[end] = _str[end-1];--end;}_str[pos] = ch;++_size;
}

insert字符串

思想步骤跟insert字符差不多

void 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) {_str[end] = _str[end-len];--end;}strncpy(_str + pos, str, len);_size += len;
}

strncpy:避免\0拷贝进去


npos

const static size_t npos = -1 ; 它在私有成员变量里也能够这么定义的,这算是一个特殊处理了吧,既算声明,也算定义

以前我们在学习类的静态成员变量的时候

类的静态成员变量定义和初始化要分离,它不走初始化列表,需要在类外给定义

原因很简单:静态成员变量只能初始化一次,如果你在类里面给缺省值,就相当于我每创建一个对象,就要对这个静态成员变量初始化一次,显然有违背于静态成员变量规则

现在可将它看作是一种特例

没有任何报错

它并不是说加了const就可以!!!

只有整数类型才能接收


erase

清除数据,缩短长度

判断删除数据的长度

再从某个位置清空数据的时候,要么全删,要么删部分长度

全删

当要删的长度刚好到从开始位置到结束位置

当要删的长度是无穷大,即-1

所以判断条件可为

if(len == npos || pos + len >= _size)

部分删

从某个位置开始删除一般长度的字符串,用strcpy即可

最终代码为

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

 swap

直接调用库里面的swap换一下就可以了

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

find字符

既然是找,给个循环就可以了

size_t find(char ch, size_t pos = npos) {for (size_t i = 0; i < _size; i++ ) {if (_str[i] == ch)return i;}return npos;
}

测试


find字符串

用strstr()函数

它会返回第一次出现在str1里面的str2的指针,没找到就返回空

代码

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

测验


substr

截取一段字符串,肯定是要返回的

代码

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

测试 

问题提出

可能很多人会认为到这里已经没有任何问题了

那我换种样子呢?

	void test6() {string s("hello substr");string s2;s2 = s.substr(6, 6);cout << s2.c_str() << endl;}

 换动是将string s2 = s.substr(6, 6);   

换成 string s2 ;         s2 = s.substr(6, 6); 

结果就是崩了 !!!

何罪至此?

问题分析

是否还记得这张图,对的,就是我们没有拷贝构造这种东西。

之前没有问题,全是编译器优化的功劳,当在同一段表达式的时候,编译器会直接优化很多,导致我们肉眼看不出来任何区别,

当我们没有写拷贝构造的时候,类里面的默认拷贝构造就只是一个简简单单的浅拷贝(值拷贝),对于我们new出来的空间,只是拷贝一个临时对象,但指向的内容还是一样的

深拷贝的解决方案就是会开一个一模一样的空间,你指你的,我指我的,不会互相打扰的,你被挂掉就挂掉吧,反正临时对象不会挂掉 

 

 问题解答

构建拷贝和赋值

string(const string& s) {_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;
}string& operator=(const string& 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;
}

测试 

此刻便是平安无事


流插入和提取

因为在此处我无需再次使用类里面的私有成员变量,所以在这里无需使用友元函数声明

流插入

循环遍历即可

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

流提取

istream& operator>>(istream& in, string& s) {char ch;in >> ch;while (ch != ' ' && ch != '\n') {s += ch;in >> ch;}return in;
}

我们先看看这样子行不行

 发现即使我们按下回车,也依旧可以继续输入

 这就需要我们知道关于cin的一个性质

cin对于空格和换行

 在我们使用流插入的时候,或者说平常使用C的scanf的时候,它会自动省略空格和换行

		int a, b;scanf("%d%d", &a, &b);printf("%d %d", a, b);
		int a, b;cin >> a >> b;cout << a<<" " << b;

 都是下面结果

  

所以我们就可以用c++里的另一个函数,专门取出字符的函数

get( )

 

get函数,可以专门提取出字符

	istream& operator>>(istream& in, string& s) {char ch;ch = in.get();while (ch != ' ' && ch != '\n') {s += ch;ch = in.get();}return in;}

换成这样,我们就能正常的读取到空格和换行

 

 

频繁扩容问题

再谈论另外一个问题,我们使用的是s+=ch; 也就是说,我们每次新增一次字符,我们就要扩容一次,因为我们要调用reserve函数,我们可以知道频繁扩容给我们带来的代价是比较大的

性能开销:容器在扩容时需要重新分配内存并复制现有元素到新的内存区域。这个过程是昂贵的,因为它涉及到内存分配和数据复制

内存分配:频繁的内存分配和释放可能导致内存碎片,影响程序的内存使用效率

时间复杂度:如果一个容器在添加元素时需要频繁扩容,那么其时间复杂度可能从理论上的O(1)增加到O(n),因为每次扩容都需要复制所有现有元素

异常安全性:在C++中,如果内存分配失败,会抛出异常。频繁的内存分配增加了抛出异常的可能性,这可能需要额外的异常处理逻辑。

资源竞争:在多线程环境中,频繁的内存分配和释放可能导致资源竞争,从而影响程序的并发性能。

解决办法

所以,为了解决这个问题,我们可以像缓冲区一样,构建一个缓冲

即,先划分一个相对比较合理的空间,让内容先填进去,等到满了,或者触发了某个特定条件,比如空格跟换行,再把缓冲内容倒进容器里。这样就可以避免频繁扩容

所以比之前较为优化的写法为

	istream& operator>>(istream& in, string& s) {s.clear();char buffer[128];char ch = in.get();int i = 0;while (ch != ' ' && ch != '\n') {buffer[i] = ch;i++;if (i == 127) {	//不能到128,因为0-127就是128位了buffer[i] = '\0';s += buffer;i = 0;}ch = in.get();}if (i > 0) {buffer[i] = '\0';s += buffer;}return in;}

以上就是本次博客的学习内容,如有错误,还望各位大佬指点,谢谢阅读

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

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

相关文章

平安养老险广西分公司开展7.8公益健步行活动

近日&#xff0c;平安养老保险股份有限公司&#xff08;以下简称“平安养老险”&#xff09;广西分公司在南宁邕江沿岸开展“7.8”公益健步行活动&#xff0c;在分公司班子的号召下&#xff0c;各部门内外勤员工均踊跃参与。 员工们沿途随手捡拾垃圾&#xff0c;传递积极、绿色…

就在刚刚,中国 IMO 奥数遗憾地失去了第一名的宝座,连续五年的统治地位被美国队所终结。

&#x1f431; 个人主页&#xff1a;TechCodeAI启航&#xff0c;公众号&#xff1a;TechCodeAI &#x1f64b;‍♂️ 作者简介&#xff1a;2020参加工作&#xff0c;专注于前端各领域技术&#xff0c;共同学习共同进步&#xff0c;一起加油呀&#xff01; &#x1f4ab; 优质专…

Vue3开源Tree组件研发:节点勾选支持v-model

自研Tree组件有两个原因&#xff1a;1. 目前开源UI对Tree组件的用户API不太友好&#xff0c;2. 提升Vue3组件自研能力。 目前已实现的功能见上面思维导图。想象Tree组件的一个使用场景&#xff1a;后台管理员通过Tree组件来完成用户角色授权&#xff0c;同时支持对权限进行新增…

Spring中使用到的设计模式及其源码分析

前言 众所周知&#xff0c;Spring框架是一个强大而灵活的开发框架。这不&#xff0c;上次的面试刚问到这些&#xff0c;没防住&#xff01;&#xff01;&#xff01;因此下来总结一下。这篇文章主要介绍Spring中使用到的设计模式&#xff0c;自己做个面试复盘&#xff0c;同时…

Spring Security 原理

Spring Security是一个功能强大且广泛使用的身份验证和授权框架&#xff0c;专为保护Java应用程序的安全性而设计。它提供了一套可配置的安全性规则和机制&#xff0c;用于对应用程序的资源进行访问控制和保护。以下是Spring Security的主要原理&#xff1a; 1. 过滤器链&…

引领安全新风尚:WT2605/WT2003H芯片方案赋能电动汽车,打造智能低速安全报警器"

电动汽车低速报警器(AVAS)方案可采用WT2605或WT2003H方案芯片,可实现当车辆时速低于设定值(如20Km/h),报警器会发出类似发动机加速减速的声音,倒档时装置也会发出倒车警示,在嘈杂的城市环境中帮助提升行人对驶近的电动汽车的感知,降低行人、骑行者和弱势群体面临的风险。 WT26…

十分钟带你入门Go语言(Golang)开发

概述 Go语言是由 Google 的 Robert Griesemer,Rob Pike 及 Ken Thompson 开发的一种静态强类型、编译型语言。Go语言的设计目标是将静态语言的安全性和性能与动态语言的易用性相结合。Go语言在语言层面提供了对协程的支持&#xff0c;特别适合编写高并发的项目。随着使用Go语言…

基于opencv的答题卡识别

文章目录 一、背景需求二、处理步骤图片预处理检测到答题卡轮廓透视变换找每个圆圈的轮廓轮廓排序判断是否答题正确 一、背景需求 传统的手动评分方法耗时且容易出错&#xff0c;自动化评分可以可以显著提高评分过程的速度和准确性、减少人工成本。 答题卡图片处理效果如下&am…

想出国?去外企?建议网工无脑冲思科认证。

近年来&#xff0c;国内职场竞争愈发激烈&#xff0c;内卷现象严重&#xff0c;大部分人都面临着巨大的就业压力&#xff0c;或是找工作无门、或是中年危机悄然来临&#xff0c;时刻担心被职场优化。 在这样的背景下&#xff0c;出国或进入外企工作&#xff0c;成为了许多人寻…

[JavaScript] 动态获取方法参数名

JavaScript&#xff08;简称“JS”&#xff09;是一种具有函数优先的轻量级&#xff0c;解释型或即时编译型的编程语言。虽然它是作为开发Web页面的脚本语言而出名&#xff0c;但是它也被用到了很多非浏览器环境中&#xff0c;JavaScript基于原型编程、多范式的动态脚本语言&am…

Optional类的使用 java8(附代码)

&#x1f370; 个人主页:_小白不加班__ &#x1f35e;文章有不合理的地方请各位大佬指正。 &#x1f349;文章不定期持续更新&#xff0c;如果我的文章对你有帮助➡️ 关注&#x1f64f;&#x1f3fb; 点赞&#x1f44d; 收藏⭐️ 文章目录 一、什么是Optional&#xff1f;二、…

科研绘图系列:R语言和弦图 (Chord diagram)

介绍 和弦图(Chord Diagram)是一种用于展示多个实体之间相互关系的数据可视化方法。它通常用于表示网络或系统中不同节点(实体)之间的连接强度或流量。和弦图由一个圆形布局组成,每个节点在圆周上占据一个扇形区域,节点之间的连接通过圆内的线条(和弦)来表示。 特点:…

数据结构第七讲:栈和队列OJ题

数据结构第七讲&#xff1a;栈和队列OJ题 1.有效的括号2.用队列实现栈3.用栈实现队列4.设计循环队列 1.有效的括号 链接: OJ题目链接 typedef char StackDataType;typedef struct Stack {StackDataType* arr;//使用一个指针来指向开辟的数组int capacity;//保存数组的空间大…

springboot爱宠屋宠物商店管理系统-计算机毕业设计源码52726

目录 摘要 1 绪论 1.1 选题背景与意义 1.2国内外研究现状 1.3论文结构与章节安排 2系统分析 2.1 可行性分析 2.2 系统流程分析 2.2.1系统开发流程 2.2.2 用户登录流程 2.2.3 系统操作流程 2.2.4 添加信息流程 2.2.5 修改信息流程 2.2.6 删除信息流程 2.3 系统功能…

【机器学习】正规方程的简单介绍以及如何使用Scikit-Learn实现基于正规方程的闭式解线性回归

引言 Scikit-learn 是一个开源的机器学习库&#xff0c;它支持 Python 编程语言。它提供了多种机器学习算法的实现&#xff0c;并用于数据挖掘和数据分析 文章目录 引言一、正规方程的定义二、正规方程的原理三、使用 Scikit-Learn 实现基于正规方程的闭式解线性回归3.1 工具3.…

实验15.多线程调度

简介 实验.多线程调度 内核线程 1.在时钟中断函数中处理中&#xff0c;减少当前线程pcb的tick&#xff0c;tick为0则启动调度2.调度&#xff0c;把当前线程pcb放入就绪对立队尾&#xff0c;把就绪线程队首拿出来执行主要代码 引导 省略内核 list.h #ifndef __LIB_KERNEL_…

【2024最新】 服务器安装Ubuntu20.04 (安装教程、常用命令、故障排查)持续更新中.....

安装教程&#xff08;系统、NVIDIA驱动、CUDA、CUDNN、Pytorch、Timeshift、ToDesk、花生壳&#xff09; 制作U盘启动盘&#xff0c;并安装系统 在MSDN i tell you下载Ubuntu20.04 Desktop 版本&#xff0c;并使用Rufus制作UEFI启动盘&#xff0c;参考UEFI安装Ubuntu使用GPTU…

mysql 的MHA

mysql 的MHA 什么是MHA 高可用模式下的故障切换&#xff0c;基于主从复制。 单点故障和主从复制不能切换的问题。 至少需要3台。 故障切换过程0-30秒。 vip地址&#xff0c;根据vip地址所在的主机&#xff0c;确定主备。 主 vip 备 vip 主和备不是优先确定的&#xff…

InternLM Linux 基础知识

完成SSH连接与端口映射并运行hello_world.py 创建并运行test.sh文件 使用 VSCODE 远程连接开发机并创建一个conda环境

“pandas”的坑

参考&#xff1a;百度安全验证 本文基于python第三方数据分析库pandas&#xff0c;分享这几天所遇到的3个爬坑的案例&#xff0c;希望对也在爬坑的同学们尽一份绵薄之力&#xff0c;如有错误或者写得不好的地方&#xff0c;烦请指正&#xff0c;谢谢。 01df中startswith的坑 …