【CPP】string 类的模拟实现

news2024/11/15 13:35:49

​🌠 作者:@阿亮joy.
🎆专栏:《吃透西嘎嘎》
🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
在这里插入图片描述

目录

    • 👉前言👈
    • 👉访问和遍历 string 类👈
    • 👉capacity、resize 和 reserve👈
    • 👉string 类插入字符或字符串👈
    • 👉erase👈
    • 👉find👈
    • 👉流插入和流提取重载👈
    • 👉拷贝构造和赋值运算符重载的传统写法👈
      • 拷贝构造
      • 赋值运算符重载
    • 👉拷贝构造和赋值运算符重载的现代写法👈
      • 拷贝构造
      • 赋值运算符重载
    • 👉浅拷贝和深拷贝👈
      • 浅拷贝
      • 深拷贝
    • 👉总结👈

👉前言👈

因为 string 类的函数接口组合在一起才好玩,所以我们模拟实现 string 类就一组一组函数来模拟实现。为了避免我们模拟实现的 string 类和库里的 string 类冲突,所以我们将自己实现的 string 类封装在命名空间里。

👉访问和遍历 string 类👈

首先,string 类是一个可以动态增长的字符数组,其实就相当于是存储字符的顺序表。那么 string 类的成员变量如下方代码:

namespace Joy
{
	class string
	{
	public:
		// 成员函数
	private:
		char* _str; // 指向动态开辟的数组
		size_t _size; // 数组的有效字符个数
		size_t _capacity; // 数组的容量
	};
}

那现在我们就来模拟实现 string 类了。string 类最重要的就是构造函数了,所以我们先实现构造函数。

string 类的构造函数

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

	strcpy(_str, str);
}

注:string类的构造函数最好提供空串缺省参数,然后还有多开一个空间来存储\0标识字符,标识字符不算是 string 类的有效字符,给 _size 和 _capacity 都赋好初值后,就借助 strcpy 函数来拷贝字符串了。

string 类的析构函数

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

为了 string 类能用起来像数组一样,我们需要提供以下函数接口sizec_str[]运算符重载。

const char* c_str() const
{
	return _str; // 返回数组首元素的地址
}

size_t size() const
{
	return _size;
}

// 普通对象:可读可写
char& operator[](size_t pos)
{
	assert(pos < _size);
	return _str[pos];
}

// const对象:只读
const char& operator[](size_t pos) const
{
	assert(pos < _size);
	return _str[pos];
}

以上实现的函数接口,足以帮我构造一个 string 类对象和访问 string 类对象了,那么现在我们就来测试一下。

在这里插入图片描述
这样,访问和遍历 string 类对象的第一种方式就搞定了。那我们现在来实现一下遍历 string 类对象的第二种方式:迭代器iterator

迭代器可能是指针,也有可能不是指针,所以我们实现 string 类迭代器的方式是指针。

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;
}

那现在我们来测试一下功能。

在这里插入图片描述
注:以上的 string 类迭代器是使用指针实现的,但是库里的 string 类的迭代器就有可能不是用指针实现了,比如 string 类的反向迭代器就不是使用指针来实现的了。因为reverse_iterator rit = s.rbegin() rit++会走到s.rend(),而如果使用指针实现,应该it--才能走到s.end(),使用 string 类的反向迭代器不是使用指针来实现的。

现在我们已经实现了 string 类的正向迭代器,其实范围 for 也实现好了。因为范围 for 的底层原理就是迭代器,编译器会自动将范围 for 替换成迭代器。

在这里插入图片描述
如果我们将正向迭代器的代码屏蔽掉,那范围 for 的代码就无法编译通过了。

👉capacity、resize 和 reserve👈

size_t capacity() const
{
	return _capacity;
}

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

		_capacity = n;
	}
}

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

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

reserve 函数接口说明

  • 如果 n > _capacity,那么就先申请一块 n + 1的空间tmp,然后将数据拷贝到这块空间,_str = tmp,更新容量的大小_capacity = n
  • 如果n <= _capacity,那么就什么都不用做。如果缩容的话,就会造成效率低下。

resize 函数接口说明

  • n > _size时,调用 reserve 函数调整容量的大小,让插入字符 ch。
  • n <= _size时,直接_str[_size] = '\0' _size = n,注意:这个过程也不要缩容。

在这里插入图片描述

👉string 类插入字符或字符串👈

我们主要模拟实现最常用的几个插入的接口,目的不是造更好的轮子。那我们现在开干!

string& push_back(char ch)
{
	if (_size == _capacity)
	{
		int newCapacity = _capacity == 0 ? 4 : _capacity * 2;
		reserve(newCapacity);
	}

	_str[_size] = ch;
	++_size;
	_str[_size] = '\0';

	return *this;
}

string& append(const char* str)
{
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}

	strcpy(_str + _size, str);
	_size += len;

	return *this;
}

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

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

注:不管是 push_back 函数,还是 append 函数,都要考虑是否需要扩容(reverse 函数会多开一个空间存储标识字符'\0'),然后再插入数据,最后更新_size和加上标识字符'\0'。对于operator +=运算符重载就可以赋用 push_back 函数和 append 函数了。

在这里插入图片描述

除了以上在尾部的插入,还有在任意位置的插入。

string& insert(size_t pos, char ch)
{
	assert(pos <= _size);

	if (_size == _capacity)
	{
		int newCapacity = _capacity == 0 ? 4 : _capacity * 2;
		reserve(newCapacity);
	}

	// 挪动数据
	/*
	int end = _size;
	while (end >= (int)pos)
	{
		_str[end + 1] = _str[end];
		--end;
	}
	_str[pos] = ch;
	++_size;
	return *this;
	*/

	size_t end = _size + 1;
	while (end > pos)
	{
		_str[end] = _str[end - 1];
		--end;
	}
	_str[pos] = ch;
	++_size;
	return *this;
}

string& insert(size_t pos, const char* str)
{
	assert(pos <= _size);

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

	// 挪动数据
	/*
	int end = _size;
	while (end >= (int)pos)
	{
		_str[end + len] = _str[end];
		--end;
	}

	strncpy(_str + pos, str, len);
	_size += len;
	return *this;
	*/

	size_t end = _size + len;
	while (end > pos + len - 1)
	{
		_str[end] = _str[end - len];
		--end;
	}

	strncpy(_str + pos, str, len);
	_size += len;
	return *this;
}

注:挪动数据就要注意对边界条件的控制,如果控制不会就会造成死循环和越界访问的问题。还有拷贝数据,不能使用strcpy函数,strcpy函数会将\0'拷贝过去。如果是>=,要讲pos强转为int。

在这里插入图片描述

👉erase👈

bool empty() const
{
	return _size == 0;
}

string& erase(size_t pos, size_t len = npos)
{
	assert(!empty());
	assert(pos < _size);

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

	return *this;
}

注:const static size_t npos = -1npos是 string 类的静态成员变量,其值为-1。

erase 函数接口说明

  • len == nps || pos + len >= _size时,就相当于将pos位置及其之后的字符都删掉,所以此时只需要将pos位置的字符改为'\0'并更新_size的值为pos
  • 除此之外,用 strcpy 函数将后面的字符向前覆盖删除,然后_size -= len就行了。

在这里插入图片描述

👉find👈

size_t find(char ch, size_t pos) const
{
	assert(pos < _size);
	while (pos < _size)
	{
		if (_str[pos] == ch)
		{
			return pos;
		}
		++pos;
	}

	return npos;
}

size_t find(const char* str, size_t pos) const
{
	assert(pos < _size);
	const char* ptr = strstr(_str + pos, str);
	if (ptr == nullptr)
	{
		return npos;
	}
	else
	{
		return ptr - _str;
	}
}

注:strstr函数是暴力匹配的查找算法,除了这种算法外,还有KMP算法。如果想要了解KMP算法的话,可以看一下这篇文章。需要注意的是,strstr函数的返回值是指针,而find函数的返回值为下标,所以我们要将指针进行相减转换成下标。

在这里插入图片描述

在这里插入图片描述

👉流插入和流提取重载👈

<< 运算符重载

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

<< 运算符是将类对象里的字符一个个打印出来的,所以它跟 c.str() 有一点区别。

在这里插入图片描述

在这里插入图片描述

clear 函数清空类对象的数据

void clear()
{
	_size = 0;
	_str[0] = '\0';
}

>> 运算符重载

istream& operator>>(istream& in, string& s)
{
	s.clear();
	
	// 第一种方式
	/*char ch = in.get();
	while (ch != ' ' && ch != '\n')
	{
		s += ch;
		//in >> ch;
		ch = in.get();
	}
	return in;*/

	// 第二种方式
	char buff[128] = { '\0' };
	size_t i = 0;
	char ch = in.get();
	while (ch != ' ' && ch != '\n')
	{
		if (i == 127)
		{
			s += buff;
			i = 0;
		}

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

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

	return in;
}

以上两种方式,都能实现流插入。但是第二种方式相较于第一种方式不需要频繁地扩容。注:流提取是以空格或者换行结束的。

在这里插入图片描述

注:以上的流插入和流提取重载可以用友元实现,友元的话就可以直接访问类对象的数据了,而我实现的方式是通过函数接口来访问类对象的数据。

👉拷贝构造和赋值运算符重载的传统写法👈

拷贝构造

//传统拷贝构造写法 s2(s1)
string(const string& s)
{
	_str = new char[s._capacity + 1];
	_capacity = s._capacity;
	_size = s._size;

	strcpy(_str, s._str);
}

赋值运算符重载

// 传统赋值运算符重载 s2 = s1
string& operator=(const string& 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;
}

在这里插入图片描述
注:拷贝构造和赋值运算符重载都是先申请或释放空间,然后再把数据拷贝到对象中去。

👉拷贝构造和赋值运算符重载的现代写法👈

拷贝构造

在这里插入图片描述

交换类对象的数据

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

库里面也有交换的函数模板,但是这个函数接口会涉及三次深拷贝,效率不高。所以我们就自己实现一个交换类对象数据的函数接口,string 类中的交换函数也是这样实现的,效率较高。

在这里插入图片描述

// 现代拷贝构造写法 s2(s1)
string(const string& s)
	: _str(nullptr)
	, _size(0)
	, _capacity(0)
{
	string tmp(s._str); // 构造函数
	//this->swap(tmp);
	swap(tmp);
}

拷贝构造现代写法说明

  • 先初始化列表,初始化对象 s2
  • 然后调用构造函数string tmp(s._str),构造一个对象出来
  • 最后将 tmp 的数据和 s2 的数据进行交换
  • 注:s2 一定要走初始化列表,如果不走初始化列表,s2 的数据将会是随机值,随机指向一块空间。将 tmp 和 s2 的数据交换后,tmp 会被销毁,那么随机指向的空间将会被delete掉,程序会崩溃。

未写初始化列表

在这里插入图片描述

赋值运算符重载

交换类对象的数据

void swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}
// 现代赋值运算符重载 s2 = s1
/*string& operator=(const string& s)
{
	if (this != &s)
	{
		//string tmp(s._str);
		string tmp(s);
		swap(tmp);
	}
	return *this;
}*/

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

赋值运算符重载现代写法有两种:第一种现代赋值运算符重载的参数是类对象的引用,可以减少拷贝构造。当 s2 != s1 时,才进行调用拷贝构造函数构造对象tmp,再将tmp的数据和s2的数据进行交换。第二种现代赋值运算符重载的参数类对象,传参需要调用拷贝构造函数构造s,然后将s的数据和s2的数据进行交换。

在这里插入图片描述

注:拷贝构造和赋值运算符重载的现代写法并不是追求效率,而是追求简洁。深拷贝不要求_capacity相同。

有了模板之后,自定义类型也需要有构造函数和析构函数。见下图代码:

在这里插入图片描述
为什么要支持内置类型的构造函数和析构函数呢?因为有了模板,要支持泛型编程。比如下图:如果 T1 和 T2 为内置类型,就要去调用构造函数和析构函数了。

在这里插入图片描述

👉浅拷贝和深拷贝👈

浅拷贝

浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共
享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为
还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。


就像一个家庭中有两个孩子,但父母只买了一份玩具,两个孩子愿意一块玩,则万事大吉,万一不想分享就 你争我夺,玩具损坏。

在这里插入图片描述
在这里插入图片描述
可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。父母给每个孩
子都买一份玩具,各自玩各自的就不会有问题了。

深拷贝

如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情
况都是按照深拷贝方式提供。

在这里插入图片描述

👉总结👈

其实 string 类的函数接口远不止这些,还有很多。而我们模拟实现的只是一些常用的、重点的,那些比较字符串大小的函数接口不经常使用,我们就不实现了。那么,以上就是本篇博客的全部内容,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️

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

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

相关文章

Angular 学习 之 Hello World !

目录 0.前言・前提&#xff08;Angular介绍&#xff09; 前言 前提&#xff08;node.js已经按照&#xff09; 1.安装・查看版本 2.创建・启动Angular工程 2.1.创建工程 2.2.启动工程 2.3.启动之后&#xff0c;浏览器访问&#xff0c;显示的效果 2.4.工程目录结构 3.各…

计算机毕业设计——基于html汽车商城网站页面设计与实现论文源码ppt(35页) HTML+CSS+JavaScript

常见网页设计作业题材有 个人、 美食、 公司、 学校、 旅游、 电商、 宠物、 电器、 茶叶、 家居、 酒店、 舞蹈、 动漫、 服装、 体育、 化妆品、 物流、 环保、 书籍、 婚纱、 游戏、 节日、 戒烟、 电影、 摄影、 文化、 家乡、 鲜花、 礼品、 汽车、 其他等网页设计题目, A…

Flutter高仿微信-第29篇-单聊

Flutter高仿微信系列共59篇&#xff0c;从Flutter客户端、Kotlin客户端、Web服务器、数据库表结构、Xmpp即时通讯服务器、视频通话服务器、腾讯云服务器全面讲解。 详情请查看 效果图&#xff1a; 实现代码&#xff1a; 单聊包含&#xff1a;文本、表情、语音、图片、小视频、…

HTML学生个人网站作业设计——HTML+CSS+JavaScript优分期大学生分期购物商城(7页)

常见网页设计作业题材有 个人、 美食、 公司、 学校、 旅游、 电商、 宠物、 电器、 茶叶、 家居、 酒店、 舞蹈、 动漫、 服装、 体育、 化妆品、 物流、 环保、 书籍、 婚纱、 游戏、 节日、 戒烟、 电影、 摄影、 文化、 家乡、 鲜花、 礼品、 汽车、 其他等网页设计题目, A…

Vue3【Composition API 的优势、新的组件(Fragment、Teleport、Suspense)、全局API的转移】

文章目录四、Composition API 的优势1.Options API 存在的问题2.Composition API 的优势五、新的组件1.Fragment2.Teleport3.Suspense六、其他1.全局API的转移2.其他改变四、Composition API 的优势 1.Options API 存在的问题 使用传统OptionsAPI中&#xff0c;新增或者修改一…

web前端网页设计与制作:HTML+CSS旅游网页设计——桂林旅游(3页) web前端旅游风景网页设计与制作 div静态网页设计

&#x1f468;‍&#x1f393;学生HTML静态网页基础水平制作&#x1f469;‍&#x1f393;&#xff0c;页面排版干净简洁。使用HTMLCSS页面布局设计,web大学生网页设计作业源码&#xff0c;这是一个不错的旅游网页制作&#xff0c;画面精明&#xff0c;排版整洁&#xff0c;内容…

Flink时间窗口语义

Flink时间窗口语义WarterMarker特点自定义水位线策略周期性水位线生成器&#xff08;Periodic Generator&#xff09;断点式水位线生成器&#xff08;Punctuared Generator&#xff09;在自定义数据源中发送水位线水位线的传递水位线总结窗口&#xff08;Window&#xff09;窗口…

spark-core-源码、Worker启动、sparksubmit提交、Driver启动

sparksubmit源码解析 在提交我们写好的jar包时候&#xff0c;用到submit命令&#xff0c;他的源码解析流程如上图 位于deploy里的SparkSubmit里面&#xff0c;根据main方法一点点run进去&#xff0c;分配我们传的参数&#xff0c;尤其是 val (childArgs, childClasspath, spa…

电脑屏幕亮度怎么调?四种自由调节亮度方式

现在的电脑是很方便的&#xff0c;可以说我们日常的生活中离不开电脑了。但是电脑屏幕亮度怎么调呢&#xff1f;这是一个问题&#xff0c;我们应该如何去调节&#xff1f;其实调节的方式有很多&#xff0c;我们根据自己的需求进行调节即可。接下来&#xff0c;为大家介绍四种电…

今日睡眠质量记录82分

昨天回去得比较晚了&#xff0c;不过睡眠质量还不错的&#xff0c;睡得比较沉&#xff0c;睡眠质量记录还不错的&#xff0c;大概有82分左右了。

计算机的发展史

文章目录计算机的发展史一&#xff0c;算盘二&#xff0c;纳皮尔骨筹三&#xff0c;帕斯卡林四&#xff0c;莱布尼茨步进计算器五&#xff0c;差分机六&#xff0c;分析机七&#xff0c;制表机八&#xff0c;微分分析机九&#xff0c;Mark I 计算机十&#xff0c;五代计算机计算…

如何使用CSS创建高级动画,这个函数必须掌握

微信搜索 【大迁世界】, 我会第一时间和你分享前端行业趋势&#xff0c;学习途径等等。 本文 GitHub https://github.com/qq449245884/xiaozhi 已收录&#xff0c;有一线大厂面试完整考点、资料以及我的系列文章。 我们每天都在网上摸鱼&#xff0c;作为前端开发人员&#xff0…

SD-WAN行业经常说CPE、uCPE、vCPE是什么意思,各自有什么区别和应用场景?

我们先来看下传统的硬件终端CPE到底是什么。 CPE&#xff08;CustomerPremisesEquipment&#xff0c;客户端设备&#xff09;是指位于用户端的网络终端设备&#xff0c;用于与运营商对接服务&#xff0c;是网络解决方案的重要组成部分&#xff0c;通常是路由器、防火墙或者路由…

Linux 性能分析命令详解

top 命令 top -1 按数字1可以看到 多个核&#xff0c;每个核的cpu的使用情况 监控工具\平台来收集cpu的使用率 是所有cpu数量的一个总体的使用率 top -E 按大写字母E可以看到不同单位的内存使用情况 KB MB GB TB mem&#xff1a; buffer cache swap buffer是磁盘虚拟出来…

99-104-Hadoop-MapReduce-排序:

99-Hadoop-MapReduce-排序&#xff1a; WritableComparable 排序 排序是MapReduce框架中最重要的操作之一。 MapTask和ReduceTask均会对数据按 照key进行排序。该操作属于 Hadoop的默认行为。任何应用程序中的数据均会被排序&#xff0c;而不管逻辑上是否需要。 默认排序是按…

PID控制原理基本介绍(图解)

PID控制原理基本介绍(图解) 这里先以一个阶跃响应做图解说明: 如下图所示,目标值设定为单位1,随着我们逐渐增大比例系数Kp,可以看到系统相应速度逐渐加快,但是始终存在稳态误差, 如下图所示,引入积分环节后,随着积分控制系数Ki逐渐加大,误差逐渐减小,并最终达…

SSM毕设项目 - 基于SSM的婚纱摄影网站(含源码+论文)

文章目录1 项目简介2 实现效果2.1 界面展示3 设计方案3.1 概述3.2 系统流程3.2.1 系统开发流程3.3 系统结构设计4 项目获取1 项目简介 Hi&#xff0c;各位同学好呀&#xff0c;这里是M学姐&#xff01; 今天向大家分享一个今年(2022)最新完成的毕业设计项目作品&#xff0c;【…

OPNET Modeler 的安装及其相关配置

文章目录前言一、安装包下载1、OPNET Modeler 安装包下载2、Visual Studio 2010 安装包下载二、配置C/C环境变量三、OPNET Modeler 的安装1、安装 modeler_145A_PL1_7116_win2、安装 modeler_docs_28-Jan-2008_win3、安装 models_145A_PL1_27Feb08_win4、安装 OPNET.Modeler.14…

PLC中ST编程的基础知识

程序组织单元&#xff0c;简称POU&#xff1b;完整的PLC程序就是由无数个POU组成的&#xff1b; FB&#xff1a;功能块&#xff0c;也称函数块&#xff0c;执行时产生一个或多个值&#xff0c;一个功能块可以创建多个实例&#xff1b; VAR_IN&#xff1a;输入变量&#xff0c…

java绘制标注框,注册字体

文章目录场景思路步骤1.注册字体2.绘制标注框保存文本3.效果如下:场景 有个项目需要在java的后台将AI算法的标识框&#xff0c;置信度值&#xff0c;画到上传的报警图片上。以前都在算法部分画&#xff0c;但是效率有点低&#xff0c;所以传过来原始的图片&#xff08;也会用来…