C++中String的语法及常用接口的底层实现详解

news2024/11/24 15:40:47

  在C语言中,我们知道处理字符串所用的类型是 char []或者char* 。字符串是以‘\0’结尾的。在C++中,string 是一个标准库类(class),用于处理字符串。它提供了一种更高级、更便捷的字符串操作方式,string 类提供了一系列成员函数和重载运算符,以便于对字符串进行操作和处理。本编文章会对C++中的 string 进行详解,希望本篇文章会对你有所帮助。

目录

一、string类

二、string的常用见用法

2、1 string对象的构造

2、1、1 string对象的构造的使用方法

2、1、2 string()的底层实现

2、1、3 string(const char* s)的底层实现

2、2 string对象的修改操作

2、3 string对象的容量操作

2、4 string对象的访问和遍历操作

三、string常用结构的底层实现

3、1 初建结构

3、2 返回大小和容量

3、3 拷贝构造和赋值重载

3、4 扩容(reserve)

3、5 插入(push_back、append、operator+=、insert)

3、6 删除(erase)

3、7 查找(find)

3、8 返回子串(substr)

3、9 迭代器(iterator)

3、10 比较(>、<、>=、<=、==、!=)

四、总结


🙋‍♂️ 作者:@Ggggggtm 🙋‍♂️

👀 专栏:C++  👀

💥 标题:String讲解💥

 ❣️ 寄语:与其忙着诉苦,不如低头赶路,奋路前行,终将遇到一番好风景 ❣️  

一、string类

  在学习 string 前,我们不妨先来了解一下 string 类到底是什么,有什么用呢?下图是C++标准库中的对 string 内容:

  what???没错,C++标准库都是英语解释。我们也应该试着去适应,不懂的可以查阅。当然,在这里我就直接给出翻译,主要是以下内容:

  1. 字符串是表示字符序列的类;
  2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
  3. string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信息,请参阅basic_string)。
  4. string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)。
  5. 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。

  了解到上面的内容后,我们要开始真正的学习 string 的用法了。

二、string的常用见用法

2、1 string对象的构造

2、1、1 string对象的构造的使用方法

  最为常用的无非就是我们用串string来构造一个对象,也就是存储一个字符,常用的方法有如下几点:

  • string()——构造空的 string 类对象,即空字符串;
  • string(const char* s)——用 char* 来构造 string 类对象;
  • string(size_t n, char c)——string类对象中包含n个字符c
  • string(const string&s)——拷贝构造函数

  下面是使用方法所对应的实例,帮助更好的理解其用法。

  根据上面的实例和对应的输出结果,我们可以更好的理解。 

2、1、2 string()的底层实现

  构造空串,其有效字符长度为0,但是实际上是开辟了一个字节的空间存储 ‘\0’ 的。具体如下:

	string()
		:_str(new char[1])
		, _size(0)
		,_capacity(0)
	{
	}

2、1、3 string(const char* s)的底层实现

  我们这里就给出以上两个底层的构造实现,其余两个类似,就不再给出。具体实现如下:

string(const char* str = "") //默认空串。注意:空串是以 \0 结尾
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_size + 1];
			strcpy(_str, str);
		}

2、2 string对象的修改操作

  当我们构建好对象后,我们接下来就要往对应的字符串对象进行修改操作了。常用的修改操作无非就是插入和查找,具体有如下几种常见用法:

  • push_back——在字符串后尾插字符c
  • insert——在pos位置插入n个字符或者插入一个字符串;
  • append ——在字符串后追加一个字符串
  • operator+=——在字符串后追加字符或者字符串str;
  • c_str——返回C格式字符串;
  • find——从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置。

  我们再看其具体的使用方法实例,如下图:

  上图为常用的修改用法。当然,其他的用法还有很多。如果想了解的过可去C++官网(cppreference)或者 cplusplus 的标准库中查询。具体也可看下图:

  insert

  operator+= 

  find 

2、3 string对象的容量操作

  在平常对字符串的操作中,我们也经常需要去了解到字符串的实际长度为多少,或者改数组到底能够存下多长的字符串,又或是修改字符串的长度和空间大小。C++的string类中,这些操作都提供了相应的接口,具体如下:

  • size——返回字符串有效字符长度;
  • length——返回字符串有效字符长度
  • empty——检测字符串释放为空串,是返回true,否则返回false;
  • reserve——为字符串预留空间;
  • resize——将有效字符的个数该成n个,多出的空间用字符c填充;
  • capacity——返回空间总大小;
  • clear——清空有效字符;

  我们发现。size和length的功能一样的。确实都是求字符串的有效长度。那我们接着看其具体的实例:

  注意:capacity返回的是空间的总大小size和length返回的是字符串的实际有效长度。 两者是有所区别的。一个字符串的capacity是有底层的具体实现决定的,不同的编译器可能实现的是不同的。

2、4 string对象的访问和遍历操作

  string对象的访问,支持像数组一样使用 [] 进行访问,也可通过迭代器进行访问,具体有如下用法:

  • operator[]——返回pos位置的字符,const string类对象调用;
  • begin+ end——begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭
    代器;
  • 范围for——C++11支持更简洁的范围for的新遍历方式。

   其具体的用法实例如下:

  范围for的底层实现就是用迭代器来实现的。写起来更加的便捷和方便。 

  以上为string中较为常用的接口。以上完全足够我们平常的使用了,如果想要了解的更多,可参考C++的标准库。不过string容器一共实现了106个接口!!!其中大部分都是冗余的。这也是很多人吐槽string类实现的过于复杂和冗余的一个重要原因。所以在查看时,我们只需要看自己想要了解的接口即可。 

三、string常用结构的底层实现

3、1 初建结构

  我们通过上述的构造,不难发现也不难理解string的底层其实就是一个字符指针,该指针指向一个数组。当然,我们还需要两个变量来维护其有效长度(_size)数组容量(_capacity)

  其次,我们自己实现的string类为了区分std命名空间,我们可自己设置一个命名空间。处型的模拟实现如下:

namespace gtm
{

	class string
	{
     public:
        //string()
		//	:_str(new char[1])
		//	, _size(0)
		//	,_capacity(0)
		//{
		//}

		//string(const char* str)
		//	:_str(new char[strlen(str) + 1])  //三次strlen函数,效率低。
		//	,_size(strlen(str))
		//	,_capacity(strlen(str))
		//{
		//	strcpy(_str, str);
		//}

		// 不再使用strlen函数,初始化列表与变量声明顺序固定
		string(const char* str = "") //默认空串。注意:空串是以 \0 结尾
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_size + 1];
			strcpy(_str, str);
		}
        ~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}
     private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};

  注意,我们上述省略了无参的构造。原因是我们在字符串的构造中有缺省参数,即为空串。

3、2 返回大小和容量

  这两个部分,是比较容易实现的两部分。同时也是较为常用的两部分。具体如下:

		size_t size() const
		{
			return _size;
		}

		size_t capacity() const
		{
			return _capacity;
		}

3、3 拷贝构造和赋值重载

  这两部分较为复杂的两部分。其中均需要深拷贝去实现完成,而浅拷贝是不可以的。注意:拷贝构造使用一个已定义变量去初始化另一个变量,赋值重载是两个已定义变量进行赋值

具体实现如下:

		//深拷贝
		//string(const string& s)
		//	:_str(new char[s._capacity+1])
		//	,_size(s._size)
		//	,_capacity(s._capacity)
		//{
		//	strcpy(_str, s._str);
		//}

		void swap(string& tmp)
		{
			//调用全局的swap
			::swap(_str, tmp._str);
			::swap(_size, tmp._size);
			::swap(_capacity, tmp._capacity);
		}

		//借助变量tmp
		string(const string& s)
			:_str(nullptr) 
			, _size(0)
			, _capacity(0)
		{
			string tmp(s._str);
			swap(tmp);
		}

		//赋值
		//string& operator=(const string& s)
		//{
		//	if(this == &s)
		//	{
		//		return *this;
		//	}
		//	//先开空间拷贝数据,以防new失败销毁原来的空间
		//	char* tmp = new char[s._capacity + 1];
		//	strcpy(tmp, s._str);

		//	delete[] _str;
		//	_str = tmp;
		//	_size = s._size;
		//	_capacity = s._capacity;
		//	return *this;


		//	//delete[] _str;
		//	//_str = new char[s._capacity + 1];
		//	//strcpy(_str, s._str);
		//	//_size = s._size;
		//	//_capacity = s._capacity;
		//	return *this;
		//}

		//string& operator=(const string& s)
		//{
		//	if(this == &s)
		//	{
		//		return *this;
		//	}
		//	string tmp(s._str);
		//	swap(tmp);
		//  return *this;
		//}

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

   上述的辅助重载我们巧妙地借助了临时变量s。当赋值完成后,出了作用域s会自动调用戏后进行销毁,这里是需要反复理解的。

3、4 扩容(reserve)

  我们可简单的理解reserve为扩容(扩容的前提为要求的容量比原来的大),但是我们要记得把字符数组中原有的内容拷贝过来,并且释放之前所动态开辟的空间。 具体实现如下:

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

				_str = tmp;
				_capacity = capacity;
			}
		}

3、5 插入(push_back、append、operator+=、insert)

  插入的实现,主要的点就是是否要进行扩容。其次,当我们实现push_back和append后,其他的均可复用这两个结构进行实现。具体实现如下:

        void push_back(char ch)
		{
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}

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

		void append(const char* str)
		{
			size_t len = strlen(str);

			if (len + _size > _capacity)
			{
				reserve(len + _size >= _capacity * 2 ? len + _size : _capacity * 2);
			}

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

		void append(const string& s)
		{
			append(s._str);
		}

		void append(int n, char ch)
		{
			reserve(_size + n);
			for (int i = 0; i < n; i++)
			{
				push_back(ch);
			}
		}
		string& operator+= (char ch)
		{
			push_back(ch);

			return *this;
		}

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

			return *this;
		}

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

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

			//注意,当运算数一个是有符号,另一个是无符号时,有符号的运算数会强制类型转换为无符号数。pos等于0的位置插入,end--后为超大数据,会出错。
			//int end = _size;
			//while (end >= (int)pos)
			//{
			//	_str[end + 1] = _str[end];
			//	end--;
			//}

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

			size_t len = strlen(str);

			if (len + _size > _capacity)
			{
				reserve(len + _size >= _capacity * 2 ? len + _size : _capacity * 2);
			}


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

			for (int i = pos,j=0; j < len;j++, i++)
			{
				_str[i] = str[j];
			}
			_size += len;
			return *this;
		}

3、6 删除(erase)

  我们这里实现的从某个位置开始删除,删除长度为 len 的字符。len有一个缺省参数,为npos(npos是一个很大的数,也就是不传参给 len 的话,默认删除到最后)。如果 len 本就很大,删除的长度超过从pos开始所剩余的长度,那么默认也是pos后的删除完。那么我们看其具体的实现。

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

3、7 查找(find)

  查找的话,主要常用的就两个:从pos位置开始查找,查找的内容可能是一个字符,也可能是一个子串。如果找到,则返回其下标。没找到就返回npos。具体实现如下:

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

			return npos;
		}

		size_t find(const char* sub, size_t pos = 0)const
		{
			const char* ret=strstr(_str + pos, sub);
			if (ret == nullptr)
			{
				return npos;
			}
			else
			{
				return ret - _str;
			}
		}

3、8 返回子串(substr)

  返子串也是我们经常需要的一个接口。返回子串就一个接口,从某个位置开始查找,查找长度为 len 的字符。len有一个缺省参数,为npos(npos是一个很大的数,也就是不传参给 len 的话,默认返回到最后)。如果 len 本就很大,返回子串的长度超过从pos开始所剩余的长度,那么默认也是pos后的子串全部返回。我们看其具体实现:

        string  substr(size_t pos, size_t len = npos)const
		{
			assert(pos < _size);
			size_t realLen = len;
			if (len == npos || pos + len > _size)
			{
				realLen = _size - pos;
			}

			string s;
			for (size_t i = 0; i < realLen; i++)
			{
				s += _str[pos + i];
			}

			return s;
		}

3、9 迭代器(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;
		}

  这里的begin()就是返回的字符串的首元素地址end()返回的是字符串最后一个元素的后一个地址

3、10 比较(>、<、>=、<=、==、!=)

  字符串的比较并非比较其长度,而是与其相同位置字符的大小有关,也就是我们所说的字典序。我们这里只需要实现其中的两个,其他均可复用。具体如下:

		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 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 !(*this > s);
		}
		bool operator!= (const string& s) const
		{
			return !(*this == s);
		}

四、总结

  string 在C++中算是比较重要的了,也是入门时必须所学的容器。在平常中使用的频率较高,所以我们不仅要掌握其简单的用法,更应该去了解其底层的实现。这有助于我们后续的使用和理解。本篇文章列举出了string中常用的语法和接口底层的底层实现,这些都是我们应该熟练掌握的内容。

  本篇文章讲解就到这里,感谢观看ovo~

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

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

相关文章

mysql 开启binlog

1&#xff09;检查binlog功能是否有开启 mysql> show variables like log_bin; ---------------------- | Variable_name | Value | ---------------------- | log_bin | OFF | ---------------------- 1 row in set (0.00 sec) &#xff08;2&#xff09;如果显…

用视频制作gif动画的小窍门!知识分享

Gif动画图片是当下最为流行的一种图片格式&#xff0c;它的内容丰富画面生动&#xff0c;能够快速抓住人们的眼球。有非常好信息传递效果&#xff0c;但是很多小伙伴不知道这种gif动画怎么制作的。别担心&#xff0c;接下来小编就跟大家分享一下利用视频制作gif动画的小窍门&am…

Docker Compose资源限制

一、资源限制原因&#xff1a; 防止容器占用过多资源,影响其他容器或宿主机保证容器稳定运行,避免OOM等情况.OOM现象&#xff1a;根据优先机制kill掉宿主机上最高的进程从而来释放空间&#xff0c;只要是宿主机的进程都可能被kill掉的。进行资源隔离,提高安全性 二、Docker Com…

eclipse (C/C++) 常用设置记录

Eclipse 是一个开放源代码的、基于Java的可扩展开发平台&#xff1b;现公司用其作为开发单片机的IDE&#xff1b;因此记录一下常用的配置方法&#xff1b; 文章目录 零、常用默认快捷键一、高亮相同变量二、修改高亮变量颜色三、在整个工程内搜索某个函数四、切换主题五、改变字…

主分区,逻辑分区,扩展分区有什么区别

1.地位不同 逻辑分区属于扩展分区&#xff0c;扩展分区属于主分区。 给新硬盘上建立分区时都要遵循以下的顺序&#xff1a;建立主分区→建立扩展分区→建立逻辑分区→激活主分区→格式化所有分区。 2.位置不同 主分区又叫做引导分区&#xff0c;最多只能创建四个。 扩展分…

软考A计划-系统集成项目管理工程师-信息化知识(四)

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例点击跳转>软考全系列 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff…

人机接口回路原理(一)

一、人机接口框图 一般来说&#xff0c;微机保护的人机接口回路是指键盘、显示器及接口 CPU插件电路。人机接口回路的主要作用是通过键盘和显示器完成人机对话任务、时钟校对及与各保护CPU插件通信和巡检任务。在单CPU结构的保护中&#xff0c;接口 CPU就由保护CPU兼任。为了减…

什么是IO Pad?

1.什么是IO pad&#xff1f; IO pad是一个芯片管脚处理模块&#xff0c;即可以将芯片管脚的信号经过处理送给芯片内部&#xff0c;又可以将芯片内部输出的信号经过处理送到芯片管脚。输入信号处理包含时钟信号&#xff0c;复位信号等&#xff0c;输出信号包含观察时钟、中断等…

详细介绍Eclipse控制台Console使用说明

1.说明 本文详细介绍Eclipse控制台Console使用说明&#xff0c; 调试时通过控制台查看日志&#xff0c; 有时候日志太多会找不到上面的日志&#xff0c; 有时候几个控制台会不受控制的弹出&#xff0c; 那么请参考本文&#xff0c; 通过调整Eclipse控制台, 更高效方便的查看日志…

高效项目评审的6大注意事项

评审活动是软件项目降低风险的重要手段&#xff0c;但在实际评审活动中往往因主题不明确、现场争执不停&#xff0c;主持人无法有效控制会议流程等问题&#xff0c;最终导致项目评审活动效果大打折扣。 那么在项目评审过程中有什么注意事项&#xff0c;有助于完成高效的评审会议…

软件测试技能,JMeter压力测试教程,正则表达式提取(三)

目录 前言 一、场景案例 二、登录请求 三、正则提取器 四、引用参数 前言 接口关联&#xff0c;上一个接口返回的 token 作为下个接口的入参&#xff0c;除了前面一篇讲到的用 json 提取器提取&#xff0c;也可以用正则提取 json 提取器只能提取 json 格式的数据&#x…

vue3 + TS + elementplus + pinia实现后台管理系统左侧菜单联动实现 tab根据路由切换联动内容

效果图&#xff1a; home.vue页面代码 <template><el-container><el-aside width"collapse ? 200px : 70px"><el-button color"#626aef" click"collapseToggle()"><el-icon><Expand v-if"collapse"…

[进阶]Java:多线程:线程同步

线程同步 解决线程安全问题的方案。 线程同步的思想 让多个线程实现先后一次访问共享资源&#xff0c;这样就解决了安全问题。 线程同步的常见方案&#xff1a; 加锁&#xff1a;每次只允许一个线程加锁&#xff0c;加锁后才能进入访问&#xff0c;访问完毕后自动解锁&…

css 使用杂记

水平居中 条件&#xff1a;必须有宽度&#xff0c;不能无限宽 行内元素&#xff1a;通过设置父元素text-align:center块元素&#xff1a;要设置宽度&#xff0c;然后 左右margin值为“auto” 垂直居中 条件&#xff1a;父元素必须有高度&#xff0c;不能无限高 行内元素&#…

【Solr】体验极速安装solr

目录 前言 安装下载- 方式一&#xff1a;官网下载- 方式二&#xff1a;仓库下载 启动方式 快速使用 前言 solr是基于java开发的&#xff0c;所以solr需要用到jdk环境,并且solr需要在tomcat容器中才能运行,所以需要提前配置好jdk和tomcat环境。 安装下载 需要注意的是&#…

【vue+el-transfer】穿梭框实现及遇到的bug,已解决

昨晚加班写的穿梭框组件&#xff0c;一边写一边遇到bug&#xff0c;果然只有bug才会让你印象更深刻&#xff0c;更值得记录 封装成组件FreezeTransfer 效果如下&#xff1a; 主要参考了官网上可搜索的这个示例 先说遇到的bug&#xff0c;然后贴完整的代码 1、el-transfer数据…

layui下select下拉框不显示或没有效果

layui下select下拉框不显示或没有效果 弹层layer选择框没有样式_不可点击_渲染失效的解决办法 一、必须给表单体系所在的父元素加上 class"layui-form" 在一个容器中设定 class"layui-form" 来标识一个表单元素块&#xff0c;如果你不想用 form&#xf…

java 高校学生信息管理系统Myeclipse开发mysql数据库web结构jsp编程计算机网页项目

一、源码特点 JSP 高校学生信息管理系统 是一套完善的系统源码&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;以及相应配套的设计文档&#xff0c;系统主要采用B/S模式开发 。 研究的基本内容是基于Web的学生信息…

2023 最新版网络安全保姆级指南,手把手带你从零基础进阶渗透攻防工程师

一份网络攻防渗透测试的学习路线&#xff0c;不藏私了&#xff01; 2023最新500G《黑客&网络安全入门&进阶学习资源包》 1、学习编程语言(phpmysqljshtml) 原因&#xff1a;phpmysql 可以帮助你快速的理解 B/S 架构是怎样运行的&#xff0c;只有理解了他的运行原理才…

《机器学习公式推导与代码实现》chapter12-XGBoost

《机器学习公式推导与代码实现》学习笔记&#xff0c;记录一下自己的学习过程&#xff0c;详细的内容请大家购买作者的书籍查阅。 XGBoost 从算法精度、速度和泛化能力等性能指标来看GBDT&#xff0c;仍然有较大的优化空间。XGBoost是一种基于GBDT的顶级梯度提升模型。相较于…