C++6:STL-模拟实现string

news2024/11/27 10:42:16

 string时STL中的模板库之一,类似于专门处理字符串的数据结构,在模拟实现并探讨其中构造的巧妙之处之前,我们短浅的认识一下STL是什么

目录

 什么是STL

STL的诞生

关于string

string的模拟实现

构造函数和析构函数

实现简单的string打印

 拷贝构造函数

摇人打工法实现拷贝构造

 赋值操作符重载

resize和reserve

reserve

resize

 push_back

 append追加

+=操作符重载

insert

erase

find

<<和>>

<<

>>

string的迭代器


 什么是STL

 STL的全名:standard template libaray-标准模板库

STL的诞生

出自于惠普实验室的两位大牛,Alexander Stepanov,Meng Lee,奔着开源精神,两位大牛表示STL的源码可以随意传播修改乃至商业用途,但是要求必须和源码一样开源,这就是STL的始祖版本

STL的后序版本:P. J. 版本,RW版本,SGI版本,各类版本的区别以及优缺点就不做记述了,毕竟我们作为使用者不仅是学习使用,学习其范式编程的思维也是重点。


关于string

STL其实在开发过程中更像一个非常方便的工具包,相较于什么数据结构都需要自己实现的C语言来说,STL支持了不少的数据结构,只需要包含几个头文件,我们就能非常便利的使用,比方说我们接下来需要实现的string

就平常来说,我们在C语言阶段操作字符串是比较头疼的一个过程,而使用STL,则非常的方便。

比如,我希望简单的打印一个字符串,并且将其与另一个字符串链接在一块。

 使用string,不仅不需要再写一个for循环来遍历字符串打印,甚至也能利用+=的操作实现字符串之间的相加!可以说是非常方便了,不过知其然不知其所以然就没啥意思了,毕竟单纯的使用这种事的门槛非常的低,我们还是需要了解其原理才能有所收获。


string的接口记述以及趣闻分享

string作为其中的一个模板库,由于创建的时间是有一阵子了,其中包含了许多冗余的接口,当然还有各式各样有用的接口,需要查阅的话可以跳转至这个网站:https://cplusplus.com/reference/

 当然string也是有更多有趣的事情,陈皓前辈就讲了这个问题:STL 的string类怎么啦?_haoel的博客-CSDN博客

作为一个方便使用者开发的优秀代码集大成者,STL的重要性以及其他的特点我相信其他文章的作者一定写的比我更好,我也就不多说了。

string的模拟实现

基本的成员变量

class mystring
{
   public:
    private:
		char* _str;
		size_t _size;
		size_t _capacity;
}

模拟实现string类就逃不开最基本的几个成员函数了,毕竟我们需要在堆上开辟空间,所以我们要自己实现构造函数

构造函数和析构函数

 构造函数其实和顺序表差不多,但是有了new之后整个代码也变短了不少

		
	//构造函数,字符串初始化版本,求长度,算容量,开空间
	mystring( const	char* str="")
	{
		_size = strlen(str);
		_str = new char[_size + 1];//留一个给斜杠零

			
		strcpy(_str, str);//拷贝数据到新空间
		_capacity = _size;
	}

析构函数,对应格式delete掉即可

//析构函数
~mystring()
{
	delete[] _str;
    _str = nullptr;
	_size = _capacity = 0;
}

实现简单的string打印

我们在使用STL的sting的时候打印非常方便,string直接适配了cout,但是目前我们只需要简单的打印功能,重载流插入和流提取操作符的具体操作后面再一块说,我们先简单的实现一个。

首先,我们需要解引用整个字符串,然后打印出来就好了,由于string的本身是一个字符串数组,其本身就支持【】解引用操作

void Print()
	{
		for (int i = 0; i < _size; ++i)
		{
				cout << _str[i];
		}
	}

然后往里面塞一个字符串看看效果

 当然我们还需要支持随机访问,也就是支持str的【】解引用功能,那么很简单,我们直接重载【】操作符

重载没什么难度,不过不要忘记多实现一个const版本。

char& operator [](size_t a)
{
	assert(a < _size);
	return _str[a];
}

const char& operator[](size_t index)const
{
	assert(index < _size);
	return _str[index];
}

关于const版本的成员函数,我打算在这里复盘一把,为什么要实现const版本的成员函数?

const修饰成员函数 

const修饰的成员函数,其本质上修饰的并不是这个函数,而是当前这个类对象成员函数中的那个this指针

拿上文的const版本的成员函数举例,我们看到的是

const char& operator[](size_t index)const

而实际上,真实的样子是

const char& operator[](const mystring *const this  ,size_t index)

const修饰了当前的This指针,this指针本身携带一个const,本身自带的这个const是用来保护this自己的,但是并不会保护this指针所指向的对象,而const成员函数则是保护所指向对象的。

实现const版本的成员函数,可以保证const类型的类对象能成功调用成员函数。

如果没有,const类对象就用不成了。

但是const版本的成员函数也不是都要实现,只有当满足以下条件时,实现才有必要。

1.任何不会修改成员的函数都应该声明为const类型。

2.const成员函数不可以修改对象的数据。

 拷贝构造函数

 拷贝构造函数需要传引用,为了得到原生的字符串头指针来使用strcpy,我们还需要额外实现一个c_str

mystring(const mystring& str )
{
	_size = str._size;
	_str = new char[str._capacity+1];
	_capacity = str._capacity;

	strcpy(_str, str.c_str());

}

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

 这种方法比较朴实无华,我们实现一个比较”现代“的写法,摇人打工

摇人打工法实现拷贝构造

//拷贝构造现代写法,摇人打工
mystring(mystring& str)
	:_str(nullptr), _size(0), _capacity(0)
	{
		mystring tmp(str._str);
		swap(str);

	}

我们利用构造函数创建一个临时变量tmp,然后将tmp与当前的this交换,为了防止tmp析构的时候报错,我们给一个空指针到this,delete一个nullptr不会报错。

当然swap我们也要重写一个,算法库内部的swap效率较低。

 题外话:T c(a);  这个看上去是个构造函数啊,是不是内置类型就不能用了?
 

C++在产生模板之后,为了能照顾到内置类型,为内置类型也提供了类似构造函数的初始化方法。

比如:

int i(10);
int j = int();

 所以这种调用构造函数的方法对内置类型也是生效的,不必担心

		void swap(mystring& str)
		{
			std::swap(_str, str._str);
			std::swap(_size, str._size);
			std::swap(_capacity, str._capacity);

		}

 赋值操作符重载

 赋值,传入一个字符串,把当前的sting置换成传入的字符串
与构造函数类似,但是需要注意连续赋值的问题,str1 = str2 = str3
还有刁钻情况,自己给自己赋值。

mystring& operator=(const mystring& s)
{
	if (this != &s)
	{
		char* tmp = new char[s._capacity + 1];
		strcpy(tmp, s._str);

		delete[] _str;

		_str = tmp;

		_size = s._size;
		_capacity = s._capacity;
	}
	return *this;
}

resize和reserve

 resize和reserve这两个接口函数在控制sting的空间上还算是挺有用的,接下来就来实现一下

reserve

reserve的功能,我们查阅原码,简单来说实现的功能就是:重置容量,但是我们还是需要注意缩容的问题。也就是传入的参数大小小于当前的容量时,我们不能执行,不然就缩容了。

	void reserve(size_t n = 0)
	{
		if (n > _capacity)
		{
            //新空间,之所以是n+1则是为了斜杠零而留出的空间
			char* tmp = new char [n + 1];

            //拷贝
			strcpy(tmp, _str);

            //更新容量
			_capacity = n;
            
            //销毁原空间
			delete[]_str;
			_str = tmp;
		}
	}

resize

阅读原码的解释,resize需要传递两个参数,一个是更新size的值,另一个则是当N大于当前size值时,我们可以选择是否往空出来的那一部分填充字符。那么在这里我们直接给个缺省值‘\0’

	void resize(size_t  n, char c = '\0')
	{
		if (n > _size)
		{
			reserve(n);
			for (size_t i = _size; i < n; ++i)
			{
				_str[i] = c;
			}
			_size = n;
			_str[_size] = '\0';

		}
		else
		{
			_size = n;
			_str[_size] = '\0';
		}
	}

 push_back

 push_back 这个函数我们都非常熟悉了,不过这里面还是有一些细节需要处理

size的位置被strcpy直接覆盖掉了,我们不仅是扩容size自增,还需要在加上一个'\0'

void push_back(char c)
	{
		//尾插,如满,扩容
		if (_size == _capacity)
		{
			int newcapacity = _capacity == 0 ? 4 : _capacity * 2;

			reserve(newcapacity);
		}

		_str[_size] = c;
		++_size;

		//strcpy从\0处开始覆盖,需要在后面加上\0
		_str[_size] = '\0';

	}

 append追加

 这个函数用于在当前的string后面追加一个字符串,有多个重载·版本,在这里就实现最简单的,追加一个字符串


		//字符串追加
		void append(const char* str)
		{
			size_t  size = strlen(str);

			if (_size + size > _capacity)
			{
				reserve(_capacity + size);
			}
			strcpy(_str + _size, str);

			//只是重置了容量大小,size还没有更新。
			_size += size;


		}

+=操作符重载

实现过了append,+=就非常简单了,直接复用即可

		mystring& operator+= (const mystring& str)
		{
			append(str.c_str());

			return *this;
		}

insert

string& insert(size_t pos, const char* str)
{

	assert(pos <= _size);

	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}

	size_t end = _size+len ;
	while (end > pos + len-1)
	{
		_str[end] = _str[end -len];
		--end;
	}
	//用strncpy是因为直接strcpy会把\0也拷过去,读的时候就直接断层了,用strncpy的话可以规避掉\0
	strncpy(_str + pos, str,len);
	_size += len;


	return *this;
}

erase

//删除
//给个npos的缺省值,当没有传递需要删除的个数的时候全部删除
//还需要额外处理,当len<size- pos的时候正常删除,如果后面还有剩余的字符串,直接挪动过去覆盖就好,没有的话就
//在POS位置放一个\0
string& erase(size_t pos, size_t len = npos)
{
	assert(pos < _size);
	if (len < _size - pos)
	{
		strcpy(_str + pos, _str + pos + len);
		_size -= len;
	}
	else if (len == npos || len >=_size + pos)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	return *this;
}

find

		size_t find(const char* str, size_t pos = 0) const
		{
			assert(pos < _size);

			char* ptr = strstr(_str+pos, str);

			if (ptr != nullptr)
			{
				return ptr - _str;
			}
			else
			{
				return npos;
			}

		}

<<和>>

<<

对于流插入和流提取的问题,我们有一个前置条件,这两个重载不能称为成员函数,因为this指针会抢占第一个操作符的位置让我们使用起来非常难受。

那么我们将其写成全局函数。

这里有一个小问题,我们在类和对象的阶段所接触的<<由于访问限定符的限制原因,我们将其写成了友元成员函数,那么<<的重载函数一定要是友元么?

显然不是,不能直接访问你的成员变量,我可以写一个函数间接的去访问。

	ostream& operator<<(ostream& out,const mystring& str)
	{
		for (size_t i = 0; i < str.size(); ++i)
		{
			out << str[i];
		}
		return out;
	}

返回值使用ostream做返回值的对象,用来支持连续cout等操作

>>

提取的函数也不算困难,需要注意的是缓冲区的问题

我们先简单的实现一下,利用一下istream里面的get函数来获取当前缓冲区内部的字符,get的行为很像gets,不过我们还是在C和C++之间做出区别较好。

	istream& operator>>(istream& in, mystring& str)
	{
		char ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			str += ch;
			ch = in.get();
		}

		return in;
	}

目前的版本仅能实现拿取空格和换行符之间的字符

 当然要实现一次拿完也可以,修改条件即可

 不过库里的实现就是这样的,我们跟着库来实现。不过这里有一些效率上的问题,应对短小的字符串+=的操作还算可行,可一旦字符串变长了,效率就下来了。

那我们可以分批次处理。

	istream& operator>>(istream& in, mystring& str)
	{
		str.clear();

		char ch = in.get();
		char buff[128] = { '\0' };
		size_t i = 0;
		while (ch != ' ' && ch != '\n')
		{
			//已满,+=数据
			if (i == 127)
			{
				str += buff;			
				//i置零
				i = 0;
			}

			buff[i++] = ch;
			ch = in.get();

		}

		//已跳出循环,遇到'\n'或' '已取完当前缓冲区分割区域内的数据,
		//由于上逻辑为满127才+=数据,那么一定会有剩余情况发生,在这里额外处理

		if (i > 0)
		{
			buff[i] = '\0';
			str += buff;
		}

		return in;
	}

string的迭代器

 我们知道迭代器的使用方法类似指针,那么我们可以简单的实现一下。

	//迭代器:
	
	typedef char* iterator;

	iterator begin()
	{
		return _str;
	}
		
	iterator end()
	{
		return _str + _size;
	}

 


到这,一个具有基本功能的stirng就模拟实现完成了

感谢阅读,希望对你有点帮助

 

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

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

相关文章

【蓝桥杯】简单数论2——快速幂矩阵快速幂

1、快速幂 1.1运算模 定义&#xff1a;模运算为a除以m的余数&#xff0c;记为a mod m&#xff0c;有a mod m a % m。 模运算是大数运算中的常用操作&#xff1a;如果一个数太大&#xff0c;无法直接输出&#xff0c;或者不需要直接输出&#xff0c;可以把它取模后&#xff0…

Android 深入系统完全讲解(37)

7.5 源码讲解 dlopen 打开动态库 dlsym 找到符号 (*print_func)(); 调用方法 我们可以看到&#xff0c;要使用一个 so 库的某个方法&#xff0c;就上面三步骤&#xff0c;加载 &#xff0c;查找 &#xff0c;使用 。我 们这里调用了 so 库中的 my_print 方法。 7.6 运行 我们把…

Linux——进程间通信

文章目录前言1. 进程间通信方式的一些标准&#xff1a;2. 管道2.1 什么是管道2.2 管道的原理2.3 匿名管道2.3.1 实例代码1. demo代码2. 总结管道的特点&#xff0c;理解以前的管道 |3. 扩展——进程池2.4 管道读写规则2.5 命名管道2.5.1 创建一个命名管道2.5.2 命名管道的打开规…

Python break用法详解

我们知道&#xff0c;在执行 while 循环或者 for 循环时&#xff0c;只要循环条件满足&#xff0c;程序将会一直执行循环体&#xff0c;不停地转圈。但在某些场景&#xff0c;我们可能希望在循环结束前就强制结束循环&#xff0c;Python 提供了 2 种强制离开当前循环体的办法&a…

路由处理及功能(实现了权限控制vue admin)

界面简化 将 template 改为&#xff1a; <template><div class"login-container"><el-formref"loginForm":model"loginForm":rules"loginRules"class"login-form"autocomplete"on"label-positio…

Mybatis遇到的脑残问题

一、MySQL的版本问题 有的教程mysql是8.0版本使用jar包不一样 <dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0</version></dependency>然后我查了一下我的mysql版本是5.7版…

分支语句与循环语句

文章目录 什么是语句&#xff1f; 分支语句&#xff08;选择结构&#xff09;循环语句goto语句前言 一、什么是语句&#xff1f; C语句可分为以下五类&#xff1a; 1. 表达式语句 2. 函数调用语句 3. 控制语句 4. 复合语句 5. 空语句 控制语句用于控制程序的执行流程&#xff0…

第九层(1):初识STL

文章目录前情回顾初识STLSTL的诞生STL的基本概念STL六大组件STL中的容器、算法、迭代器容器算法迭代器容器、算法、迭代器的配合使用vector中的嵌套使用石碑倒下...后面还有石碑&#xff1f;本章知识点&#xff08;图片形式&#xff09;&#x1f389;welcome&#x1f389; ✒️…

为什么带NOLOCK的查询语句还会造成阻塞

背景客户反映HIS数据库在11点出现了长时间的阻塞&#xff0c;直到手动KILL掉阻塞的源头。请我们协助分析原因&#xff0c;最终定位到.NET程序中使用的SqlDataReader未正常关闭导致。现象登录SQL专家云&#xff0c;进入趋势分析&#xff0c;在活动会话中回溯11点一个小时内的运行…

【Ajax】防抖和节流

一、防抖什么是防抖防抖策略&#xff08;debounce&#xff09;是当事件被触发后&#xff0c;延迟 n 秒后再执行回调&#xff0c;如果在这 n 秒内事件又被触发&#xff0c;则重新计时。如果事件被频繁触发&#xff0c;防抖能保证只有最有一次触发生效&#xff01;前面 N 多次的触…

【Linux IO】文件描述符、重定向、缓冲区

1.open函数1.1第二个参数的解释&#xff1b;O_RDONLY: 只读打开 O_WRONLY: 只写打开 O_RDWR : 读&#xff0c;写打开上面三个常量&#xff0c;必须指定一个且只能指定一个 O_CREAT : 若文件不存在&#xff0c;则创建它。需要使用mode选项&#xff0c;来指明新文件的访问权限 O_…

MyBatis 连接数据库与增删改查

❤️作者主页&#xff1a;微凉秋意 ✅作者简介&#xff1a;后端领域优质创作者&#x1f3c6;&#xff0c;CSDN内容合伙人&#x1f3c6;&#xff0c;阿里云专家博主&#x1f3c6; ✨精品专栏&#xff1a;数据结构与课程设计 &#x1f525;系列专栏&#xff1a;javaweb 文章目录前…

C++设计模式(8)——命令模式

命令模式 亦称&#xff1a;动作、事务、Action、Transaction、Command 意图 命令模式是一种行为设计模式&#xff0c; 它可将请求转换为一个包含与请求相关的所有信息的独立对象。 该转换让你能根据不同的请求将方法参数化、 延迟请求执行或将其放入队列中&#xff0c; 且能…

linux基本功系列之-lsattr命令实战

文章目录一. lsattr命令实战二. 语法格式及常用选项三. 参考案例3.1 查看指定文件上的隐藏属性&#xff1a;3.2 查看目录的隐藏属性3.3 查看目录中全部文件的隐藏属性总结前言&#x1f680;&#x1f680;&#x1f680; 想要学好Linux&#xff0c;命令是基本功&#xff0c;企业中…

英语学习打卡day4

2023.1.24 1.out of curiosity 出于好奇 out of necessity 出于必要 out of interest 出于利益 out of sympathy 出于同情 out of respect 出于尊敬 out of’ fear 出于害怕 out of desperation 出于不得已/绝望 2.ashore adv.向(或在)岸上;上岸 a在… …的 shore岸- >在…

Java 23种设计模式(5.结构型模式-代理模式)

结构型模式 代理模式 结构型模式描述如何将类或对象按某种布局组成更大的结构。 它分为类结构型模式和对象结构型模式&#xff0c;前者采用继承机制来组织接口和类&#xff0c;后者釆用组合或聚合来组合对象。 由于组合关系或聚合关系比继承关系耦合度低&#xff0c;满足“合成…

分享127个ASP源码,总有一款适合您

ASP源码 分享127个ASP源码&#xff0c;总有一款适合您 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff0c; 127个ASP源码下载链接&#xff1a;https://pan.baidu.com/s/1JpOFmxkovbScxmz0_MhUWg?pwd758t 提取码&#x…

算法:一维与二维最大连续子序列和(子矩阵和,c++实现 动态规划)

文章目录一维最大连续子序列和代码示例二维最大连续子序列和、代码示例一维最大连续子序列和 给你一个序列 【-1&#xff0c;-2&#xff0c;3&#xff0c;6&#xff0c;4&#xff0c;-9】的最大的连续的子序列和的值。 什么是最大连续子序列和&#xff0c;首先要满足两个条件…

Java基础 Stream流方法引用异常

Stream流 引例 需求&#xff1a;按照下面要求完成集合的创建和遍历 创建一个集合&#xff0c;存储多个字符串元素 1. 把所有以“曹”开头的元素存储到新集合中 2. 把曹开头&#xff0c;长度为3的元素存储到新集合中 List<String> list List.of("曹操", "…

19.2、Javaweb案例_Servlet代码抽取优化分页数据redis缓存优化分页数据展示

优化Servlet 目的 减少Servlet的数量&#xff0c;现在是一个功能一个Servlet&#xff0c;将其优化为一个模块一个Servlet&#xff0c;相当于在数据库中一张表对应一个Servlet&#xff0c;在Servlet中提供不同的方法&#xff0c;完成用户的请求。 Idea控制台中文乱码解决&…