【C++】string类的模拟实现

文章目录

  • string类的存储结构
  • 默认成员函数
    • 构造函数
    • 析构函数
    • 拷贝构造函数
    • 赋值重载
  • 容量操作
    • size()
    • capacity()
    • reserve()
    • resize()
    • clear()
  • 遍历与访问
    • operator[ ]
    • 迭代器范围与for
  • 增删查改
    • push_back()
    • pop_back()
    • append()
    • operator+=
    • insert()
    • erase()
    • c_str()
    • find()
    • substr()
  • 非成员函数
    • operator+
    • 关系运算符
    • 流插入<<和流提取>>
    • getline()

本篇参考C++string类参考手册,实现一些string的常用接口,接口原型在该网站查阅。

string类的存储结构

string类的底层实际上是char类型的顺序表,所以结构上也比较相似。

namespace lw
{class string{public:static const size_t npos;private:size_t _size;//有效数据个数size_t _capacity;//可存储的容量char* _str;//指向字符串起始位置的地址};const size_t string::npos = -1;
};

STL源码中,许多整型变量类型都是size_t无符号整型(没有负值);npos是string类常用到的一个值,有些函数的参数或者返回值是npos,并且这个值设为-1(表示232-1),参考标准库中的定义。
在这里插入图片描述

为什么string定义在一个命名空间中?
我们如果展开了标准库using namespace std; 那string默认使用的就是标准库中的。如果不展开标准库,那么所有的cin和cout都需要加上std:: 比较繁琐。

用一个命名空间对我们自己实现的string类进行封装,可以方便我们随时测试代码,与库里的string类进行测试对比,只需要改::前面的作用域即可。

#include <iostream>
using namespace std;
int main()
{string s("hello world");//默认标准库std::string s("hello world");//标准库lw::string s("hello world");//自己定义的类cout << s << endl;return 0;
}

string与C语言中的char类型字符串的一个较大的区别就是:char类型的字符串是以\0为结束符,遇到\0就停止;而string是以_size来判断是否结束,遇到\0并不会停止。 这点很重要,一定要弄清楚!


默认成员函数

常用的四个默认成员函数,一般情况我们不需要显示定义,使用编译成生成的即可;但string类涉及到资源申请,所以我们必须自己显示定义。

构造函数

官网的参考手册中给的string标准库有很多构造函数重载,我们只实现常用的两个构造。这两个我们可以合并成一个实现。

string();
string(const char* s);

缺省值不能给nullptr,一是因为strlen无法计算空指针,二是因为无参标准库默认给的就是空串。
这里strcpy和memcpy都可以,因为是对char类型字符串进行拷贝,拷贝str在\0之前的内容。用memcpy只是为了和后面的写法统一。

string(const char* str = ""): _size(strlen(str)), _capacity(strlen(str)), _str(new char[strlen(str) + 1])//多一个空间给'\0'
{//strcpy(_str, str);//strcpy也可以,只是为了与后面统一,换成memcpymemcpy(_str, str, _size + 1);//'\0'也要拷贝
}

我们在new新空间时,每次多new一个空间给\0留位置。 memcpy在拷贝时,多拷贝一个字节把末尾\0也拷贝过去。


析构函数

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

拷贝构造函数

深浅拷贝问题
编译器默认生成的是浅拷贝,就是只将s._str的指针地址拷贝给了新对象的_str,两个对象指向同一块地址! 那么就会出问题:

1.一块空间最后会析构两次,程序必然崩溃;
2.一个对象修改,另一个对象也会修改;

所以我们需要进行深拷贝,重新申请一块空间,把s._str指向地址的内容拷贝过来。

string(const string& s)
{_str = new char[s.capacity() + 1];//多一个空间给'\0'//strcpy(_str, s._str);//遇到'\0'终止,后面内容无法拷贝memcpy(_str, s._str, s._size + 1);//末尾'\0'也拷贝过来_size = s._size;_capacity = s._capacity;
}

注意:这里不能用strcpy来拷贝数据,因为strcpy的拷贝结束条件是遇到\0终止,而string对象是可以存储\0的,strcpy不会将\0后面内容拷贝过来,所以要用memcpy或者memmove按照字节进行拷贝,多拷贝一个字节是将末尾的\0也拷贝过来。


赋值重载

与拷贝构造原理类似,但要先将申请的空间存放在临时变量里,防止空间开辟失败丢失原数据。原始空间别忘了释放!

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;//支持连续赋值
}

第二种写法:我们可以利用库里的swap函数将两个对象_str指向的地址和数据个数、容量完全交换。顺便将swap接口也实现了。

void swap(string& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}
//现代写法
string& operator=(string s)//不能传引用也不能加const
{swap(s);return *this;
}

注意:不能影响右操作数实参的值,所以右操作数传参只能以传值方式,且不能加const,否则不能交换改变。并且我们不需要进行自己给自己赋值的判断,因为s是实参的拷贝,一定与原对象地址不同。


容量操作

size()

size_t size() const
{return _size;
}

capacity()

size_t capacity() const
{return _capacity;
}

reserve()

只扩容不缩容,指定n大于原始容量则进行扩容,否则不进行操作。
拷贝时还是同样要注意\0问题,不能用strcpy。

void reserve(size_t n)
{if (n > _capacity){char* tmp = new char[n + 1];//strcpy(tmp, _str);//错误,无法拷贝'\0'后面的内容memcpy(tmp, _str, _size + 1);delete[] _str;_str = tmp;_capacity = n;}
}

resize()

resize()只改变有效数据个数,不会改变容量。
n > _size:缩小长度为n,多余内容删掉。
n < _szie:增加长度到n,剩下空间用给定的字符参数c填充,无参默认补充\0

void resize (size_t n);
void resize (size_t n, char c);

库里的resize()函数有两个版本,同样我们可以合成一个版本,第二个参数给缺省值\0即可。

void resize(size_t n, char ch = '\0')
{assert(n >= 0);if (n < _size)//缩小长度,后面删掉{_size = n;_str[_size] = '\0';}else{reserve(n);//reserve会检查容量for (size_t i = _size; i < n; i++){_str[i] = ch;}_size = n;_str[_size] = '\0';}
}

clear()

void clear()
{_size = 0;_str[0] = '\0';
}

遍历与访问

operator[ ]

需要实现两个版本:普通对象调用[ ]可读可写,const对象调用[ ]只可读不可写。

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

迭代器范围与for

迭代器不一定是指针,而string的迭代器我们可以用指针来模拟实现。将指针重命名为迭代器。

typedef char* iterator;//普通迭代器 可读可写
iterator begin()
{return _str;
}
iterator end()
{return _str + _size;
}typedef const char* const_iterator;//const迭代器 只可读
const_iterator begin() const
{return _str;
}
const_iterator end() const
{return _str + _size;
}

左闭右开区间,所以begin()返回字符串起始位置,end()返回末尾字符的下一个位置\0

范围for
实现迭代器后,我们就可以使用范围for了。范围for是C++11的新语法,它的底层是傻瓜式地替换成迭代器,支持迭代器就支持范围for。

int main()
{lw::string s1("hello world");lw::string::iterator it = s1.begin();while (it != s1.end()){*it += 1;cout << *it << ' ';it++;}cout << endl;for (auto ch : s1){cout << ch << ' ';}cout << endl;
}

增删查改

push_back()

末尾要放\0

void push_back(char ch)
{if (_size == _capacity){reserve(_capacity == 0 ? 10 : _capacity * 2);}_str[_size++] = ch;_str[_size] = '\0';
}

pop_back()

void pop_back()
{assert(_size > 0);_size--;_str[_size] = '\0';
}

append()

append()在末尾追加字符串,空间不够则扩容,如果每次将_capacity扩2倍,一方面可能造成空间浪费,另一方面可能原始空间太小导致频繁扩容,所以我们最好提前计算好扩容的空间。

append的接口也有很多,下面给出最常用的两种。

string& append(const char* str)
{size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}//strcpy(_str + _size, str);//strcat效率O(n+m) strcpy效率O(m)memcpy(_str + _size, str, len + 1);//末尾的'\0'也要拷贝_size += len;return *this;
}
string& append(const string& s)
{if (_size + s._size> _capacity){reserve(_size + s._size);}memcpy(_str + _size, s._str, s._size + 1);//末尾的'\0'也要拷贝_size += s._size;return *this;
}

operator+=

+=是string经常用到的操作符,使用非常方便。
operator+=总共有三个重载版本,我们可以直接复用push_back()和append()

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

insert()

任意位置插入字符ch

string& insert(size_t pos, size_t n, char ch)
{assert(pos <= _size);if (_size + n > _capacity){reserve(_size + n);}for (size_t end = _size; end >= pos; end--){//pos==0时end会减到-1//无符号整型 end=-1时表示42亿多 不加判断会无限循环if (end == npos){break;}_str[end + n] = _str[end];}for (size_t i = 0; i < n; i++){_str[i + pos] = ch;}_size += n;return *this;
}

任意位置插入字符串str

string& insert(size_t pos, const char* str)
{assert(pos <= _size);size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}for (size_t end = _size; end >= pos; end--){if (end == npos){break;}_str[end + len] = _str[end];}memcpy(_str + pos, str, len);//不能多拷贝一个字节_size += len;return *this;
}

注意:这里的memcpy不能跟之前一样多拷贝一个字节,否则会将原对象pos+1的字符替换成\0,会出错。


erase()

string& erase(size_t pos, size_t len = npos)
{assert(pos < _size);//没给参数 或者 要删除的个数>=pos后面剩下的个数 则后面包括pos位置全部删掉if (len == npos || pos + len >= _size){//pos及pos后面所有字符都删掉_size = pos;_str[_size] = '\0';}else{memcpy(_str + pos, _str + pos + len, len);_size -= len;}return *this;
}

c_str()

返回字符串首地址,以\0结束。这个接口主要是用来兼容C语言的。

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

find()

find查找失败会返回npos,找到则返回下标

//查找字符
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* p = strstr(_str + pos, str);if (p){return p - _str;}else{return npos;}
}

substr()

返回子串

string substr(size_t pos = 0, size_t len = npos) const
{assert(pos < _size);size_t n = len;//子串长度if (len == npos || pos + len > _size){n = _size - pos;}string tmp;for (size_t i = 0; i < n; i++){tmp += _str[i + pos];//复用+=}return tmp;
}

非成员函数

operator+

实际上+操作符很少用,直接用+=更方便省事;这里只实现了一个接口,重载为友元函数。

string operator+(const string& s1, const string& s2)
{string tmp(s1);tmp += s2;return tmp;
}

关系运算符

C++官方手册中,每种运算符都有3种重载版本,这里每个只实现了一种,重要的是理解本质,加深对string的理解。

为什么用C语言的memcmp函数进行字节上的比较,不用strcmp?
因为strcmp遇到\0终止,而string不看\0,以_size为终止。
当然也可以不用memcmp,遍历每个字符进行比较。

bool operator==(const string& s1, const string& s2)
{return s1._size == s2._size && memcmp(s1._str, s2._str, min(s1._size, s2._size)) == 0;
}
bool operator<(const string& s1, const string& s2)
{int ret = memcmp(s1._str, s2._str, min(s1._size, s2._size));return ret == 0 ? s1._size < s2._size : ret < 0;
}//直接复用
bool operator!=(const string& s1, const string& s2)
{return !(s1 == s2);
}
bool operator<=(const string& s1, const string& s2)
{return (s1 < s2) || (s1 == s2);
}
bool operator>(const string& s1, const string& s2)
{return !(s1 <= s2);
}
bool operator>=(const string& s1, const string& s2)
{return !(s1 < s2);
}

流插入<<和流提取>>

流插入的实现可以借助范围for,本质是遍历整个字符串打印每个字符。

ostream& operator<<(ostream& out, const string& s)
{//out << s._str;//'\0'后面的内容无法打印for (auto ch : s){out << ch;}return out;//带返回值 支持连续输出
}

流提取需要注意几种情况:
1.每次读取之前需要先清空string对象,否则会叠加之前的内容。
2.为什么用get()读取而不用>>?
流提取>>默认是跳过空格和换行的,所以>>永远无法读取空格和换行,程序会一直运行一直可以输入。
3.如果字符串前面有空格或者换行,标准库的>>会默认清理空格和换行。所以要预先处理前面的空格和换行。

istream& operator>>(istream& in, string& s)
{s.clear();//清空之前的内容char ch = in.get();//可以读取空格和换行//处理掉缓冲区前面的空格或者换行while (ch == ' ' || ch == '\n'){ch = in.get();}while (ch != ' ' && ch != '\n'){s += ch;ch = in.get();}return in;
}

getline()

getline可以自定义读取的分隔符,遇到分隔符就不再读取,字符参数默认为\n,换行截断。

istream& getline(istream& in, string& s, char delim = '\n')
{s.clear();//清空之前的内容char ch = in.get();while (ch != delim){s += ch;ch = in.get();}return in;
}

整体代码->:string模拟实现代码

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

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

相关文章

VisualRules组件功能介绍-计算表格(二)

本章内容 1、计算表格数据回写数据库 2、计算表格数据更新 3、计算表格数据汇总 4、计算表格数据追加 一、计算表格数据回写数据库 计算表格数据回写数据库表。采用遍历计算表格逐条插入数据库表。在具体操作过程可以采用向导方式操作。 先在数据库表中创建tb_user_new表。…

python-糖果俱乐部(赛氪OJ)

[题目描述] 为了庆祝“华为杯”的举办&#xff0c;校园中开展了许多有趣的热身小活动。小理听到这个消息非常激动&#xff0c;他赶忙去参加了糖果俱乐部的活动。 该活动的规则是这样的&#xff1a;摊位上有 n 堆糖果&#xff0c;第 i 堆糖果有 ai​ 个&#xff0c;参与的同学可…

让采购和工程师们既爱又恨的任务——BOM

在项目研发与生产过程中&#xff0c;有一个常常让采购经理和工程师们既爱又恨的任务&#xff0c;那就是整理BBOMB。BOM作为连接设计与制造的桥梁&#xff0c;其重要性不言而喻&#xff0c;它详细列出了产品构成所需的所有零部件、材料及其规格、数量&#xff0c;是成本估算、采…

用四个场景案例,分析使用大模型对程序员工作的帮助提升_大模型应用场景

引言 随着人工智能技术的不断发展&#xff0c;大模型在软件开发中的应用越来越广泛。 这些大模型&#xff0c;如GPT、文心一言、讯飞星火、盘古大模型等&#xff0c;可以帮助程序员提高工作效率&#xff0c;加快开发速度&#xff0c;并提供更好的用户体验。 本文将介绍我在实…

Unity海面效果——4、法线贴图和高光

Unity引擎制作海面效果 大家好&#xff0c;我是阿赵。 继续做海面效果&#xff0c;上次做完了漫反射颜色和水波动画&#xff0c;这次来做法线和高光效果。 一、 高光的计算 之前介绍过高光的光照模型做法&#xff0c;比较常用的是Blinn-Phong 所以我这里也稍微连线实现了一下 …

苍穹外卖项目 常用注解 + 动态sql

常用注解 常见的注解解析方法有两种&#xff1a; 编译期直接扫描&#xff1a;编译器在编译 Java 代码的时候扫描对应的注解并处理&#xff0c;比如某个方法使用Override 注解&#xff0c;编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。运行期通过反射处理&…

云数据中心运维新纪元:让Linux服务器如虎添翼

文章目录 一、Linux系统管理的高级技巧1. 性能调优与监控&#xff1a;2. 自动化与脚本编写&#xff1a;3. 文件系统与存储管理&#xff1a; 二、服务器配置优化的策略1. 硬件选型与配置&#xff1a;2. 网络配置与优化&#xff1a;3. 应用部署与调优&#xff1a; 三、安全策略的…

postgre事务id用完后,如何解决这个问题

在PG中事务年龄不能超过2^31 &#xff08;2的31次方2,147,483,648&#xff09;&#xff0c;如果超过了&#xff0c;这条数据就会丢失。 PG中不允许这种情况出现&#xff0c;当事务的年龄离2^31还有1千万的时候&#xff0c;数据库的日志中就会 有如下告警&#xff1a; warning:…

js获取当前浏览器地址,ip,端口号等等

前言&#xff1a; js获取当前浏览器地址&#xff0c;ip&#xff0c;端口号等等 window.location属性查询 具体属性&#xff1a; 1、获取他的ip地址 window.location.hostname 2、获取他的端口号 window.location.port 3、获取他的全路径 window.location.origin 4、获取…

【机器学习】基于层次的聚类方法:理论与实践

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 基于层次的聚类方法&#xff1a;理论与实践引言1. 层次聚类基础1.1 概述1.2 距离…

讨论Nginx服务器的反爬虫和反DDoS攻击策略

Nginx服务器是一个高性能的Web服务器和反向代理服务器&#xff0c;具有强大的反爬虫和反DDoS攻击能力。本文将讨论Nginx服务器的反爬虫和反DDoS攻击策略&#xff0c;并给出相关的代码示例。 一、反爬虫策略 爬虫是一种自动化程序&#xff0c;用于从互联网上收集特定网站的数据…

【产品运营】Saas的核心六大数据

国内头部软件公司的一季度表现惨不忍睹&#xff0c;为啥美国的还那么赚钱呢&#xff1f;其实核心是&#xff0c;没几个Saas产品经理是看数据的&#xff0c;也不知道看啥数据。 SaaS 行业&#xff0c;天天抛头露面、名头叫的响的 SaaS 产品&#xff0c;真没有几个赚钱的。 那为…

笔记101:OSQP求解器的底层算法 -- ADMM算法

前言1&#xff1a;这篇博客仅限于介绍拉格朗日乘子法&#xff0c;KKT条件&#xff0c;ALM算法&#xff0c;ADMM算法等最优化方法的使用以及简版代码实现&#xff0c;但不会涉及具体的数学推导&#xff1b;不过在下面我会给出具体数学推导的相关文章和截图&#xff0c;供学有余力…

Pytest+Allure+Yaml+PyMsql+Jenkins+Gitlab接口自动化(四)Jenkins配置

一、背景 Jenkins&#xff08;本地宿主机搭建&#xff09; 拉取GitLab(服务器)代码到在Jenkins工作空间本地运行并生成Allure测试报告 二、框架改动点 框架主运行程序需要先注释掉运行代码&#xff08;可不改&#xff0c;如果运行报allure找不到就直接注释掉&#xff09; …

CCAA:认证通用基础 10(审核的概念、审核有关的术语、审核的特征、审核原则)

10.审核的概念、审核有关的术语、审核的特征、审核原则 10.1审核的基本概念 第一章 审核基础知识 第一节 概述 1.什么是审核 审核是认证过程中最基本的活动&#xff0c;是审核方案的重要组成部分&#xff0c;其实施效果直接影响到审核方案的意图和审核目标的达成。 在认证…

葡萄串目标检测YoloV8——从Pytorch模型训练到C++部署

文章目录 软硬件准备数据准备数据处理脚本模型训练模型部署数据分享软硬件准备 训练端 PytorchultralyticsNvidia 3080Ti部署端 fastdeployonnxruntime数据准备 用labelimg进行数据标注 数据处理脚本 xml2yolo import os import glob import xml.etree.ElementTree as ETxm…

DSPy:变革式大模型应用开发

大模型相关目录 大模型&#xff0c;包括部署微调prompt/Agent应用开发、知识库增强、数据库增强、知识图谱增强、自然语言处理、多模态等大模型应用开发内容 从0起步&#xff0c;扬帆起航。 大模型应用向开发路径&#xff1a;AI代理工作流大模型应用开发实用开源项目汇总大模…

ADS1220IRVAR 模数转换器(ADC) TI德州仪器 封装 国产替代

ADS1220IRVAR 模数转换器&#xff08;ADC&#xff09; TI德州仪器 封装 国产替代

docker 多网卡指定网卡出网

前言 宿主机中有多个网卡 ens160 192.168.4.23/20 内网通信用 ens192 10.31.116.128/24 出公网访问-1 ens193 10.31.116.128/24 出公网访问-2 现在需要不同容器中不同出网访问&#xff0c;举例 容器1 192.168.0.1/20 网段走宿主机 ens160网卡&#xff0c;否则全部走ens192 网…

vue根据文字长短展示跑马灯效果

介绍 为大家介绍一个我编写的vue组件 auto-marquee &#xff0c;他可以根据要展示文本是否超出展示区域&#xff0c;来判断是否使用跑马灯效果&#xff0c;效果图如下所示 假设要展示区域的宽度为500px&#xff0c;当要展示文本的长度小于500px时&#xff0c;只会展示文本&…