C++效率掌握之STL库:string底层剖析

文章目录

  • 1.学习string底层的必要性
  • 2.string类对象基本函数实现
  • 3.string类对象的遍历
  • 4.string类对象的扩容追加
  • 5.string类对象的插入、删除
  • 6.string类对象的查找、提取、大小调整
  • 7.string类对象的流输出、流提取
  • 希望读者们多多三连支持
  • 小编会继续更新
  • 你们的鼓励就是我前进的动力!

了解完 string 函数的主要用法,很有必要对 string 进行深层次的剖析,进一步了解其运作原理,深化理解的同时帮助我们在找 Bug 时提升效率

在学习本专题前,请详细学习有关 string 的使用

传送门:C++效率掌握之STL库:string函数全解

1.学习string底层的必要性

在 C++ 中,知道 string 是如何以字符数组的形式存储,以及字符串连接、查找等操作的时间复杂度,就可以避免在循环中频繁进行字符串连接操作,因为这可能会导致多次内存重新分配和数据复制,从而影响性能,而是选择更高效的方式,如预先分配足够的空间。同时,理解 string 底层对内存的管理方式,有助于优化内存使用,避免空指针和越界的情况出现

2.string类对象基本函数实现

实现一个类首先先从其基本函数开始,包括构造函数析构函数内存管理

namespace bit
{
	class string
	{
	public:
		string()//空构造
			:_size(0)
			, _capacity(0)
			, _str(new char[1])
		{
			_str[0] = '\0';
		}

		string(const char* str)//字符串构造
			:_size(strlen(str))
			,_capacity(_size)
			,_str(new char[_capacity + 1])
		{
			strcpy(_str, str);
		}
		
		string(const string& s)//拷贝构造
		{
			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}
		
		// 赋值运算符重载
        string& operator=(const string& s) {
            if (this != &s) {  // 避免自我赋值
                delete[] _str;  // 释放原有内存
                _size = s._size;
                _capacity = s._capacity;
                _str = new char[_capacity + 1];  // 分配新内存
                strcpy(_str, s._str);  // 复制内容
            }
            return *this;
        }

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

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

	private:
		size_t _size;
		size_t _capacity;
		char* _str;
	};
}

简单实现一个空构造字符串构造,因为还没写 string 流输出的运算符重载,先将 string 类转成 C 语言风格来输出

🔥值得注意的是: 注意变量声明的顺序要和初始化列表一致,也要注意变量初始化顺序对另一个变量的影响=运算符重载:自我赋值是指对象在赋值时被赋值给自己,例如 s1 = s1,在这种情况下,如果我们没有进行检查,就会先删除对象的内存,然后再试图复制同一个对象的内容,这样会导致程序崩溃。因此,重载赋值运算符时,自我赋值检查是非常必要的

3.string类对象的遍历

size_t size() const//加const保证const和普通string都能调用
{                  //增加普遍性,涉及权限的缩小
	return _size;
}

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

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

typedef char* iterator;
iterator begin()
{
	return _str;
}

iterator end()
{
	return _str + _size;
}

typedef const char* const_iterator;
iterator begin() const
{
	return _str;
}

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

想要遍历一个字符串,首先就要知道大小,然后需要用方括号来获取索引,或者用迭代器遍历,迭代器的本质其实就是一个字符数组

🔥值得注意的是:

  1. 注意 size 函数和 c_str 函数要具有普遍性,所以要包括 const 变量的情况,,即使是普通类型调用也是权限的缩小,两种情况共用一个函数
  2. operator[] 分为加 const 和不加 const ,分别代表只读可读写
  3. 同样迭代器也分为 iteratorconst_iterator
  4. begin() 指向第一个有效字符,end() 指向最后一个有效字符的后一位

4.string类对象的扩容追加

void reserve(size_t n)
{
	// 检查请求的内存大小 n 是否大于当前的容量 _capacity
	if (n > _capacity)
	{
		// 若 n 大于 _capacity,则分配 n + 1 个字符的内存空间
		// 多分配一个字符是为了存储字符串的结束符 '\0'
		char* tmp = new char[n + 1];

		// 将原字符串 _str 复制到新分配的内存区域 tmp 中
		strcpy(tmp, _str);

		// 释放原字符串 _str 所占用的内存空间
		delete[] _str;

		// 将 _str 指针指向新分配的内存区域 tmp
		_str = tmp;

		// 更新当前字符串的容量为 n
		_capacity = n;
	}
}

void push_back(char ch)
{
	// 检查当前字符串的实际字符数量 _size 是否等于其容量 _capacity
	if (_size == _capacity)
	{
		// 如果容量为 0,将容量扩展为 4;否则将容量扩大为原来的 2 倍
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}

	// 在字符串的末尾(即 _size 位置)添加新字符 ch
	_str[_size] = ch;

	// 实际字符数量加 1
	++_size;

	// 在新的字符串末尾添加字符串结束符 '\0'
	_str[_size] = '\0';
}

void append(const char* str)
{
	// 计算要追加的字符串的长度
	size_t len = strlen(str);

	// 检查当前字符串的实际长度 _size 加上要追加的字符串长度 len 是否超过当前容量 _capacity
	if (_size + len > _capacity)
	{
		// 如果超过容量,调用 reserve 函数进行扩容,扩容后的容量至少为 _size + len
		reserve(_size + len);
	}

	// 将追加的字符串 str 复制到当前字符串 _str 的末尾位置(_str + _size)
	strcpy(_str + _size, str);

	// 更新当前字符串的实际长度,加上要追加的字符串的长度
	_size += len;
}

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

或许扩容添加的函数看起来操作简单,但其实其底层有许多细节

🔥值得注意的是:

  1. reserve 传入的参数 n 指的是有效字符,new 一个新空间时 +1 是为了给 '\0' 留位置,capacity 也表示的是有效字符的容量,同时要记得释放原来指向的不使用的空间
  2. push_back 函数 reserve 时要判断下是因为扩容是 *2 ,避免空间为 0 时扩容 *2 导致出错
  3. push_back 通常只是添加一个字符,不会涉及修改,所以不用传 const 参数;append 的参数可能会被错误修改,所以要传 const 参数,普通的参数可以通过权限缩小正常使用函数

5.string类对象的插入、删除

void insert(size_t pos, size_t n, char ch)
{
	assert(pos <= _size);

	if (_size + n > _capacity)
	{
		reserve(_size + n);
	}

	size_t end = _size;
	while (end >= pos && end != npos)
	{
		_str[end + n] = _str[end];
		--end;
	}

	for (size_t i = 0; i < n; i++)
	{
		_str[pos + i] = ch;
	}
	_size += n;
}

void 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;
	while (end >= pos && end != npos)
	{
		_str[end + len] = _str[end];
		--end;
	}

	for (size_t i = 0; i < len; i++)
	{
		_str[pos + i] = len;
	}
	_size += len;
}

void erase(size_t pos, size_t len = npos)
{
	assert(pos <= _size);

	if (len == npos || pos + len >= _size)
	{
		_str[pos] = '\0';
		_size = pos;
		_str[_size] = '\0';
	}
	else
	{
		size_t end = pos + len;
		while (end <= _size)
		{
			_str[pos++] = _str[end++];
		}
		_size -= len;
	}
}

string 类的插入删除都利用的是移动覆盖的思想,这里就不画图了,在数据结构阶段就已经学习了大致的思路

🔥值得注意的是:

  1. 这里使用 size_t 类型的 end 作为索引来遍历字符串,size_t 是无符号整数类型。当 end 递减到 0 后再进行 --end 操作时,会发生无符号整数溢出,end 的值会变成 size_t 类型所能表示的最大值,这个值恰好和 npos(被初始化为 -1 转换后的 size_t 最大值)相等
    如果没有 end != npos 这个条件,当 end 溢出后,end >= pos 仍然可能为真(因为溢出后的值很大),这就会导致循环继续执行,从而造成数组越界访问,引发未定义行为。加上 end != npos 这个条件,当 end 溢出变成 npos 时,循环就会终止,避免了越界访问的问题
  2. 注意 capacityreserve 里已经修改过了,所以外面只需要再修改 size 就行了

6.string类对象的查找、提取、大小调整

size_t find(char ch, size_t pos = 0)
{
	assert(pos < _size);

	for (size_t i = pos; i < _size; i++)
	{
		if (_str[i] == ch)
		{
			return i;
		}
	}
	return npos;
}

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

	const char* ptr = strstr(_str + pos, str);
	if (ptr)
	{
		return ptr - _str;
	}
	else
	{
		return npos;
	}

}

string substr(size_t pos = 0, size_t len = npos)
{
	assert(pos < _size);

	size_t n = len;
	if (len == npos || len + pos > _size)
	{
		n = _size - pos;
	}
	string tmp;
	tmp.reserve(n);
	for (size_t i = pos; i < n; ++i)
	{
		tmp.push_back(_str[i]);
	}
	return tmp;
}

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

string 的查找操作比较简单,提取要注意提取的长度与原字符串长度的关系,调整大小也要注意 '\0' 的位置

🔥值得注意的是:

return ptr - _str:通过指针相减计算子字符串在原字符串中的起始位置索引。因为 ptr 指向子字符串的起始位置,_str 指向原字符串的起始位置,两者相减得到的差值就是子字符串的起始位置索引

7.string类对象的流输出、流提取

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

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

istream& operator>>(istream& in, string& s)
{
	// 清空字符串 s 原有的内容
	s.clear();
	// 从输入流 in 中读取一个字符并赋值给 ch
	char ch = in.get();
	//处理前缓冲区前面的空格和换行
	while (ch == ' ' || ch == '\n')
	{
		ch = in.get();
	}
	// 定义一个大小为 128 的字符数组 buff 用于临时存储字符,初始化为全 '\0'
	char buff[128] = { '\0' };
	// 定义一个索引变量 i 用于记录 buff 数组当前存储字符的位置,初始化为 0
	int i = 0;

	// 循环处理连续的空格和换行符
	while (ch == ' ' || ch == '\n')
	{
		// 将当前读取到的空格或换行符存入 buff 数组,并将索引 i 加 1
		buff[i++] = ch;
		// 检查 buff 数组是否快满(只剩下一个位置用于存储字符串结束符 '\0')
		if (i == 127)
		{
			// 在 buff 数组末尾添加字符串结束符 '\0'
			buff[i] = '\0';
			// 将 buff 数组中的内容添加到字符串 s 中
			s += buff;
			// 重置索引 i 为 0,以便重新使用 buff 数组
			i = 0;
		}
		// 从输入流 in 中读取下一个字符并赋值给 ch
		ch = in.get();
	}

	// 如果 buff 数组中还有剩余字符(即 i 不为 0)
	if (i != 0)
	{
		// 在 buff 数组末尾添加字符串结束符 '\0'
		buff[i] = '\0';
		// 将 buff 数组中的剩余内容添加到字符串 s 中
		s += buff;
	}
	// 返回输入流 in,以便支持链式输入操作
	return in;
}

🔥值得注意的是:

  1. 当放在自定义的命名空间以外时,需要在参数 string 前加作用域限定,不然默认访问了库里自带的 string
  2. 由于不断的 += 来输入字符要不断的更新空间,效率不高,所以采用开辟数组的方式

希望读者们多多三连支持

小编会继续更新

你们的鼓励就是我前进的动力!


请添加图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SCI学术论文图片怎么免费绘制:drawio,gitmind

SCI学术论文图片怎么免费绘制 目录 SCI学术论文图片怎么免费绘制overleaf怎么图片不清晰怎么办SCI学术论文图片怎么导出pdfdrawiogitmind**1. 使用在线工具****Lucidchart****2. Draw.io****3. ProcessOn****4. 使用桌面工具****Dia****5. 使用Markdown工具(如Typora)**如果你…

对于RocksDB和LSM Tree的一些理解,以及TiDB架构初识

LSM Tree的读写过程 HBase、LevelDB&#xff0c;rocksDB&#xff08;是一个引擎&#xff09;底层的数据结构是LSM Tree适合写多读少的场景&#xff0c;都是追加写入内存中的MemTable&#xff0c;写入一条删除&#xff08;或修改&#xff09;标记&#xff0c;而不用去访问实际的…

Java 设计模式之迭代器模式

文章目录 Java 设计模式之迭代器模式概述UML代码实现Java的迭代器 Java 设计模式之迭代器模式 概述 迭代器模式(Iterator)&#xff0c;提供一种方法顺序访问一个聚合对象中的各个元素&#xff0c;而又不暴露该对象的内部表示。 UML Iterator&#xff1a;迭代器接口&#xff…

【原创】解决vue-element-plus-admin无法实现下拉框动态控制表单功能,动态显隐输入框

前言 目前使用vue-element-plus-admin想要做一个系统定时任务功能&#xff0c;可以选择不同的定时任务类型&#xff0c;比如使用cron表达式、周期执行、指定时间执行等。每种类型对应不同的输入框&#xff0c;需要动态显隐输入框才行&#xff0c;但是这个vue-element-plus-adm…

上位机学习之串口通信与温湿度项目实战

文章目录 一、串口通信与温湿度项目实战1、学习串口通信硬件&#xff1a;巩固RS-485串口硬件和通信基础知识1.1、串行通信的数据流和格式1.2、串口通信参数设置1.3、modbus协议基础1.4、数据存储和功能代码1.5、modbus通信报文分析 2、主-从通信仿真测试2.1、组件设计2.2、创建…

深度求索—DeepSeek API的简单调用(Java)

DeepSeek简介 DeepSeek&#xff08;深度求索&#xff09;是由中国人工智能公司深度求索&#xff08;DeepSeek Inc.&#xff09;研发的大规模语言模型&#xff08;LLM&#xff09;&#xff0c;专注于提供高效、智能的自然语言处理能力&#xff0c;支持多种场景下的文本生成、对…

Zotero7 从下载到安装

Zotero7 从下载到安装 目录 Zotero7 从下载到安装下载UPDATE2025.2.16 解决翻译api异常的问题 下载 首先贴一下可用的链接 github官方仓库&#xff1a;https://github.com/zotero/zotero中文社区&#xff1a;https://zotero-chinese.com/官网下载页&#xff1a;https://www.z…

沃德校园助手系统php+uniapp

一款基于FastAdminThinkPHPUniapp开发的为校园团队提供全套的技术系统及运营的方案&#xff08;目前仅适配微信小程序&#xff09;&#xff0c;可以更好的帮助你打造自己的线上助手平台。成本低&#xff0c;见效快。各种场景都可以自主选择服务。 更新日志 V1.2.1小程序需要更…

Spring Boot (maven)分页3.0版本 通用版

前言&#xff1a; 通过实践而发现真理&#xff0c;又通过实践而证实真理和发展真理。从感性认识而能动地发展到理性认识&#xff0c;又从理性认识而能动地指导革命实践&#xff0c;改造主观世界和客观世界。实践、认识、再实践、再认识&#xff0c;这种形式&#xff0c;循环往…

解锁机器学习算法 | 线性回归:机器学习的基石

在机器学习的众多算法中&#xff0c;线性回归宛如一块基石&#xff0c;看似质朴无华&#xff0c;却稳稳支撑起诸多复杂模型的架构。它是我们初涉机器学习领域时便会邂逅的算法之一&#xff0c;其原理与应用广泛渗透于各个领域。无论是预测房价走势、剖析股票市场波动&#xff0…

广告深度学习计算:阿里妈妈大模型服务框架HighService

一、背景 HighService(High-Performance Pythonic AI Service) 是在支持阿里妈妈业务过程中&#xff0c;不断提炼抽象出的高性能Python AI服务框架&#xff0c;支持视频、图文、LLM等多种模型&#xff0c;能够显著加快模型的推理速度&#xff0c;提高集群的资源利用效率。随着S…

稀土抑烟剂——为汽车火灾安全增添防线

一、稀土抑烟剂的基本概念 稀土抑烟剂是一类基于稀土元素&#xff08;如稀土氧化物和稀土金属化合物&#xff09;开发的高效阻燃材料。它可以显著提高汽车内饰材料的阻燃性能&#xff0c;减少火灾发生时有毒气体和烟雾的产生。稀土抑烟剂不仅能提升火灾时的安全性&#xff0c;…

如何下载AndroidStudio的依赖的 jar,arr文件到本地

一、通过jitpack.io 下载依赖库 若需要下载 com.github.xxxxx:yy-zzz:0.0.2 的 jar则 https://jitpack.io/com/github/xxxxx/yy-zzz/0.0.2/ 下会列出如下build.logyy-zzz-0.0.2.jaryy-zzz-0.0.2.pomyy-zzz-0.0.2.pom.md5yy-zzz-0.0.2.pom.sha1jar 的下载路径为https://jitpack…

【仪器仪表专题】案例:示波器控制通道开关SCPI命令不同的原因

背景 在文章【仪器仪表专题】仪器支持SCPI控制,要怎么验证命令是否正确?-CSDN博客中我们提到SCPI命令的历史。并且提到了关于制造商为控制仪器而使用的命令,除了公共命令外,并没有统一的规则。比如同一位制造商生产的不同型号仪器甚至会采用不同的规则。 如下所示的众多仪器…

【Deepseek 零门槛指南】DeepSeek 教程和常见问题解答 | 大白技术控

粉丝朋友们大家好&#xff0c;我是极客学长。最近一直在玩 DeepSeek&#xff0c;积累了一点经验&#xff0c;用它提高写作的效率挺好用的。 在使用DeepSeek的过程中&#xff0c;也遇到了如下几个问题(相信很多小伙伴也遇到了)&#xff1a; DeepSeek 官网卡顿&#xff0c;突然出…

2025年SEO工具有哪些?老品牌SEO工具有哪些

随着2025年互联网的发展和企业线上营销的日益重要&#xff0c;SEO&#xff08;搜索引擎优化&#xff09;逐渐成为了提高网站曝光率和流量的重要手段。SEO的工作不仅仅是简单地通过关键词优化和内容发布就能够实现的&#xff0c;它需要依赖一系列专业的SEO工具来帮助分析、监测和…

怎么让DeepSeek自动化写作文案

在数字化时代&#xff0c;内容创作已成为企业争夺用户注意力的核心竞争力。面对海量信息需求&#xff0c;企业往往面临内容创作效率低下、质量参差不齐、周期长等问题。如何用技术手段解决这些痛点&#xff0c;成为企业迫切需要破解的难题。今天&#xff0c;我们将以DeepSeek为…

【vue3】实现pdf在线预览的几种方式

今天一天对当前可用的pdf预览插件做了测试&#xff0c;主要需求是只能预览不能下载&#xff0c;但对于前端来说&#xff0c;没有绝对的禁止&#xff0c;这里只罗列实现方式。 目前采用vue3版本为&#xff1a;3.2.37 iframevue-officepdfjs-dist iframe 先说最简单的&#xf…

软件测试之接口测试理论知识

文章目录 前言接口的定义接口的分类 接口测试什么是接口测试接口测试的基本原理为什么要进行接口测试&#xff1f;接口测试的测试范围&#xff08;测试维度&#xff09; 接口测试的流程1.需求分析2.接口文档分析接口文档分析要素 3.编写接口测试计划4.编写接口测试用例&评审…

生成式人工智能:技术革命与应用图景

(这文章有些地方看不懂很正常&#xff0c;因为有太多生词&#xff0c;需要对 计算机/人工智能 研究至深的人才能看懂&#xff0c;遇到不会的地方用浏览器搜索或跳过&#xff09; 引言 2023年被称我们为"生成式AI元年"&#xff0c;以GPT-4、DALL-E 3、Stable Diffusi…