【c++进阶(二)】STL之string类的模拟实现

💓博主CSDN主页:Am心若依旧💓

⏩专栏分类c++从入门到精通⏪
🚚代码仓库:青酒余成🚚

🌹关注我🫵带你学习更多c++
  🔝🔝

 

 1.前言

本章重点

本章主要介绍一些关键接口的模拟实现,例如:构造函数,拷贝构造函数,析构函数,赋值重载函数,插入删除函数等等。

 2.默认成员函数

在实现默认成员函数之前,先确定成员变量

namespace wzz
{
    class string
    {
        private:
            char* _str;//用一个字符数组来模拟string
            size_t _size;//数组中实际有效的个数
            size_t _capacity;//数组中容量的大小
            static const size_t npos;//静态成员变量来表示最大值
    };
    const string::size_t npos=-1;//静态成员变量在类内声明,在类外定义
}

1.默认构造函数--当没有传值的时候,就用一个缺省值来代替--这个缺省值用空字符串代替

string (const char* str="")
{
    _str=new char[_capacity+1];
    _size=strlen(str);
    _capacity=_size;//初始时,容量就让其等于有效元素的个数
    strcpy(_str,str);
}

补充迭代器构造

template <class Inputeiterrator>
string(Inputeiterator begin,Inputeriterator end)
{
       while(begin!=end)
        {
            push_back(*begin);
            begin++;        
        }
}

2.拷贝构造函数--用一个char*来初始化另一个char*

在这里会详细介绍深浅拷贝--后续就不在重点介绍了

浅拷贝:拷贝出来的目标对象的指针和源对象的指针指向的内存空间是同一块空间。其中一个对象的改动会对另一个对象造成影响。
深拷贝:深拷贝是指源对象与拷贝对象互相独立。其中任何一个对象的改动不会对另外一个对象造成影响。

很明显,深拷贝比浅拷贝更加安全,不会有同一块空间析构两次的问题。 这里拷贝构造就介绍两种写法

法一:开辟一块空间,然后把数据拷贝过来--如下图

代码:

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

法二:利用swap函数

法二与法一的思想不同:先根据源字符串的C字符串调用构造函数构造一个tmp对象,然后再将tmp对象与拷贝对象的数据交换即可。拷贝对象的_str与源对象的_str指向的也不是同一块空间,是互相独立的。

代码如下:


string(const string& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	string tmp(s.begin(),s.end()); //调用构造函数(这里用的是迭代器构造),构造出一个C字符串为s._str的对象
	swap(tmp); //交换这两个对象
}

swap函数的模拟实现

只需要把每个成员变量进行交换即可

//交换两个对象的数据
void swap(string& s)
{
	//调用库里的swap
	std::swap(_str, s._str); //交换两个对象的C字符串
	std::swap(_size, s._size); //交换两个对象的大小
	std::swap(_capacity, s._capacity); //交换两个对象的容量
}

这里推荐使用方法二来模拟实现构造函数

3.赋值重载函数

这里也涉及到深浅拷贝的问题--因此提供两种深拷贝的写法

法一:把原来的空间直接释放,然后开辟新空间,然后再赋值

//法一
string& operator=(const string& s)
{
	if (this != &s) //防止自己给自己赋值
	{
		delete[] _str; //将原来_str指向的空间释放
		_str = new char[s._capacity + 1]; //重新申请一块空间
		strcpy(_str, s._str);    //将s._str拷贝一份到_str
		_size = s._size;         //_size赋值
		_capacity = s._capacity; //_capacity赋值
	}
	return *this; //返回左值(支持连续赋值)
}

法二:用交换函数

//方法二
string& operator=(const string& s)
{
	if (this != &s) //防止自己给自己赋值
	{
		string tmp(s); //用s拷贝构造出对象tmp
		swap(tmp); //交换这两个对象
	}
	return *this; //返回左值(支持连续赋值)
}

注意:

用传值返回还是传引用返回,要看你返回的这个值除了作用域还在不在,如果在的话就可以使用传值返回,如果不在的话就一定要使用传引用返回,否则就会报错。

4.析构函数

string类的析构函数需要我们进行编写,因为每个string对象中的成员_str都指向堆区的一块空间,当对象销毁时堆区对应的空间并不会自动销毁,为了避免内存泄漏,我们需要使用delete手动释放堆区的空间。

//析构函数
~string()
{
	delete[] _str;  //释放_str指向的空间
	_str = nullptr; //及时置空,防止非法访问
	_size = 0;      //大小置0
	_capacity = 0;  //容量置0
}

 3.迭代器相关函数

 string类中的迭代器实际上就是字符指针,只是给字符指针起了一个别名叫iterator而已。

typedef char* iterator;
typedef const char* const_iterator;
 

 注意:不是所有的迭代器都是指针,比如在list中迭代器就是节点

begin函数---返回字符的第一个地址

iterator begin()
{
	return _str; //返回字符串中第一个字符的地址
}
const_iterator begin()const
{
	return _str; //返回字符串中第一个字符的const地址
}

end函数---返回字符的最后一个地址(即\0的地址)

iterator end()
{
	return _str + _size; //返回字符串中最后一个字符的后一个字符的地址
}
const_iterator end()const
{
	return _str + _size; //返回字符串中最后一个字符的后一个字符的const地址
}

4.容量大小相关的函数

size和capacity

因为string类的成员变量是私有的,我们并不能直接对其进行访问,所以string类设置了size和capacity这两个成员函数,用于获取string对象的大小和容量
size函数用于获取字符串当前的有效长度(不包括’\0’)。

//大小
size_t size()const
{
	return _size; //返回字符串当前的有效长度
}

capacity用于获取当前的容量

//容量
size_t capacity()const
{
	return _capacity; //返回字符串当前的容量
}

reverse和resize

reverse--只有当大于当前容量时才会进行扩容,其他不关心

//改变容量,大小不变
void reserve(size_t n)
{
	if (n > _capacity) //当n大于对象当前容量时才需执行操作
	{
		char* tmp = new char[n + 1]; //多开一个空间用于存放'\0'
		strncpy(tmp, _str, _size + 1); //将对象原本的C字符串拷贝过来(包括'\0')
		delete[] _str; //释放对象原本的空间
		_str = tmp; //将新开辟的空间交给_str
		_capacity = n; //容量跟着改变
	}
}

注意:代码中使用strncpy进行拷贝对象C字符串而不是strcpy,是为了防止对象的C字符串中含有有效字符’\0’而无法拷贝(strcpy拷贝到第一个’\0’就结束拷贝了)。

reseize

 1、当n大于当前的size时,将size扩大到n,扩大的字符为ch,若ch未给出,则默认为’\0’。
 2、当n小于当前的size时,将size缩小到n。

//改变大小
void resize(size_t n, char ch = '\0')
{
	if (n <= _size) //n小于当前size
	{
		_size = n; //将size调整为n
		_str[_size] = '\0'; //在size个字符后放上'\0'
	}
	else //n大于当前的size
	{
		if (n > _capacity) //判断是否需要扩容
		{
			reserve(n); //扩容
		}
		for (size_t i = _size; i < n; i++) //将size扩大到n,扩大的字符为ch
		{
			_str[i] = ch;
		}
		_size = n; //size更新
		_str[_size] = '\0'; //字符串后面放上'\0',这里如果不放\0后续就会产生错误
	}
}

empty--判断是否还有元素

//判空
bool empty()
{
	return _size== 0;
}

5.与插入删除有关的函数

insert函数的作用是在字符串的任意位置插入字符或是字符串。
insert函数用于插入字符时,首先需要判断pos的合法性,若不合法则无法进行操作,紧接着还需判断当前对象能否容纳插入字符后的字符串,若不能则还需调用reserve函数进行扩容。插入字符的过程也是比较简单的,先将pos位置及其后面的字符统一向后挪动一位,给待插入的字符留出位置,然后将字符插入字符串即可。

//在pos位置插入字符
string& insert(size_t pos, char ch)
{
	assert(pos <= _size); //检测下标的合法性
	if (_size == _capacity) //判断是否需要增容
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2); //将容量扩大为原来的两倍
	}
	char* end = _str + _size;
	//将pos位置及其之后的字符向后挪动一位
	while (end >= _str + pos)
	{
		*(end + 1) = *(end);
		end--;
	}
	_str[pos] = ch; //pos位置放上指定字符
	_size++; //size更新
	return *this;
}

如果要用于插入字符串也是和上述插入一个字符同理

//在pos位置插入字符串
string& insert(size_t pos, const char* str)
{
	assert(pos <= _size); //检测下标的合法性
	size_t len = strlen(str); //计算需要插入的字符串的长度(不含'\0')
	if (len + _size > _capacity) //判断是否需要增容
	{
		reserve(len + _size); //增容
	}
	char* end = _str + _size;
	//将pos位置及其之后的字符向后挪动len位
	while (end >= _str + pos)
	{
		*(end + len) = *(end);
		end--;
	}
	memcpy(_str + pos, str, len); //pos位置开始放上指定字符串
	_size += len; //size更新
	return *this;
}

push_back和push_front

void push_back(char ch)
{
    insert(_size(),ch);
}


void push_front(char ch)
{
    insert(0,ch);
}

erase函数

erase函数的作用是删除字符串任意位置开始的n个字符。删除字符前也需要判断pos的合法性,进行删除操作的时候分两种情况:
1、pos位置及其之后的有效字符都需要被删除。
这时我们只需在pos位置放上’\0’,然后将对象的size更新即可。

2.删除pos位置的一部分len长度的字符

//删除pos位置开始的len个字符
string& erase(size_t pos, size_t len = npos)
{
	assert(pos < _size); //检测下标的合法性--下标是0-_size-1,所以删除的位置不能超过_size
	size_t n = _size - pos; //pos位置及其后面的有效字符总数
	if (len >= n) //说明pos位置及其后面的字符都被删除
	{
		_size = pos; //size更新
		_str[_size] = '\0'; //字符串后面放上'\0'
	}
	else //说明pos位置及其后方的有效字符需要保留一部分
	{
		strcpy(_str + pos, _str + pos + len); //用需要保留的有效字符覆盖需要删除的有效字符
		_size -= len; //size更新
	}
	return *this;
}

pop_back和pop_front函数

void pop_back()
{
    erase(_size,1);
}

void pop_front()
{
    erase(0,1);
}

append函数

主要含义就是在尾部追加字符串

//尾插字符串
void append(const char* str)
{
	insert(_size, str); //在字符串末尾插入字符串str
}

6.访问相关函数

operator[]---实际是一个函数重载

//[]运算符重载(可读可写)---由于是char*的结构,支持下标随机访问
char& operator[](size_t i)
{
	assert(i < _size); //检测下标的合法性
	return _str[i]; //返回对应字符
}

//只能读
const char& operator[](size_t i)
{
	assert(i < _size); //检测下标的合法性
	return _str[i]; //返回对应字符
}

find函数

 找到了就返回下标,没找到就返回npos,即-1

//正向查找第一个匹配的字符
size_t find(char ch, size_t pos = 0)
{
	assert(pos < _size); //检测下标的合法性
	for (size_t i = pos; i < _size; i++) //从pos位置开始向后寻找目标字符
	{
		if (_str[i] == ch)
		{
			return i; //找到目标字符,返回其下标
		}
	}
	return npos; //没有找到目标字符,返回npos
}

7.总结

string类的大部分重要接口都已经完全的实现了,希望这部分内容能够重点掌握--因为在面试的过程中很有可能考官让你手撕一个string类

                                                下期预告:vector 

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

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

相关文章

Simulink从0搭建模型09-P10 各类常用子系统介绍

Simulink从0搭建模型09-P10 各类常用子系统介绍 今日学习内容1. 使能子系统 Enabled Subsystem2. 触发子系统 Triggered Subsystem3. 函数调用子系统 Function-Call Subsystem3.1. 使用Function-Call Generator 产生一个f()信号3.2. 使用Stateflow产生f()一个信号3.2.1. Chart模…

CentOS 7~9 救援模式恢复root密码实战指南

在管理Linux服务器时&#xff0c;忘记root密码是一件棘手的事情&#xff0c;但幸运的是&#xff0c;CentOS提供了救援模式来帮助我们重置root密码。本文将详细介绍如何通过GRUB引导菜单进入紧急模式&#xff08;或称为救援模式&#xff09;&#xff0c;进而恢复root用户的密码。…

政安晨【零基础玩转各类开源AI项目】:解析开源项目:Champ 利用三维参数指导制作可控且一致的人体图像动画

目录 论文题目 Champ: 利用三维参数指导制作可控且一致的人体图像动画 安装 创建 conda 环境&#xff1a; 使用 pip 安装软件包 推理 1. 下载预训练模型 2. 准备准备引导动作数据 运行推理 训练模型 准备数据集 运行训练脚本 数据集 政安晨的个人主页&#xff1a;…

Stable diffusion文生图大模型——隐扩散模型原理解析

1、前言 本篇文章&#xff0c;我们将讲这些年非常流行的文生图大模型——Stable Diffusion。该模型也不难&#xff0c;甚至说很简单。创新点也相对较少&#xff0c;如果你学会了我以前的文章讲过的模型&#xff0c;学习这个也自然水到渠成&#xff01; 参考论文&#xff1a;H…

JVM-JAVA-双亲委派机制

双亲委派机制 双亲委派机制Tomcat打破双亲委派机制 双亲委派机制 双亲委派机制&#xff0c;加载某个类时会先委托父加载器寻找目标类&#xff0c;找不到再委托上层父加载器加载&#xff0c;如果所有父加载器在自己的加载类路径下都找不到目标类&#xff0c;则在自己的类加载路径…

Docker 基础使用 (1) 使用流程概览

文章目录 Docker 软件安装Docker 镜像仓库Docker 仓库指令Docker 镜像指令Docker 容器指令Docker 使用实例 —— 搭建 nginx 服务nginx 概念nginx 使用用 docker 启动 nginx Docker 基础使用&#xff08;0&#xff09;基础认识 Docker 基础使用 (1) 使用流程概览 Docker 基础使…

PHPStudy(xp 小皮)V8.1.1 通过cmd进入MySQL命令行模式

PHPStudy是一个PHP开发环境集成包&#xff0c;可用在本地电脑或者服务器上&#xff0c;该程序包集成最新的PHP/MySql/Apache/Nginx/Redis/FTP/Composer&#xff0c;一次性安装&#xff0c;无须配置即可使用。MySQL MySQL是一个关系型数据库管理系统&#xff0c;由瑞典 MySQL A…

【C++入门到精通】C++ thread线程库 [ C++入门 ]

阅读导航 引言一、thread类的简单介绍二、thread类的用法1. 创建线程2. 使用 Lambda 表达式3. 传递参数给线程4. 线程的 join 和 detach5. 检查线程是否可 join6. 线程的 ID7. 线程的移动语义8. 线程的析构&#x1f6a8; 注意事项 三、线程函数参数温馨提示 引言 C thread线程…

代码随想录第二十三天 | 530.二叉搜索树的最小绝对差,501.二叉搜索树中的众数,236.二叉树的最近公共祖先

530.二叉搜索树的最小绝对差 看完想法&#xff1a;需要熟悉一下双指针的操作&#xff0c;好久没复习了&#xff0c;优先掌握递归 遇到在二叉搜索树上求什么最值&#xff0c;求差值之类的&#xff0c;都要思考一下二叉搜索树可是有序的&#xff0c;要利用好这一特点&#xff0…

el-date-picker选择开始日期的近半年

<el-date-pickerv-model"form[val.key]":type"val.datePickerType || daterange":clearable"val.clearable && true"range-separator"~"start-placeholder"开始日期"end-placeholder"结束日期"style&q…

ETF期权开户流程复杂吗?

ETF期权开户流程复杂吗&#xff1f;对于许多初次接触ETF期权的投资者来说&#xff0c;这个问题可能会让他们感到困惑。实际上&#xff0c;ETF期权开户的流程虽然涉及一些步骤&#xff0c;但只要遵循正确的指引&#xff0c;理解每一步的要求&#xff0c;整个过程并不会显得过于复…

【图像处理与机器视觉】频率域滤波

知识铺垫 复数 CRjI 可以看作复平面上的点&#xff0c;则该复数的坐标为&#xff08;R&#xff0c;I&#xff09; 欧拉公式 e j θ c o s θ j s i n θ e^{j\theta} cos \theta j sin \theta ejθcosθjsinθ 极坐标系中复数可以表示为&#xff1a; C ∣ C ∣ ( c o s…

安徽京准NTP时钟系统:GPS北斗卫星授时下的生活重塑

安徽京准NTP时钟系统&#xff1a;GPS北斗卫星授时下的生活重塑 安徽京准NTP时钟系统&#xff1a;GPS北斗卫星授时下的生活重塑 时间的流逝自古以来时钟都是人类生活与活动的基础。然而&#xff0c;随着科技的进步&#xff0c;我们对时间管理和测量的方法已经发生了翻天覆地的变…

0603《哎选》已经稳定运行2年

0603《哎选》已经稳定运行2年 0603《哎选》已经稳定运行2年 介绍 2022年6月3日经过一年的努力&#xff0c;优雅草蜻蜓G系统原生版诞生&#xff0c;本产品应用于《哎选》&#xff0c;经过2年的运营不断的更新迭代&#xff0c;目前产品已经有了一定的用户量&#xff0c;本产品…

毕业论文轻松写:AI写作工具如何助你一臂之力?

时间过的好快&#xff0c;马上又到了一年一度的毕业季了&#xff0c;对于即将毕业的学生来说毕业论文是一道难过的坎&#xff0c;想到自己为了毕业论文熬的夜&#xff0c;掉的头发&#xff0c;真的深有感触。 不过虽然翟博士给大家的毕业论文设了高门槛&#xff0c;但是随着时…

深度神经网络——什么是梯度下降?

如果对神经网络的训练有所了解&#xff0c;那么很可能已经听说过“梯度下降”这一术语。梯度下降是提升神经网络性能、降低其误差率的主要技术手段。然而&#xff0c;对于机器学习新手来说&#xff0c;梯度下降的概念可能稍显晦涩。本文旨在帮助您直观理解梯度下降的工作原理。…

【C#】类和结构体的区别

目录 1.区别概述 ​编辑 2.细节区别 3.结构体的特别之处 4.如何选择结构体和类 1.区别概述 结构体和类的最大区别是在存储空间上&#xff0c;前者是值类型&#xff0c;存储在栈上&#xff0c;后者是引用类型&#xff0c;存储在堆上&#xff0c;它们在赋值上有很大的区别&a…

Windows系统下安装JMeter

大家好&#xff0c;性能测试是现代软件开发中至关重要的一环&#xff0c;它能够帮助开发人员评估系统在不同负载条件下的稳定性和性能表现。而Apache JMeter作为一款功能强大的性能测试工具&#xff0c;广泛被业界采用。如果您正在Windows系统下寻求一种可靠的性能测试工具&…

引领未来,ArmSoM-Sige5震撼发布:RK3576芯片搭载,多媒体应用新宠

在数字化浪潮的推动下&#xff0c;ArmSoM-Sige5携手Rockchip RK3576第二代8纳米高性能AIOT平台&#xff0c;以颠覆性的性能和多功能性&#xff0c;成为多媒体应用的新宠儿。这一全新产品不仅拥有6 TOPS算力NPU和最大可配16GB大内存&#xff0c;更支持4K视频编解码&#xff0c;具…

Yuan 2.0-M32 是一个基于 Yuan 2.0 架构的双语混合专家 (MoE) 语言模型,旨在以更少的参数和计算量实现更高的准确率

主要创新点&#xff1a; 注意力路由器 (Attention Router): 提出了一种新的路由器网络&#xff0c;考虑了专家之间的相关性&#xff0c;从而提高了模型的准确率。高效计算&#xff1a; 使用 MoE 架构&#xff0c;40B 总参数中仅有 3.7B 激活参数&#xff0c;训练计算消耗仅为同…