【C++笔记】C++string类模拟实现

news2024/10/7 4:29:27

【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()。

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

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

相关文章

【7z密码】如何给7z压缩包加密、解密?

7z压缩包是压缩率最大的格式&#xff0c;也有很多朋友会使用7z格式&#xff0c;那么7z压缩包如何进行加密、解密&#xff1f;今天给大家介绍详细教程。 7-zip加密 右键文件选择7-zip打开压缩软件进行压缩或者在打开7-zip软件找到需要压缩的文件&#xff0c;点击添加&#xff…

【大数据】基于 Flink CDC 高效构建入湖通道

基于 Flink CDC 高效构建入湖通道 1.Flink CDC 核心技术解析2.CDC 数据入湖入仓的挑战2.1 CDC 数据入湖架构2.2 CDC 数据 ETL 架构 3.基于 Flink CDC 的入湖入仓方案3.1 Flink CDC 入湖入仓架构3.2 Flink CDC ETL 分析3.3 存储友好的写入设计3.4 Flink CDC 实现异构数据源集成3…

ECharts数据可视化项目

Echarts 可视化面板介绍01-使用技术02- 案例适配方案03-基础设置04-header 布局05-mainbox 主体模块06-公共面板模块 panel07-柱形图 bar 模块(布局)08-中间布局09-Echarts-介绍10-Echarts-体验11-Echarts-基础配置12- 柱状图图表&#xff08;两大步骤&#xff09;13-柱状图2定…

基于Java+SpringBoot+Vue前后端分离交通管理在线服务系统设计和实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

地形有通挂支隘险远六种情况

地形有通、挂、支、隘、险、远六种情况 【安志强趣讲《孙子兵法》第34讲】 第十一篇&#xff1a;地形篇 【全文大白话】 地形有各种情况&#xff0c;行军有各种情况&#xff0c;用好地形获得交战的主动权。 【原文】 孙子曰&#xff1a;地形有通者&#xff0c;有挂者&#xff0…

百家号创业项目:小白即可日入30到100,寻找爆款文案技术教程免费分享

百家号项目&#xff1a;月入1K-3K不费力&#xff0c;寻找爆款文案的秘诀&#xff01; 当下&#xff0c;有一个备受关注的项目——百家号&#xff0c;许多人以 300-800 元不等的价格提供培训&#xff0c;声称每个号每月可以轻松赚取1K-3K。 然而&#xff0c;据我个人测试&…

容器编排学习(一)容器技术

一 容器 1 Linux 容器的起源 容器的起源可以追溯到1979年 UNIX 系统中提供的chroot命令&#xff0c;容器的最初的设计目标是为了隔离计算机中的各类资源以便降低软件开发、测试阶段的风险&#xff0c;或者充当蜜罐&#xff0c;吸引黑客的攻击&#xff0c;以便监视黑客的行为…

Logstash--logstash-syslog-putput插件安装及使用

这篇文章讲的是如何在Linux服务器上安装logstash-syslog-output插件及使用&#xff0c;是集网上之大成&#xff0c;择选出一条正确有效简短的路。 安装 插件logstash-syslog-output&#xff0c;如果你的logstash没有&#xff0c;则需要安装。 查看logstash是否含有这个插件的…

代码生成商业化一些思考

代码生成解决方案 生成项目代码有3大类的解决思路&#xff1a; 1.从底到上的生成&#xff0c;部分代码生成生成一行代码或者一个方法种一小块代码生成&#xff0c;ide插件代码生成基本这种思路 2.大语言模型作为软件工程不同角色agent&#xff0c;用户给出idea每个agent自动…

vue3:16、Pinia的基本语法

选项式APi 组合式API src/store/counter.js import { defineStore } from "pinia"; import { computed, ref } from "vue";export const userCounterStore defineStore("counter",()>{//声明数据 state - countconst count ref(100)//声…

ATTENTION, LEARN TO SOLVE ROUTING PROBLEMS!

ATTENTION, LEARN TO SOLVE ROUTING PROBLEMS! 1、背景2、基于注意力层的模型4、解码器 1、背景 本篇论文基于Transformer模型提出了一个基于注意力层的模型&#xff0c;并采用REINFORCE方法训练模型&#xff0c;来求解以下几种组合优化问题&#xff1a; 旅行商问题(Travellin…

Docker 的分层文件系统

1 分层文件系统 UnionFS 联合文件系统 bootfs&#xff1a;boot file systemrootfs&#xff1a;root file system 分层文件系统 Docker镜像都是只读的&#xff0c;当容器启动时&#xff0c;一个新的可写层被加到镜像的顶部&#xff0c;这一层就是我们通常说的容器层&#xf…

Redis缓存预热、缓存雪崩、缓存击穿、缓存穿透

文章目录 Redis缓存预热、缓存雪崩、缓存击穿、缓存穿透一、缓存预热1、问题排查2、解决方案&#xff08;1&#xff09;准备工作&#xff08;2&#xff09;实施&#xff08;3&#xff09;总结 二、缓存雪崩1、解决方案 三、缓存击穿1、解决方案&#xff08;1&#xff09;互斥锁…

大语言模型之八-提示工程(Prompt engineer)

大语言模型的效果好&#xff0c;很大程度上归功于算力和数据集&#xff0c;算力使得模型可以足够大&#xff0c;大到模型的理解、记忆、推理和生成以及泛化能力可以同时兼备&#xff0c;而数据集则是模型学习的来源。 LLM中的prompt方法主要用于fine-tune阶段&#xff0c;即在…

DIM层维度表学习之用户维度表分析

1.用户维度表的模型 DROP TABLE IF EXISTS dim_user_zip; CREATE EXTERNAL TABLE dim_user_zip (id STRING COMMENT 用户ID,name STRING COMMENT 用户姓名,phone_num STRING COMMENT 手机号码,email STRING COMMENT 邮箱,user_level STRING COM…

ArcGIS Pro3.0.2保姆级安装教程

软件简介&#xff1a; ArcGIS Pro是ERSI推出的新一代原生态64位ArcGIS桌面产品。具备强大的二维、三维一体化功能&#xff0c;继承了传统桌面产品ArcMap等产品几乎所有的功能&#xff0c;并在多个方面作了进一步的优化和改进&#xff0c;是云端一体化、数据科学与空间数据科学…

深入理解 JVM 之——字节码指令与执行引擎

更好的阅读体验 \huge{\color{red}{更好的阅读体验}} 更好的阅读体验 类文件结构 Write Once&#xff0c;Run Anywhere 对于 C 语言从程序到运行需要经过编译的过程&#xff0c;只有经历了编译后&#xff0c;我们所编写的代码才能够翻译为机器可以直接运行的二进制代码&#x…

Java“牵手”淘宝商品列表数据,关键词搜索淘宝商品数据接口,淘宝API申请指南

淘宝商城是一个网上购物平台&#xff0c;售卖各类商品&#xff0c;包括服装、鞋类、家居用品、美妆产品、电子产品等。要获取淘宝商品列表和商品详情页面数据&#xff0c;您可以通过开放平台的接口或者直接访问淘宝商城的网页来获取商品详情信息。以下是两种常用方法的介绍&…

面试求职-简历编写技巧

没有高水平简历 只有高匹配的简历 试问一下&#xff1a;如果一个非常牛逼的软件工程的硕士&#xff0c;投递市场营销岗位&#xff0c;结果会是什么样呢&#xff1f; 这位同学大概率没办法通过简历。 不是因为他不够优秀&#xff0c;而是因为简历和岗位不够匹配。 在公司的招…

第20章 原子操作实验(iTOP-RK3568开发板驱动开发指南 )

在上一章节的实验中&#xff0c;对并发与竞争进行了实验&#xff0c;两个app应用程序之间对共享资源的竞争访问引起了数据传输错误&#xff0c;而在Linux内核中&#xff0c;提供了四种处理并发与竞争的常见方法&#xff0c;分别是原子操作、自旋锁、信号量、互斥体&#xff0c;…