【C++笔记】C++string类模拟实现
【C++笔记】C++string类模拟实现
- 一、实现模型和基本接口
- 1.1、各种构造和析构
- 1.2、迭代器
- 二、各种插入和删除接口
- 2.1、插入接口
- 2.2、删除接口
- 2.3、resize接口
- 三、各种运算符重载
- 3.1、方括号运算符重载
- 3.2、各种比较运算符重载
- 四、查找接口
- 4.1、查找字符
- 4.2、查找子串
- 五、流插入流提取运算符重载
- 5.1、流插入运算符重载
- 5.2、流提取运算符重载
C++的string类也就是字符串,它是一个比较早的类,所以就没有被归到STL里。
这里实现的string只是为了粗浅的了解一下string的底层原理,所以可定不会有库里面的那么详细,而且这里也只会实现一些常用的接口,一些不常用的接口实现起来也没什么意思。
一、实现模型和基本接口
既然是管理字符串的,那我们就直接封装一个char*即可:
class String {
public :
private :char* _str; // 时刻被操作的字符串size_t _size; // 长度size_t _capacity; // 容量
};
然后我们实现的各个接口都是为了操作类中封装的这个_str即可。
1.1、各种构造和析构
构造函数:
其实构造函数有很多种实现方式,但我们日常用的最多的应该就是字符串构造,因为他本身就是存储字符串的嘛。
所以我们仅提供一个全缺省的构造函数即可:
String(const char* str = "") {_size = strlen(str);_capacity = strlen(str);_str = new char[_capacity + 1];strcpy(_str, str);}
需要注意的是,这里的容量表示的是该字符串最多能存多少个字符,但我们都知道字符串的结束标志’\0’是不能少的,每个字符串的结尾都必须存一个’\0’,所以我们这里开空间是中要比容量多1。
拷贝构造:
拷贝构造也很简单,直接开一段新空间然后复制参数中_str的内容即可。
String(const String& str) {// 只用来提示构造函数被调用cout << "String(const String& str)" << endl;_str = new char[str._capacity + 1];strcpy(_str, str._str);_size = str._size;_capacity = str._capacity;}
但是依然要注意,我们这里开空间一定要比容量多1。
析构函数:
因为我们至始至终都只管理着一个外部资源也就是_str,所以析构函数要做的工作也就很简单了,直接释放掉_str即可:
~String() {// 只用来提示析构函数被调用cout << "~String()" << endl; delete[] _str;_str = nullptr;_size = 0;_capacity = 0;}
然后我们可以先提供一个简易的打印接口,来试验我们以上所写的接口:
const char* get_str() const {return _str;}

从运行的结果来看,以上写的接口是没什么问题的。
1.2、迭代器
C++中的迭代器是一个很棒的设计,有了迭代器,我们就可以使很多类的遍历方法变得相同。
但这只是写的时候相同,迭代器其实是屏蔽了底层的原理,然后使用一套统一的方法来遍历,例如:

如上,字符串、vector和链表的底层肯定是不一样的,但它们展现出来的遍历方式却是一样的,这就是iterator迭代器的妙处。
这样一来即使有一些容器我们并不知道它的底层实现原理怎样,但是我们也还是可以照常遍历它们。
相信大家从上面对it的解引用也可以看得出,其实迭迭代器底层就是模拟的指针的行为。
但迭代器也并非都是用原生指针来实现的,对于像string和vector指针顺序结构,使用原生指针是完全可以的。但是对于list这种链式结构和各种树形结构使用原生指针的话就不行了,这需要对原生指针就行再次封装才行。
而现在的string使用原生指针是完全可以的:
class String {
public :
typedef char* iterator;
private :char* _str; // 时刻被操作的字符串size_t _size; // 长度size_t _capacity; // 容量
};
迭代器我们往往只需要提供两个位置即开始和结尾即可:
iterator begin() {return _str;
}
// 迭代器结束位置
iterator end() {return _str + _size;
}
这样我们就可以使用迭代器来遍历我们模拟实现的string类了:

有了迭代器,我们就可以使用一个更简便的遍历方式——范围for,因为范围for的底层就是使用迭代器实现的,编译器只是把范围for的语法傻傻的替换成迭代器而已:

二、各种插入和删除接口
2.1、插入接口
尾插一个字符
string中的_size指的是字符串的长度,但是因为字符串的下标其实是从0开始的,所以实际上_size所指向的位置其实是’\0’的位置:

所以当我们要尾插一个字符的时候,这个字符其实是要放在_size的位置。
void push_back(char ch) {// 检查扩容if (_size == _capacity) {reverse(_capacity == 0 ? 4 : 2 * _capacity);}_str[_size] = ch;_size++;_str[_size] = '\0';
}
如上,要插入数据就必定会遇到空间不足的情况,所以我们在正式插入之前必须先检查是否需要扩容。扩容的逻辑很简单,就是申请一块新容量的空间然后拷贝数据到新空间然后释放就空间在在让_str指向新空间即可:
void reverse(size_t newCapacity) {if (newCapacity > _capacity) {char* temp = new char[newCapacity + 1];strcpy(temp, _str);delete[] _str;_str = temp;_capacity = newCapacity;}
}
尾插一个字符串(追加)
尾插一个字符串的逻辑其实和尾插一个字符的逻辑大差不差,只不过是字符变多了而已。
void append(const char* str) {assert(str);// 检查扩容size_t len = strlen(str);if (_size + len > _capacity) {reverse(_size + len);}strncpy(_str + _size, str, len);_size += len;// 因为strcpy并不会连带'\0'一起拷贝,所以我们得自己补上_str[_size] = '\0';
}
随机插入一个字符
在pos位置插入一个字符ch。
因为string也属于顺序结构,对于顺序结构来说随机插入最烦人的就是挪动数据,例如这里我们要把从pos位置其后面的所有数据都向后移动一个位置:

未完成这个操作我们可以定义一个end指向_size的位置 (使end从_size开始是为了连‘‘0’一起移动,到最后就不需要再手动加上’\0’了),也就是’\0’的位置:

然后循环操作_str[end + 1] = _str[end],并让end–直到end与pos重合为止。
void insert(size_t pos, char ch) {assert(pos <= _size);// 检查扩容if (_size == _capacity) {reverse(_capacity == 0 ? 4 : 2 * _capacity);}// 挪动数据int end = _size;while (end >= (int)pos) {_str[end + 1] = _str[end];end--;}_str[pos] = ch;_size++;
}
随机插入一个字符串
随机插入一个字符串的逻辑也和随机插入一个字符的逻辑大差不差,我们几乎可以复用之前的逻辑,然后只需要改动挪动数据的间隔即可。
void insert(size_t pos, const char* str) {assert(pos <= _size);// 检查扩容size_t len = strlen(str);if (_size + len > _capacity) {reverse(_size + len);}// 挪动数据int end = _size;while (end >= (int)pos) {// 这里只需要将1改成len即可_str[end + len] = _str[end];end--;}// 拷贝数据strncpy(_str + pos, str, len);_size += len;}
写完我们可以顺势来检验一下:

2.2、删除接口
从pos位置开始,删除len个字符。
这个接口的逻辑就比insert要简单了,我们只需要将后面的数都向前挪动len个位置,覆盖掉前面的数据即可。
例如pos = 5,len = 6的情况:

为了完成这个操作,定义一个变量begin,让它从pos + len位置开始,然后循环执行_str[begin - len] = _str[begin],并让begin++,直到begin和_size重合(连带’\0’一起挪动):

有了以上分析,那我们写起代码来也就水到渠成了:
void erase(size_t pos, size_t len) {assert(pos < _size);size_t begin = pos + len;// 挪动数据while (begin <= _size) {_str[begin - len] = _str[begin];begin++;}_size -= len;
}
2.3、resize接口
有时候我们可能需要重置长度,特别是想要删除后面的数据只保留前面若干个数据的时候,我们或许会觉得使用erease可能太麻烦了。
所以我们要用到resize,这个接口可以一键保留前几个字符,或者扩大容量,后面的空间以某个字符进行填充。
实现这个接口其实要分两种情况:
当newSize < _size时候,我们要做的其实很简单,就是直接让_size = newSize,然后再让_str[_size] = '\0’即可。
而当newSize > _size时候我们才要进行填充,特别是当newSize > _capacity时,我们就需要先扩容再进行填充。
void resize(size_t newSize, char ch = '\0') {if (newSize > _size) {if (newSize > _capacity) {reverse(newSize);}// 填充字符memset(_str + _size, ch, newSize - _size);}_size = newSize;_str[_size] = '\0';
}
三、各种运算符重载
有了各种运算符重载我们就可以像内置类型一样使用我们的自定义类型了。
3.1、方括号运算符重载
有了方括号运算符重载,我们就可以像数组一样使用方括号[]来对我们定义的字符串进行遍历了。
它的实现原理很简单,就是返回_str[i]位置的引用即可:
char& operator[](size_t index) {assert(index < _size);return _str[index];
}
const char& operator[](size_t index) const {assert(index < _size);return _str[index];
}
但方括号运算符重载也要分const和非const版本的,因为有些时候我们只是想读取数据而不想修改数据,有些时候我们即想读也想改。
紧接着我们就可以来测试一下了:

3.2、各种比较运算符重载
小于运算符重载
既然我们都是在对_str进行操作,那当然就可以直接使用strcmp进行比较了:
bool operator<(const String& str) {return strcmp(_str, str._str) < 0;
}
等于运算符重载
bool operator==(const String& str) {return strcmp(_str, str._str) == 0;
}
小于等于运算符重载
其实实现了上面这两个之后,其他的都变得很简单了,我们直接复用即可,这不仅对于string这个类如此。几乎所有类都可以这样,因为这都是一些逻辑判断而已。
bool operator<=(const String& str) {return (*this < str) || (*this == str);
}
大于运算符重载
bool operator>(const String& str) {return !(*this <= str);
}
大于等于运算符重载
bool operator>=(const String& str) {return (*this > str) || (*this == str);
}
不等于运算符重载
bool operator!=(const String& str) {return !(*this == str);
}
四、查找接口
4.1、查找字符
从pos位置开始查找,返回字符ch在String中第一次出现的位置,找不到则返回起始位置。
这个逻辑其实很简单,从pos位置开始匹配即可。
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 pos;
}
我们可以测试一下:

4.2、查找子串
从pos位置开始查找,返回子串str在String中第一次出现的位置。
这个接口我们可以直接使用C++的前身C语言中的库函数strstr:

但是通过查看上面文档中的描述我们就会发现,strstr返回的并不是整数而是一个指针——指向子串在字符串中第一次出现的位置。
但这并不是什么大问题,不要忘了指针是可以相减的,并且相减得的结果也是一个整数,所以我们可以拿strstr的返回值减去_str,也就得到了我们想要的结果。
size_t find(const char* str, size_t pos = 0) const {assert(pos < _size);return strstr(_str + pos, str) - _str;
}
我们同样可以来测试一下:

五、流插入流提取运算符重载
5.1、流插入运算符重载
虽然我们已经有了三种遍历字符串的方式(方括号、迭代器、范围for),但好像觉得都不是很方便。因为我们内置的字符数组其实是可以直接使用cout打印的:

如果用这个和其他的遍历方式进行比较,毫无疑问这个就是最方便的遍历方式了。
那我们实现的string能不能也只用这样的方法呢?
当然可以,我们只需要对String重载流插入运算符即可。
我们之前已经了解过了,因为参数的顺序问题,流插入和流提取运算符重载最好是重载成全局函数。
而又因为在类外边不能访问私有成员,所以我们要将这两个函数声明称String类的友元函数:
class Stirng {// 流插入流提取友元声明friend ostream& operator<<(ostream& _cout, const String& str);friend istream& operator>>(istream& _cin, String& str);
public :
private :char* str;size_t _size;size_t _capacity;
};
然后我们就可以开始动手写代码了:
ostream& operator<<(ostream& _cout, const String& str) {for (auto it : str) {_cout << it;}return _cout;
}
这里还要补充的一点就是关于const迭代器的事项,如果我们没有定义实现const迭代器,那我们直接使用范围for是会报错的:

原因就在于我们这里传的是一个const对象,而我们这里只实现了非const的迭代器,const对象是不能调用非const函数的。
想要解决这个问题我们就要把const迭代器实现了:

然后在实现const迭代器版本的begin和end:

这样我们的代码才能正确运行:

5.2、流提取运算符重载
流提取就相当于C语言中的scanf函数和C++中的cin运算符:

想要重载这个操作符我们在正式接收数据的时候需要先将原字符串内的数据清空:
// 清理数据
void clear() {_size = 0;
}
然后在接收数据插入字符串。
然后我们就可以动手写代码了:
// 流提取运算符重载
istream& operator>>(istream& _cin, String& str) {// 先清理数据str.clear();char ch = _cin.get();while (ch != ' ' && ch != '\n') {str += ch;ch = _cin.get();}return _cin;
}
这里还需要注意的一点就是,像scanf和cin都是默认以空格和换行符为多个字符的分隔的,也就是说他们不会接收空格和换行符。所以要想接收到空格和换行符不能直接用cin,而是要使用cin.get()。
相关文章:
【C++笔记】C++string类模拟实现
【C笔记】Cstring类模拟实现 一、实现模型和基本接口1.1、各种构造和析构1.2、迭代器 二、各种插入和删除接口2.1、插入接口2.2、删除接口2.3、resize接口 三、各种运算符重载3.1、方括号运算符重载3.2、各种比较运算符重载 四、查找接口4.1、查找字符4.2、查找子串 五、流插入…...
操作系统之课后习题——引论
(一)简答题 1.在计算机系统上配置OS的目标是什么?作用主要表现在哪几个方面? 答: 在计算机系统上配置OS,主要目标是实现:方便性、有效性、可扩充性和开放性; OS的作用主要表现在以下…...
【PHP代码审计】反序列化漏洞实战
文章目录 概述资源下载地址Typecho代码审计-漏洞原理call_user_func()_applyFilter()、get()与__get__toString()__construct()install.php POC利用漏洞利用复现利用链执行phpinfo()GET利用POST利用 getshell生成payload漏洞利用蚁剑连接 总结 概述 序列化,“将对象…...
Socks5 与 HTTP 代理在网络安全中的应用
目录 Socks5和HTTP代理在网络安全中的应用。 Socks5代理和HTTP代理的优点和缺点。 选择合适的代理IP需要考虑的因素: 总结 在网络安全领域中,Socks5和HTTP代理都扮演着重要的角色。作为两种不同的代理技术,它们在网络安全中的应用各有特点…...
进阶C语言-指针的进阶(中)
指针的进阶 📖5.函数指针📖6.函数指针数组📖7.指向函数指针数组的指针📖8.回调函数 📖5.函数指针 数组指针 - 指向数组的指针 - 存放的是数组的地址 - &数组名就是数组的地址。 函数指针 - 指向函数的指针 - 存放的…...
保姆级-微信小程序开发教程
一,注册微信小程序 如果你还没有微信公众平台的账号,请先进入微信公众平台首页,点击 “立即注册” 按钮进行注册。注册的账号类型可以是订阅号、服务号、小程序以及企业微信,我们选择 “小程序” 即可。 接着填写账号信息&#x…...
数据库-DQL
DQL:用来查询数据库表中的记录 关键字:SELECT 语法: select:字段列表 from:表名列表 where:条件列表 group by:分组列表 having:分组后条件列表 order by:排序字段列表…...
19 螺旋矩阵
螺旋矩阵 题解1 循环(4个标志——根据顺时针)题解2 方向 给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。 提示: - m matrix.length - n matrix[i].length - 1 < m, n <…...
数据结构与算法:概述
目录 算法 评价标准 时间的复杂度 概念 推导原则 举例 空间的复杂度 定义 情形 运用场景 数据结构 组成方式 算法 在数学领域,算法是解决某一类问题的公式和思想; 计算机科学领域,是指一系列程序指令,用于解决特定的…...
顺序表详解
💓 博客主页:江池俊的博客⏩ 收录专栏:数据结构探索👉专栏推荐:✅C语言初阶之路 ✅C语言进阶之路💻代码仓库:江池俊的代码仓库🔥编译环境:Visual Studio 2022Ἰ…...
基于RabbitMQ的模拟消息队列之六——网络通信设计
自定义基于TCP的应用层通信协议。实现客户端对服务器的远程调用 编写服务器及客户端代码 文章目录 基于TCP的自定义应用层协议一、请求1.请求格式2.创建Request类 二、响应1.响应格式2.创建Response类 三、客户端-服务器交互四、type五、请求payload1.BasicAruguments(方法公共…...
算法:数组中的最大差值---“打擂台法“
文章来源: https://blog.csdn.net/weixin_45630258/article/details/132737088 欢迎各位大佬指点、三连 1、题目: 给定一个整数数组 nums,找出给定数组中两个数字之间的最大差值。要求,第二个数字必须大于第一个数字。 2、分析特…...
三种方式查看 JVM 垃圾收集器
一、引言 不同版本的 JVM 默认使用的垃圾收集器是不同的,目前的新生代和老年代的垃圾收集器如下图所示,新生代和老年代之间的连线表示这些垃圾收集器可以进行搭配使用 垃圾收集器的名字和 JVM 里面的参数对照表如下,即在 JVM 里面并不是存储的…...
React中函数式组件与类组件有何不同?
Function Component 与 Class Component 有何不同 目录 Function Component 与 Class Component 有何不同 文章核心观点: 解释一下: 总结: 文章核心观点: Function components capture the rendered values.函数式组件捕获…...
windows11安装docker时,修改默认安装到C盘
1、修改默认安装到C盘 2、如果之前安装过docker,请删除如下目录:C:\Program Files\Docker 3、在D盘新建目录:D:\Program Files\Docker 4、winr,以管理员权限运行cmd 5、在cmd中执行如下命令,建立软联接: m…...
python模块之 aiomysql 异步mysql
mysql安装教程 mysql语法大全 python 模块pymysql模块,连接mysql数据库 一、介绍 aiomysql 是一个基于 asyncio 的异步 MySQL 客户端库,用于在 Python 中与 MySQL 数据库进行交互。它提供了异步的数据库连接和查询操作,适用于异步编程环境 …...
开开心心带你学习MySQL数据库之第八篇
索引和事务 ~~ 数据库运行的原理知识 面试题 索引 索引(index) > 目录 索引存在的意义,就是为了加快查找速度!!(省略了遍历的过程) 查找速度是快了,但是付出了一定的代价!! 1.需要付出额外的空间代价来保存索引数据 2.索引可能会拖慢新增,删除,修改的速度 ~~ …...
yml配置动态数据源(数据库@DS)与引起(If you want an embedded database (H2, HSQL or Derby))类问题
1:yml 配置 spring:datasource:dynamic:datasource:master:url: jdbc:mysql://192.168.11.50:3306/dsdd?characterEncodingUTF-8&useUnicodetrue&useSSLfalse&tinyInt1isBitfalse&allowPublicKeyRetrievaltrue&serverTimezoneUTCusername: ro…...
yolov5运行过程遇到的小问题(随时更新)
1.关于git的问题 解决办法:插入下面代码 import os os.environ["GIT_PYTHON_REFRESH"] "quiet"2.页面太小无法完成操作 解决办法: 如果不好使再考虑降低Batch_Size大小或者调整虚拟内存可用硬盘空间大小!(调整虚拟内存…...
使用FabricJS创建Image对象的JSON表示
本篇文章介绍一下如何创建图像的 JSON 表示形式 使用 FabricJS 的对象。我们可以通过创建一个实例来创建一个 Image 对象 织物.图像。由于它是FabricJS的基本元素之一,我们也可以轻松地 通过应用角度、不透明度等属性来自定义它。为了创建 JSON Image 对象的表示&am…...
web vue 项目 Docker化部署
Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段: 构建阶段(Build Stage):…...
【JavaEE】-- HTTP
1. HTTP是什么? HTTP(全称为"超文本传输协议")是一种应用非常广泛的应用层协议,HTTP是基于TCP协议的一种应用层协议。 应用层协议:是计算机网络协议栈中最高层的协议,它定义了运行在不同主机上…...
大型活动交通拥堵治理的视觉算法应用
大型活动下智慧交通的视觉分析应用 一、背景与挑战 大型活动(如演唱会、马拉松赛事、高考中考等)期间,城市交通面临瞬时人流车流激增、传统摄像头模糊、交通拥堵识别滞后等问题。以演唱会为例,暖城商圈曾因观众集中离场导致周边…...
SpringBoot+uniapp 的 Champion 俱乐部微信小程序设计与实现,论文初版实现
摘要 本论文旨在设计并实现基于 SpringBoot 和 uniapp 的 Champion 俱乐部微信小程序,以满足俱乐部线上活动推广、会员管理、社交互动等需求。通过 SpringBoot 搭建后端服务,提供稳定高效的数据处理与业务逻辑支持;利用 uniapp 实现跨平台前…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...
【Java学习笔记】BigInteger 和 BigDecimal 类
BigInteger 和 BigDecimal 类 二者共有的常见方法 方法功能add加subtract减multiply乘divide除 注意点:传参类型必须是类对象 一、BigInteger 1. 作用:适合保存比较大的整型数 2. 使用说明 创建BigInteger对象 传入字符串 3. 代码示例 import j…...
七、数据库的完整性
七、数据库的完整性 主要内容 7.1 数据库的完整性概述 7.2 实体完整性 7.3 参照完整性 7.4 用户定义的完整性 7.5 触发器 7.6 SQL Server中数据库完整性的实现 7.7 小结 7.1 数据库的完整性概述 数据库完整性的含义 正确性 指数据的合法性 有效性 指数据是否属于所定…...
【Nginx】使用 Nginx+Lua 实现基于 IP 的访问频率限制
使用 NginxLua 实现基于 IP 的访问频率限制 在高并发场景下,限制某个 IP 的访问频率是非常重要的,可以有效防止恶意攻击或错误配置导致的服务宕机。以下是一个详细的实现方案,使用 Nginx 和 Lua 脚本结合 Redis 来实现基于 IP 的访问频率限制…...
Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement
Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement 1. LAB环境2. L2公告策略2.1 部署Death Star2.2 访问服务2.3 部署L2公告策略2.4 服务宣告 3. 可视化 ARP 流量3.1 部署新服务3.2 准备可视化3.3 再次请求 4. 自动IPAM4.1 IPAM Pool4.2 …...
git: early EOF
macOS报错: Initialized empty Git repository in /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/.git/ remote: Enumerating objects: 2691797, done. remote: Counting objects: 100% (1760/1760), done. remote: Compressing objects: 100% (636/636…...
