STL | string C++底层实现

news2024/9/19 16:13:15

目录

前言

总代码

string结构框架搭建

三个成员

构造

析构

拷贝构造、赋值重载 和 swap

size、c_str、operator[ ]

string迭代器的简单实现

扩容 reserve

insert(插入字符和字符串)

单字符

字符串

push_back、append、+=

erase 删除

find查找

运算符 >、<、>=、<=、==、!=

substr(重点)

operator<<

operator>>

结语


前言

string的实现较为简单,属于奖励课的知识点,但是string的知识点比较冗余,没有后面STL的其他结构那样简洁,很多是日常中不会用到的,所以我们今天就实现一些日常中特别常见且重要的

如果有需求要看所有的函数的话,可以上C++官方的网站上去学习  网址如下:

https://legacy.cplusplus.com/reference/string/string/?kw=string

总代码

如果有仅为复习需要的友友,可以直接点开下方gitee链接,里面是该博客的string实现

C++ string实现 gitee

string结构框架搭建

三个成员

我们先来将string的大体框架搭建一下吧

首先,string这个类我们可以拿一个命名空间包起来,以免我们实现后与库里命名的冲突

我们的string一共有三个成员组成:

  1. char* 类型的指针
  2. size(有效数据个数)
  3. capacity(空间总大小)

第一个指针用来指向string的头,size和capacity这两个可以用来分辨string是否满了,是否需要扩容,如下:

namespace hjx
{
	class string
	{
    private:
	    char* _str;
	    size_t _size;
	    size_t _capacity;
    };
}

构造

值得注意的是,构造函数在这里有一个坑:我们写函数参数的时候,需要给一个缺省值“”

该符号的意思是:因为是双引号,所以系统会认为这是一个字符串(只不过没有内容)

但是会默认往里面放一个 \0,我们需要这个 \0 因为不加的话,后面如果要插入,string会因为找不到\0而报错

另外,我们初始的对象有三个,一个指针,一个size,一个capacity,我们需要传一个char* 的参数来初步构建string,所以我们在初始化时还需要将空间开出来,这里直接new就好

但是我们要new多大的空间呢?

这时,我们可以使用strlen来计算传过来的参数有多大,但是如果三个参数的初始化都用strlen,那么代价就会略大

我们可以只对size使用strlen在初始化列表初始化,后面的两个都在函数体里复用size的大小即可

代码如下:

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

析构

析构倒没有构造那么多弯弯绕绕,我们只需要删除空间,指针置空,size和capacity都变为0即可

代码如下:

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

拷贝构造、赋值重载 和 swap

提及这两个,就不得不使用一下资本家的思想了(滑稽

对于资本家而言,要的就是榨干劳动者的所有价值,并且自己还很轻松

对于拷贝构造,我们可以先创建一个string类,拿参数做初始化,最后将两个类成员里面的指针互换即可

这种方法是很妙的,我们创建了一个类,生命周期是在该函数范围内的,出了函数就调用析构销毁了

但是创建出来的空间是在堆上的,如果不调用析构就不会销毁,我们只需要将一个临时的string构造出来,再让我们要构造的对象的指针指向这块空间,不让他调用析构即可

图示如下:

再举一个形象的例子,有两张银行卡,你和你老板各一张

你赚完了钱之后,你老板如何才能快速获得你的钱?

只需要让你的银行卡变成他的,他的没钱,但是那张卡变成你的,这样你老板就可以获得你的钱了

string(const string& s1)
{
	string tmp(s1._str);
	swap(tmp);
}

赋值重载同理

甚至,我们可以在参数部分就不使用引用,就让编译器复制一份,直接和参数的空间交换即可,如下:

string& operator=(string s1)
{
	swap(s1);
	return *this;
}

这里我们再将我们的swap实现一下

因为库里的swap默认会将空间一个一个交换,我们只需要交换指针即可,如下:

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

另外,为了防止有人不使用对象调用该函数(不使用的话还是会调用到库里的swap)

所以我们还需要在类外面再实现一个版本的swap,如下:

void swap(string& s1, string& s2)
{
	s1.swap(s2);
}

size、c_str、operator[ ]

size:

由于我们的成员中有_size,所以我们直接返回即可,size如下:

size_t size()const
{
	return _size;
}

c_str:

c_str的效果是使用的时候我们能将整个打印出来,所以我们直接返回成员中的指针即可,如下:

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

operator[ ]:

使用这个重载的目的就是为了返回某个位置的值,所以参数就是一个整形代表位置

返回值就用char&,因为[]使用完要返回一个字符类型,如下:

char& operator[](int pos)
{
	assert(pos < _size && pos >= 0);
	return _str[pos];
}
const char& operator[](int pos)const
{
	assert(pos < _size && pos >= 0);
	return _str[pos];
}

此处实现了加const与否的两个版本

string迭代器的简单实现

由于string的本质就是一个字符数组,所以我们不需要另外实现一个类,直接使用typedef即可

由于迭代器的作用就是模拟指针,我们这里的访问直接使用原生指针恰好可以,所以我们可以直接将char*  typedef  为 iterator,const_iterator 同理

接着就是 begin 和 end,我们将这两个实现了之后,底层才会支持范围for(范围for的底层就是迭代器)

begin其实就是将指向头部的指针返回,也就是将我们成员中的指针返回即可

end就是指向尾部的下一个节点的指针,只需要让成员中的指针+size(有效个数)再返回即可

代码如下:

typedef char* iterator;
typedef const char* const_iterator;

iterator begin()
{
	return _str;
}

iterator end()
{
	return _str + _size;
}

const_iterator begin()const
{
	return _str;
}

const_iterator end()const
{
	return _str + _size;
}

扩容 reserve

我们的扩容逻辑相对简单,如果空间满了或者不够,我们就扩容

但是考虑到有一次插入一个字符的,也有一次插入一个字符串的,所以我们需要一个长度作为参数

如果这个参数的大小比我们的capacity都要大,我们就扩容,反之就不需要处理

而到了扩容逻辑里,我们需要先判断一下这个string本来的大小是否就为0

如果为0,就扩为4(这个大小随意,只不过我喜欢扩成4而已),但如果不为0,那么我们就二倍扩容

但是C++中的扩容不想C语言,这里涉及到一个迭代器失效的问题,所以C++的扩容需要自己开新空间,拷贝数据,释放久空间

代码如下:

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

insert(插入字符和字符串)

单字符

insert需要两个参数,pos位置,和要插入的数据,意味pos位置后插入数据

insert的逻辑其实不难,单字符就是将pos位置的数据全部向后移动一格,如果大小会超就扩容

在数据全都向后移动了之后,将数据插入在pos位置(因为数组下标是从0开始的,所以pos位置就是pos的下一个位置)

代码如下:

void insert(size_t pos, const char s)
{
	if (_size == _capacity)
	{
		size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
		reserve(newcapacity);
	}

	for (size_t i = _size + 1; i > pos; i--)
		_str[i] = _str[i - 1];
	_str[pos] = s;
	_size++;
}

这里有一个大坑就是

我们for循环的 i 需要是_size+1,因为如果是_size的话,循环条件就需要是 >= pos

但是如果 >= pos 的话,如果我的pos就等于 _size,那就不会进入循环,在后期写istream的时候会发现这样会死循环,所以还是需要注意一下的

字符串

大体逻辑和插入单字符的一样,但是这里不一样的点在于,我们需要先求出参数(字符串的大小)(假设长度为len),然后再将这个数据和size相加看看是否会超过capacity,如果会超,再扩容

剩下的就是将pos位置的数据全都往后移len个大小,再用memcpy将数据拷贝过去

void insert(size_t pos, const char* s)
{
	size_t len = strlen(s);
	if (len + _size > _capacity)
		reserve(len + _size);

	for (size_t i = _size + len; i > pos + len - 1; i--)
		_str[i] = _str[i - len];

	memcpy(_str + pos, s, len);
	_size += len;
}

push_back、append、+=

由于我们前面实现了insert,所以我们这里的这些函数都可以直接复用我们的insert

push_back就是尾插一个数据

append就是尾插一个字符串

+=就是某位置插入一个字符或一个字符串

+=单加一个字符其实效果就是push_back,加一段字符串就是append,都可以相互复用

代码如下:

void push_back(const char s)
{
	insert(_size, s);
}

void append(const char* s)
{
	insert(_size, s);
}

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

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

erase 删除

删除的主逻辑就是,用后面的数据覆盖掉要删除的部分,再在尾部放上一个 \0(\0 为字符串结尾,后面即使有字符也不会查看到)

但是我们此时还需要使用for循环折磨自己吗(滑稽)

我们在这里可以直接使用 strcpy !!!

因为strcpy会将后面的所有数据都进行拷贝到前面,而memcpy则是指定大小

所以我们可以直接用strcpy将后面的数据拷贝到前面

图示如下:

代码如下:

void erase(size_t pos, size_t len = npos)
{
	if (len > _size - 1 - pos)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		strcpy(_str + pos, _str + pos + len);
		_size -= len;
	}
}

ps:代码中的 len > _size - 1 - pos 代表的是包括pos位置在内的数据都要删除,而下文中有一个substr,那里的是不包括pos位置

find查找

查找单字符的话,我们就直接用一个for循环,从到到尾找一遍,这没什么好说的,代码如下:

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

	return npos;
}

这里返回的npos指代的是整形最大值,意味你这字符串肯定没有这么长

查找字符串的话,我们可以直接使用库里面的strstr函数,我们就不需要自己写了(但其实我们自己写的话使用一个双指针,或者叫滑动窗口就解决了,本质就是整个string找一遍而已,并不是特别重点的知识)

strstr函数是找到字符数组中的相同的字符串,并将相同部分的起始位置返回

但由于库里面的find返回值是一个size_t,所以我们需要将strstr返回的结果(一个指针),与起始位置的指针相减得出的结构返回,意味从起始位置到要找到部分之间隔了多远

代码如下:

size_t find(char* s, size_t pos)
{
	char* p = strstr(_str + pos, s);
	return p - _str;
}

运算符 >、<、>=、<=、==、!=

 这些部分其实就是将 < 和 ==实现出来,然后 != 就是 == 的结果取反,>= 就是 < 的结果取反,其他都类似,由于较为简单这里就不作过多讲解,本人前面也有写过一篇有详细讲解过该内容的博客,如果有需要的可以浏览一下,链接如下:

C++ | 日期类详解

代码如下:

bool operator<(const string& s) const
{
	return strcmp(_str, s._str) < 0;
}

bool operator>(const string& s) const
{
	return !(*this <= s);
}

bool operator<=(const string& s) const
{
	return *this < s || *this == s;
}

bool operator>=(const string& s) const
{
	return !(*this < s);
}

bool operator==(const string& s) const
{
	return strcmp(_str, s._str) == 0;
}

bool operator!=(const string& s) const
{
	return !(*this == s);
}

substr(重点)

substr的主要作用就是:将string里面的一部分内容单独抽出来构造成一个string

返回值也是一个string,参数就是从哪个位置开始(pos),往后多长一段距离的字符串(len)

而我们可以先做一个小判断,如果substr后面的长度比_size - pos(意味不包括pos位置)要长,就意味着pos位置后面的所有数据都要拿来构造成一个新的string并返回,如果不是的话,就走else的情况

如果是走else了,那就只能乖乖的开空间,拷贝数据,注意 \0 不能忘,调整新string的成员size

最后返回(注意,这里使用的拷贝数据的函数是memcpy,因为比起strcpy他有一个长度的参数,可以控制想要拷贝多长的数据)

代码如下:

string substr(size_t pos, size_t len = npos)
{
	if (len > _size - pos)
	{
		string tmp(_str + pos);
		return tmp;
	}
	else
	{
		string tmp;
		tmp.reserve(len + 1);
		memcpy(tmp._str, _str + pos, len);
		tmp._str[len + 1] = '\0';
		tmp._size = len;
		return tmp;
	}
}

operator<<

试想一下,我们打印string类的时候,一般情况下还是会用范围for,但是能不能这样呢:

string s1("hello world");
cout << s1 << endl;
hello world //结果

如果要实现这种效果的话,我们就需要用到我们的ostream

首先我们这个函数是不能在string类里面实现的

因为我们在类里面实现的话,类里面的函数都会默认有一个隐含的this指针,这个this指针会将第一个参数的位置给占掉

但是我们需要的是 <<s1 而不是 s1<<

所以我们只能在类外面实现

而我们这个的大体逻辑就是让一个ostream类型的参数不断地使用<<,里面其实主要的逻辑就是一层for循环,代码如下:

ostream& operator<<(ostream& out, const string& s1)
{
	for (int i = 0; i < s1.size(); i++)
		out << s1[i];
	return out;
}

注意,这里面的返回值是由于我们现实中可能会出现这种情况:cout << s1 << s2 << s3

如果是这种情况的话,我们在调用完后还需要返回一个ostream类型的对象,这样下一个才能打印

operator>>

和operator<<相反,这个代表的作用是cin

而我们这个重载的主要逻辑就是一个一个输入(用istream参数的函数)

当遇到输入的是空格或者换行的时候,就停止

和上面一样的是,我们也需要一个返回值以防止出现 cin >> s1 >> s2 >> s3 这种情况

代码如下:

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

结语

这篇文章到这里,就结束啦 ╰(*°▽°*)╯

如果觉得对你有帮助的话,希望可以多多支持作者喔(〃 ̄︶ ̄)人( ̄︶ ̄〃)

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

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

相关文章

C++计算字符串中大写、小写、数字、空格、其他字符的个数

#include <iostream> #include <array> using namespace std;int main() {cout << "请输入一个字符串:";string str;getline(cin,str);int daxie0,xiaoxie0,num0,space0,other0;int lenstr.size();for(int i0;i<len;i){if(str[i]>A&&…

Oracle认证1Z0-071线上考试注意事项

目录 一、前言二、回顾过往战绩第一次 裸考&#x1f412;第二次 背题库硬考&#xff01;&#x1f412;第三次 软件卡住&#xff0c;寄&#xff01;&#x1f648;第四次 汇总纠错&#xff0c;通过&#xff01;&#x1f31a; 三、考试流程四、考试注意事项1. 是否需要科学上网2. …

vue(vue2和vue3)项目打包去除console.log

1.Vue2去除 module.exports { configureWebpack: (config) > {// 取消console打印config.optimization.minimizer[0].options.terserOptions.compress.drop_console truereturn {name: "项目名称",resolve: {alias: {"": resolve("src")}}…

【八股文】MySQL

1.char 和 varchar的区别 char是定长的&#xff0c;varchar是可变的字符串char适合存长度差不多的或者较短的&#xff0c;例如手机号&#xff0c;身份证&#xff0c;MD4加密算法。varchar用来存备注信息&#xff0c;用户昵称等不确定长度的信息。 2.Decimal、double和float的区…

Mybatis学习-day18

Mybatis学习-day18 数据持久化是将内存中的数据模型转换为存储模型&#xff0c;以及将存储模型转换为内存中数据模型的统称。例如&#xff0c;文件的存储、数据的读取以及对数据表的增删改查等都是数据持久化操作。 MyBatis 支持定制化 SQL、存储过程以及高级映射&#xff0c…

Java | Leetcode Java题解之第324题摆动排序II

题目&#xff1a; 题解&#xff1a; class Solution {Random random new Random();public void wiggleSort(int[] nums) {int n nums.length;int x (n 1) / 2;int mid x - 1;int target findKthLargest(nums, n - mid);for (int k 0, i 0, j n - 1; k < j; k) {if…

4章3节:缺失值的处理(上)

在医学科研中&#xff0c;由于失访、无应答或记录不清等各种原因&#xff0c;经常会遇到数据缺失的问题。本文将深入探讨医学科研中数据缺失的成因、分类、影响以及应对方法&#xff0c;结合R语言的实际应用&#xff0c;为医学研究人员提供全面的解决方案。 一、认识缺失数据 …

一款开源且免费的系统清理工具,绿色免安装

BleachBit是一款开源且免费的系统清理工具&#xff0c;最初设计用于Linux系统&#xff0c;但现在已经支持Windows。该工具的主要功能包括清理缓存、删除临时文件、清除浏览器历史记录、删除cookies和日志文件等。此外&#xff0c;它还能够安全地擦除文件内容&#xff0c;确保数…

【Java数据结构】---泛型

乐观学习&#xff0c;乐观生活&#xff0c;才能不断前进啊&#xff01;&#xff01;&#xff01; 我的主页&#xff1a;optimistic_chen 我的专栏&#xff1a;c语言 &#xff0c;Java 欢迎大家访问~ 创作不易&#xff0c;大佬们点赞鼓励下吧~ 文章目录 包装类装箱和拆箱泛型泛型…

HarmonyOS应用开发知识地图

HarmonyOS 应用开发旅程 HarmonyOS 应用开发旅程 PS&#xff1a;Xmind原文件可以直接跳转官方具体文档地址&#xff0c;如需要原文件请联系&#xff1a;DYZZ198 01.准备与学习 学习 HarmonyOS 的基本概念和架构,搭建好所需的开发工具和环境,了解开发规范和最佳实践 了解 H…

C语言菜鸟入门·数据结构·链表超详细解析

目录 1. 单链表 1.1 什么是单链表 1.1.1 不带头节点的单链表 1.1.2 带头结点的单链表 1.2 单链表的插入 1.2.1 按位序插入 &#xff08;1&#xff09;带头结点 &#xff08;2&#xff09;不带头结点 1.2.2 指定结点的后插操作 1.2.3 指定结点的前插操作 1.3 …

【HarmonyOS NEXT星河版开发学习】小型测试案例04-个人中心顶部导航

个人主页→VON 收录专栏→鸿蒙开发小型案例总结​​​​​ 基础语法部分会发布于github 和 gitee上面&#xff08;暂未发布&#xff09; 前言 主轴对齐方式在鸿蒙开发中非常重要&#xff0c;通过合理选择 justifyContent 和 alignItems 属性&#xff0c;开发者可以精确控制 Fle…

度言软件介绍

度言软件管理员操作后台 https://www.duyansoft.com企业后台为公司管理员操作后台&#xff0c;共计有七个功能版块 控制台 成员管理——员工管理 成员管理——员工管理&#xff08;添加员工&#xff09; 成员管理——团队管理 公司管理员可以新建/编辑/删除团队&#xff0c…

【Web开发手礼】探索Web开发的秘密(十五)-Vue2(2)AJAX、前后端分离、前端工程化

主要介绍了AJAX、前后端分离所需的YApi、前端工程化所需要的环境安装&#xff01;&#xff01;&#xff01; 目录 前言 AJAX ​原生Ajax Axios Axios入门 案例 前后端分离开发 YApi ​前端工程化 环境准备 总结 前言 主要介绍了AJAX、前后端分离所需的YApi、前端工…

26集 ESP32 AIchat启动代码分析-《MCU嵌入式AI开发笔记》

26集 ESP32 AIchat启动代码分析-《MCU嵌入式AI开发笔记》 这集我们分析代码如何组织起来&#xff0c;如何编译 先用sourceinsight把代码加进工程。 新建一个sourceinsight工程&#xff0c;把AI-CHAT代码加进来&#xff0c;之后把ESP IDF代码加进来&#xff0c;之后把ESP-ADF加…

android compose设置圆角不起作用

进度条progress设置背景圆角不起作用&#xff1a; 源码&#xff1a; Composablefun CircularProgress(modifier: Modifier, vm: TabarCmpViewModel?) {if (vm?.showLoading?.value ! true) returnBox(modifier modifier.background(Color(0x99000000)).defaultMinSize(minW…

【kubernetes】亲和力(Affinity)

亲和力&#xff08;Affinity&#xff09; 针对节点(NodeAffinity) 1&#xff0c;RequiredDuringSchedulinglgnoredDuringExecution 硬亲和力&#xff0c;即支持必须部署在指定的节点上&#xff0c;也支持必须不部署在指定的节点上。 2&#xff0c;PreferredDuringSchedulin…

【2024最新华为OD-C/D卷试题汇总】[支持在线评测] 全排列(100分) - 三语言AC题解(Python/Java/Cpp)

🍭 大家好这里是清隆学长 ,一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-C/D卷的三语言AC题解 💻 ACM金牌🏅️团队| 多次AK大厂笔试 | 编程一对一辅导 👏 感谢大家的订阅➕ 和 喜欢💗 🍿 最新华为OD机试D卷目录,全、新、准,题目覆盖率达 95% 以上,支持题…

【TS】使用npm全局安装typescript

查看npm安装 npm -v 安装typescript npm i -g typescript 查看安装 tsc 这就是标致着安装完成。

Linux定时任务之crontab

目录 crontab简介crontab语法自定义定时任务举例1、每天中午12点执行命令&#xff1a;2、每5分钟执行一次命令&#xff1a;3、在每月的第一天和第十五天的00:00执行命令&#xff1a;4、在周一到周五的上午 8 点到 10 点之间&#xff0c;每半小时执行一次命令&#xff1a; 使用 …