C++STL详解(二)——string类的模拟实现

news2024/11/14 13:53:40

首先,我们为了防止命名冲突,我们需要在自己的命名空间内实现string类。

一.string类基本结构

string类的基本结构和顺序表是相似的,结构如下:

//.h
namespace kuzi
{
	class string
	{
	private:
		char* _str;//字符串
		size_t _size;//长度
		size_t _capacity;//容量
	};
}

这里我们采取类内声明,类外实现的方式。

若不加声明,本文代码默认在cpp文件中实现。 

二.构造函数以及析构函数外加赋值运算符重载

首先,我们应该写构造函数和析构函数以及赋值运算符重载的头文件。

	//.h
        public:
		string(const char* str);
		string(const string& str);
		~string();
        string& operator=(const string& str)

我们在这里实现一个析构函数以及两个构造函数

这两个构造函数分别是:

  • 字符串构造
  • string类对象构造

对于构造函数的实现,我们不采取手写字符串拷贝的方式,直接用C语言库中的stycpy即可。

2.1字符串构造 

现在我们来实现第一个构造函数

我们可以很轻易的写出如下代码:

	string::string(const char* str)
		:_str(new char [strlen(str)+1])//多开一个空间给/0
		, _size(strlen(str))//不算\0
		, _capacity(strlen(str))//不算\0
	{
		strcpy(_str, str);
	}

但是这样实现的话,strlen就需要计算三次了,这显然是我们不想看到的。

因此我们这样子便行不通了。

因此,我们使用如下的方法来完成这个函数:

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

此外,由于我们还需要解决空字符串的问题

空字符串:只有一个‘\0’的字符串。

在这里我们可以通过给缺省值的方式解决。

//.h		
string(const char* str="");//字符串中默认有\0

在这里值得大家注意的是,我们不可给'\0'作为缺省值。

string(const char* str=‘\0’);

这是因为‘\0’无法隐式转换为const char*,只能隐式转换为int,也就是转换为了整型的0.

在这里会出现空指针的问题。

下面我们来实现字符串类对象构造:

2.2字符串类对象构造

有了上个构造函数的基础,我们可以很容易的写出如下代码: 

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

这样,便可以完成对字符串类对象的构造。

2.3析构函数

对于析构函数的实现也是相当简单的,我们直接释放空间然后再将析构的对象的成员置0即可

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

2.4赋值运算符重载函数

 赋值运算符重载函数和构造函数不同的地方是,我们需要清空原字符串中的字符,然后再进行赋值。因此我们可以写出如下代码:

	string& string::operator=(const string& s)
	{
		char* tmp = new char[s._capacity + 1];
		strcpy(tmp, s._str);
		delete[] _str;//赋值需要清空原字符串
		_str = tmp;
		_size = s._size;
		_capacity = s._capacity;
		return *this;
	}

三.返回成员相关函数

在这里我们实现如下四个函数:

		//.h
        const char* c_str()const;
		size_t size() const;
		char& operator[](size_t pos);
		const char& operator[](size_t pos) const;

这些函数是极其容易实现的:

	const char* string::c_str()const
	{
		return _str;
	}
	size_t string::size() const
	{
		return _size;
	}
	char& string::operator[](size_t pos)
	{
		return _str[pos];
	}
	const char& string::operator[](size_t pos) const
	{
		return _str[pos];
	}

四.迭代器以及迭代器相关函数的实现

4.1迭代器的实现

这里我们采取一种简单的方式来实现迭代器

//.h
typedef char* iterator;
typedef const char* const_iterator;

我们这里直接typedef一下指针来模拟实现迭代器。

4.2迭代器相关函数实现

下面我们来模拟实现几个与迭代器相关的函数:

		//.h
        iterator begin();
		iterator end();
		const_iterator begin()const;
		const_iterator end()const;

大家在这里需要注意的是,我们返回的是指针。

由于我们的_str是char*类型的,因此我们可以直接返回。

然后返回end位置时,我们可以通过指针运算来完成。

因此我们可以实现出如下代码:

	string::iterator string::begin()
	{
		return _str;
	}
	string::iterator string::end()
	{
		return _str + _size;
	}
	 string::const_iterator string::begin()const
	{
		 return _str;
	}
	 string::const_iterator string::end()const
	 {
		 return _str + _size;
	 }

 五.字符串的增删查函数

在这里,我们要具体实现如下函数: 

//.h
void resever(size_t n);
void push_back(char ch);
string& operator+=(char ch);
string& operator+=(const char* str);
void append(const char* str);//权限不可放大
void insert(size_t pos, char ch);
void insert(size_t pos, const char* str);
void erase(size_t pos, size_t len = npos);
size_t find(char ch, size_t pos = 0);
size_t find(const char* str, size_t pos = 0);

可能大家对为什么要把resever放在这块实现表示疑惑。

对于字符串进行增删,也就需要更改字符串的空间。

因此我们实现一个resever,就可以提高代码的耦合性。

5.1reserve的实现

由于大多数编译器都不实现缩容,因此我们在这里也不实现缩容。

由于我们很难实现原地扩容,因此我们这里采用异地扩容的方式。

我们首先开辟一块有n+1那么大的空间,然后将原字符串拷贝到这块空间即可。

需要大家注意的是:我们需要将原空间的字符串释放,否则将会导致出现空指针。

我们可以很容易的实现出如下代码:

	 void string::resever(size_t n)
	 {
		 if (_capacity < n)
		 {
			 char* tmp = new char[n + 1];
			 strcpy(tmp, _str);
			 delete[] _str;
			 _str = tmp;
			 _capacity = n;
		 }
	 }

5.2字符串尾插相关函数的实现

我们在这里需要实现的函数如下:

void push_back(char ch);
string& operator+=(char ch);
string& operator+=(const char* str);
void append(const char* str);//权限不可放大
5.2.1pushback的实现

我们先检查一下空间,然后我们再开空间即可完成对空间的操作

然后我们直接尾插该字符并再追加一个\0即可。

	 void string::push_back(char ch)
	 {
		 if (_size == _capacity)
		 {
			 size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
			 resever(newcapacity);
		 }
		 _str[_size] = ch;
		 _str[_size + 1] = '\0';
		 ++_size;
	 }
5.2.2append的实现

append是尾插一个字符串的函数,因此我们的实现逻辑和pushback的逻辑有略微不同。

首先,我们应计算出该字符串的函数,然后通过计算来判断需不需要扩容。

之后,我们可以通过C库中的字符串追加函数将字符追加到原字符串的结尾。

但是这个函数需要我们遍历一遍原字符串,时间复杂度为O(N);

而字符串拷贝函数的时间复杂度为O(N)!!!

所以我们这里通过将字符串拷贝到原字符串的末尾来解决该问题。

因此,我们可以写出如下代码: 

     void string::append(const char* str)
	 {
		 size_t len = strlen(str);
		 if (_size+len> _capacity)
		 {
			 size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
			 resever(newcapacity);
		 }
		 //方式1
		 //strcat(_str, str);
		 //方式2
		 strcpy(_str + _size, str);
	 }

5.3+=操作符的重载

这里我们+=一个字符直接调用pushback;+=字符串直接调用append即可。

	 string& string::operator+=(char ch)
	 {
		 push_back(ch);//this.pushback
		 return *this;
	 }
	 string& string::operator+=(const char* str)
	 {
		 append(str);
		 return *this;
	 }

5.4insert函数的实现

我们先实现插入一个字符的insert函数

5.4.1插入一个字符

有了上面的基础,我们可以很容易的得到如下代码: 

	 void string::insert(size_t pos, char ch)
	 {
		 //检查空间
		 if (_size == _capacity)
		 {
			 size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
			 resever(newcapacity);
		 }
		 //插入
		 size_t end = _size;
		 while (end >= pos)
		 {
			 _str[end + 1] = _str[end];
			 end--;
		 }
		 _str[pos] = ch;
		 _size++;
	 }

很遗憾的是,这段代码是跑不过去的。

当我们在第一个位置插入字符时,发现end已经等于-1了,但是我们又进入了下一次循环。

这是为什么呢?

这是因为无符号的-1是整型最大值。
如果是在第一个位置插入,那么pos==0
end==0时可以进去,等到end==-1时依旧可以进去

这样下去就形成了一个死循环,也就是说这段代码就跑不过去了。

那么我们应该如何处理呢?

处理方式1:

归根结底,size_t类型是无符号整型,它是不可能小于0的。

也就是说,size_t类型的数是一定>=0的。

所以,我们只要是能够绕过>=就可以了。

那么我们如何绕过>=呢?

这里我们让end等于最后一个字符的后一个字符。

然后我们不断地把前一个字符赋给后一个字符即可

		 size_t end = _size + 1;
		 while (end > pos)//1>0   0!>0
		 {
			 _str[end] = _str[end - 1];
			 end--;
		 }
		 _str[pos] = ch;
		 _size++;

处理方式2:

既然无符号整型不行,那么我们直接不用无符号整型了,我们用int不就行了吗!

		 int end = _size;
		 while (end >= (int)pos)
		 {
			 _str[end + 1] = _str[end];
			 end--;
		 }
		 _str[pos] = ch;
         _size++;
5.4.2指定位置插入字符串

 同样的,这里我们也可以通过以上两种方式来控制字符串的插入

代码如下:

 void string::insert(size_t pos, const char* str)
 {
	 //检查空间
	 size_t len = strlen(str);
	 if (_size + len > _capacity)
	 {
		 size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
		 resever(newcapacity);
	 }
     //控制方法2
	 //int end = _size;
	 //while (end > (int)pos)
	 //{
		// _str[end+len] = _str[end];
		// --end;
	 //}
     //控制方法1
	 size_t end = _size + len;
	 while (end > pos+len-1)//hello
	 {
		 _str[end] = _str[end-len];
		 --end;
	 }
	 memcpy(_str +pos,str,len);
 }

在这里需要我们大家注意控制方法1,以下是图解。 

因此我们需要end>pos+len-1。 

5.5字符串指定位置删除函数的实现

如果我们要删除的字符长度大于len前面字符个数时,我们将那些字符全删掉即可。(直接加\0)

否则,我们直接将pos+len后的字符拷贝到前面即可。(会拷贝\0)

	 void string::erase(size_t pos, size_t len = -1)
	 {
		 //len大于前面字符个数时,全删掉。
		 if (len == -1 || len > _size - pos)
		 {
			 _str[pos] = '\0';
			 _size = pos;
		 }
		 else
		 {
			 strcpy(_str + pos, _str + pos + len);
		 }
	 }

值得注意的是,len的默认值是-1,也就是整形的最大值。 

5.6字符串从指定位置查找函数的实现

5.6.1查找字符

直接遍历查找即可。 

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

比较复杂的KMP算法以及BM算法会在后续文章介绍。 

5.6.2查找字符串

 查找字符串直接用C库中的strstr查找即可。

	size_t string::find(const char* str, size_t pos = 0)
	{
		char* poss = strstr(_str + pos, str);
		return poss - _str;

六.比较操作符函数重载

在这里,我们要实现如下六个函数: 

//操作符函数
bool operator>(const string& s)const;
bool operator>=(const string& s)const;
bool operator<(const string& s)const;
bool operator<=(const string& s)const;
bool operator==(const string& s)const;
bool operator!=(const string& s)const;

我们可以通过C库中的strcmp函数来完成这六个函数

现在我们先实现大于和等于操作符重载:

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

下面我们可通过代码的复用来完成剩余的操作符

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

七.流操作符重载

	//.h
	istream& operator>>(istream& in, string& s);
	ostream& operator<<(istream& out, string& s);

由于我们的流操作符需要调整参数的顺序,因此我们需要在类外声明 

7.1流插入操作符重载

对于流插入操作符,我们使用的方法是极其简单的。

直接一个一个的将字符输出即可。 

	 ostream& operator<<(istream& out, string& s)
	 {
		 for (size_t i = 0; i <= _str.size(); i++)
		 {
			 out << str[i];
		 }
		 return out;
	 }

7.2流提取操作符重载

对于流提取操作符,我们首先应将原字符串的字符清除。

因此,我们在这里选择再在类内声明一个clear函数,并实现。

之后,我们使用get函数,将字符一个一个的拷贝至string对象中。

代码如下:

void string::clear()
{
	_str[0] = '\0';
	_size = 0;
}
istream& operator>>(istream& in, string& s)
{
 s.clear();
 char ch = in.get();
 while (ch != ' ' && ch != '\n')
 {
	 s += ch;
	 ch = in.get();
 }
 return in;
}

这个代码需要频繁的扩容,有人给出了如下的优化版代码:

istream& operator>>(istream& in, string& s)
{
	s.clear();
	char buff[128];
	int i = 0;
	char ch = in.get();
	while (ch != ' ' && ch != '\n')
	{
		buff[i++] = ch;
		if (i == 127)
		{
			buff[i] = '\0';
			s += buff;
			i = 0;
		}
		ch = in.get();
	}
	return in;
}

 八.其他函数

这里我们实现两个函数,分别是交换函数以及取子串函数 

        //.h
		void swap(string& s);
		string substr(size_t pos = 0, size_t len=-1);

实现如下:

//std库中的swap代价特别大
	//拷贝构造和两次赋值都是深拷贝
	//交换指针来解决
	void string::swap(string& s)
	{
		std::swap(_str, s._str);
		std::swap(_size, s._size);
		std::swap(_capacity, s._capacity);
	}
	//取子串
	string string::substr(size_t pos = 0, size_t len)
	{
		if (len > _size - pos)//len大于剩余长度
		{
			string sub(_str + pos);
			return sub;
		}
		else
		{   
			string sub;
			sub.resever(len);
			for (size_t i = 0; i < len; i++)
			{
				sub += _str[pos + i];
			}
			return sub;
		}
	}

九.现代思想

在现代思想中,我们采取交换的方式来完成string类的构造,但本质没有变化。

//现代写法
string(const string& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	string tmp(s._str); //调用构造函数,构造字符串。
	swap(tmp); //交换s和tmp
}
	//传统写法
    string::string(const string& str)
	{
		_str = new char[str._capacity + 1];
        strcpy(_str, str._str);
		_size = str._size;
		_capacity = str._capacity;
	}

十.为什么要使用const成员

可能大家对成员函数中的参数为什么要用const对象表示疑惑

我们在这里进行解释:

const的对象是常的,常量是只可读不可写的。

非const的对象是可读可写的。

权限是不可以放大的,只能平移和缩小。

  • 传非const的对象。

权限变化:可读可写-->可读(权限的缩小)

  • 传const的对象

权限变化:可读-->可读(权限的平移)

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

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

相关文章

算法基础之回溯法

本文将详细介绍回溯法的基本原理和适用条件&#xff0c;并通过经典例题辅助读者理解回溯法的思想、掌握回溯法的使用。本文给出的例题包括&#xff1a;N皇后问题、子集和问题。 算法原理 在问题的解空间树中&#xff0c;回溯法按照深度优先的搜索策略&#xff0c;从根结点出发…

LDR6020:重塑iPad一体式有线键盘体验的创新力量

在移动办公与娱乐日益融合的时代&#xff0c;iPad凭借其强大的性能和便携性&#xff0c;成为了众多用户不可或缺的生产力工具。然而&#xff0c;为了进一步提升iPad的使用体验&#xff0c;一款高效、便捷的键盘成为了不可或缺的配件。今天&#xff0c;我们要介绍的&#xff0c;…

TYPE-C接口PD取电快充协议芯片ECP5701:支持PD 2.0和PD 3.0(5V,9V,12V,15V,20V)

随着智能设备的普及&#xff0c;快充技术成为了越来越多用户的刚需。而TYPE-C接口作为新一代的USB接口&#xff0c;具有正反插、传输速度快、充电体验好等优点&#xff0c;已经成为了快充技术的主要接口形式。而TYPE-C接口的PD&#xff08;Power Delivery&#xff09;取电快充协…

【数据结构】线性结构——数组、链表、栈和队列

目录 前言 一、数组&#xff08;Array&#xff09; 1.1优点 1.2缺点 1.3适用场景 二、链表&#xff08;Linked List&#xff09; 2.1优点 2.2缺点 2.3适用场景 三、栈&#xff08;Stack&#xff09; 3.1优点 3.2缺点 3.3适用场景 四、队列&#xff08;Queue&#xff09; 4.1优点…

【python】Python高阶函数--reduce函数的高阶用法解析与应用实战

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

Redis常用的5大数据类型

Reids字符串&#xff08;String&#xff09; 设置相同的key&#xff0c;之前内容会覆盖掉 Redis列表&#xff08;List&#xff09; 常用命令 从左往右放值 数据结构 Redis集合&#xff08;set&#xff09; sadd<key><value1><value2>...... 数据结构 Set数据…

前端组件化开发:以Vue自定义底部操作栏组件为例

摘要 随着前端技术的不断演进&#xff0c;组件化开发逐渐成为提升前端开发效率和代码可维护性的关键手段。本文将通过介绍一款Vue自定义的底部操作栏组件&#xff0c;探讨前端组件化开发的重要性、实践过程及其带来的优势。 一、引言 随着Web应用的日益复杂&#xff0c;传统的…

「豆包Marscode体验官」 | 云端 IDE 启动 Rust 体验

theme: cyanosis 我正在参加「豆包MarsCode初体验」征文活动 MarsCode 可以看作一个运行在服务端的远程 VSCode开发环境。 对于我这种想要学习体验某些语言&#xff0c;但不想在电脑里装环境的人来说非常友好。本文就来介绍一下在 MarsCode里&#xff0c;我的体验 rust 开发体验…

Games101学习笔记 Lecture22 Animation(cont.)

Lecture22 Animation(cont. 一、单个粒子模拟Ordinary Differential Equation ODE 常微分方程ODE求解方法——欧拉方法解决不稳定中点法改进欧拉方法自适应步长隐式欧拉方法 二、流体模拟基于位置的方法物质点方法 一、单个粒子模拟 想模拟粒子在场中的运动 Ordinary Differe…

Token Labeling(NeurIPS 2021, ByteDance)论文解读

paper&#xff1a;All Tokens Matter: Token Labeling for Training Better Vision Transformers official implementation&#xff1a;https://github.com/zihangJiang/TokenLabeling 出发点 ViTs的局限性&#xff1a;尽管ViTs在捕捉长距离依赖方面表现出色&#xff0c; 但…

代码随想录算法训练营第五十八天|108.冗余连接、109.冗余连接II

108.冗余连接 题目链接&#xff1a;108.冗余连接 文档讲解&#xff1a;代码随想录 状态&#xff1a;还行 思路&#xff1a; 并查集可以解决什么问题&#xff1a;两个节点是否在一个集合&#xff0c;也可以将两个节点添加到一个集合中。 题解&#xff1a; public class Main {p…

套用BI方案做数据可视化是种什么体验?

在数字化转型的浪潮中&#xff0c;数据可视化作为连接数据与决策的桥梁&#xff0c;其重要性日益凸显。近期&#xff0c;我有幸体验了奥威BI方案进行数据可视化的全过程&#xff0c;这不仅是一次技术上的探索&#xff0c;更是一次对高效、智能数据分析的深刻感受。 初识奥威&a…

.net dataexcel 脚本公式 函数源码

示例如: ScriptExec(""sum(1, 2, 3, 4)"") 结果等于10 using Feng.Excel.Builder; using Feng.Excel.Collections; using Feng.Excel.Interfaces; using Feng.Script.CBEexpress; using Feng.Script.Method; using System; using System.Collections.Gen…

场景分析法挖掘需求的常见4大步骤

场景分析方法&#xff0c;有助于精确定位需求&#xff0c;优化产品设计&#xff0c;促进团队协同&#xff0c;减少项目风险&#xff0c;提升用户满意度与市场竞争力。若场景分析不足&#xff0c;产品可能偏离用户需求&#xff0c;导致功能冗余或缺失&#xff0c;用户体验差&…

java中传引用问题

在 Java 中&#xff0c;所有对象都是通过引用传递的&#xff0c;而基本数据类型是通过值传递的。 引用传递&#xff1a; 当一个对象作为参数传递给方法时&#xff0c;传递的是对象的引用。对这个对象引用进行的修改会影响到原始对象。例如&#xff1a; public class Test {p…

Designing Data-Intensive Applications数据密集型应用系统设计-读书笔记

目录 第一部分可靠性、可扩展性、可维护性硬件故障描述负载 吞吐与延迟可维护性 第二章 数据模型与查询语言第三章索引哈希索引B-tree事务 第三章 编码第二部分、分布式数据系统第五章 数据复制单主从复制节点失效日志实现复制滞后问题 多主节点复制 第六章、数据分区3 第一部分…

10个常见的电缆载流表,值得收藏!

众所周知,电线电缆的载流是所有电工、电气人员都必须具备的基本储备,但是如果要将那么多的“数字”都记得清清楚楚,还是有一点困难的!今天咱们就做了一个电力电缆载流量对照表,速度收藏!下次参考不迷路! 1、0.6/1KV聚氯乙烯绝缘电力电缆载流量 以上电缆载流量计算条件:…

世界启动Ⅳ--利用AI和费曼技巧学习一切

前言 有无数的学习技巧可以帮助你消化复杂的概念&#xff0c;并有信心记住它们。如果你像我一样是一个不断学习的学生&#xff0c;你就会明白有效学习方法的重要性。其中最简单的一种就是费曼技巧。 在本文中&#xff0c;我将解释如何有效地应用费曼学习方法&#xff0c;以及…

应用最优化方法及MATLAB实现——第5章代码实现

一、概述 继上一章代码后&#xff0c;这篇主要是针对于第5章代码的实现。部分代码有更改&#xff0c;会在下面说明&#xff0c;程序运行结果跟书中不完全一样&#xff0c;因为部分参数&#xff0c;书中并没有给出其在运行时设置的值&#xff0c;所以我根据我自己的调试进行了设…

迁移学习在乳腺浸润性导管癌病理图像分类中的应用

1. 引言 乳腺癌主要有两种类型:原位癌:原位癌是非常早期的癌症&#xff0c;开始在乳管中扩散&#xff0c;但没有扩散到乳房组织的其他部分。这也称为导管原位癌(DCIS)。浸润性乳腺癌:浸润性乳腺癌已经扩散(侵入)到周围的乳腺组织。侵袭性癌症比原位癌更难治愈。将乳汁输送到乳…