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;
}
想要遍历一个字符串,首先就要知道大小
,然后需要用方括号
来获取索引,或者用迭代器遍历,迭代器
的本质其实就是一个字符数组
🔥值得注意的是:
- 注意
size
函数和c_str
函数要具有普遍性,所以要包括const
变量的情况,,即使是普通类型调用
也是权限的缩小
,两种情况共用一个函数 operator[]
分为加const
和不加const
,分别代表只读
和可读写
- 同样迭代器也分为
iterator
和const_iterator
begin()
指向第一个有效字符,end()
指向最后一个有效字符的后一位
4.string类对象的扩容追加
void reserve(size_t n)
{// 检查请求的内存大小 n 是否大于当前的容量 _capacityif (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 是否等于其容量 _capacityif (_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 是否超过当前容量 _capacityif (_size + len > _capacity){// 如果超过容量,调用 reserve 函数进行扩容,扩容后的容量至少为 _size + lenreserve(_size + len);}// 将追加的字符串 str 复制到当前字符串 _str 的末尾位置(_str + _size)strcpy(_str + _size, str);// 更新当前字符串的实际长度,加上要追加的字符串的长度_size += len;
}string& operator+=(const char* str)
{append(str);return *this;
}
或许扩容添加的函数看起来操作简单,但其实其底层有许多细节
🔥值得注意的是:
reserve
传入的参数n
指的是有效字符,new
一个新空间时+1
是为了给'\0'
留位置,capacity
也表示的是有效字符的容量,同时要记得释放原来指向的不使用的空间push_back
函数reserve
时要判断下是因为扩容是*2
,避免空间为0
时扩容*2
导致出错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
类的插入删除都利用的是移动覆盖的思想,这里就不画图了,在数据结构阶段就已经学习了大致的思路
🔥值得注意的是:
- 这里使用
size_t
类型的end
作为索引来遍历字符串,size_t
是无符号整数类型。当end
递减到0
后再进行--end
操作时,会发生无符号整数溢出,end
的值会变成size_t
类型所能表示的最大值,这个值恰好和npos
(被初始化为-1
转换后的size_t
最大值)相等
如果没有end != npos
这个条件,当end
溢出后,end >= pos
仍然可能为真(因为溢出后的值很大),这就会导致循环继续执行,从而造成数组越界访问,引发未定义行为。加上end != npos
这个条件,当end
溢出变成npos
时,循环就会终止,避免了越界访问的问题 - 注意
capacity
在reserve
里已经修改过了,所以外面只需要再修改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 中读取一个字符并赋值给 chchar ch = in.get();//处理前缓冲区前面的空格和换行while (ch == ' ' || ch == '\n'){ch = in.get();}// 定义一个大小为 128 的字符数组 buff 用于临时存储字符,初始化为全 '\0'char buff[128] = { '\0' };// 定义一个索引变量 i 用于记录 buff 数组当前存储字符的位置,初始化为 0int i = 0;// 循环处理连续的空格和换行符while (ch == ' ' || ch == '\n'){// 将当前读取到的空格或换行符存入 buff 数组,并将索引 i 加 1buff[i++] = ch;// 检查 buff 数组是否快满(只剩下一个位置用于存储字符串结束符 '\0')if (i == 127){// 在 buff 数组末尾添加字符串结束符 '\0'buff[i] = '\0';// 将 buff 数组中的内容添加到字符串 s 中s += buff;// 重置索引 i 为 0,以便重新使用 buff 数组i = 0;}// 从输入流 in 中读取下一个字符并赋值给 chch = in.get();}// 如果 buff 数组中还有剩余字符(即 i 不为 0)if (i != 0){// 在 buff 数组末尾添加字符串结束符 '\0'buff[i] = '\0';// 将 buff 数组中的剩余内容添加到字符串 s 中s += buff;}// 返回输入流 in,以便支持链式输入操作return in;
}
🔥值得注意的是:
- 当放在自定义的命名空间以外时,需要在参数
string
前加作用域限定,不然默认访问了库里自带的string
- 由于不断的
+=
来输入字符要不断的更新空间,效率不高,所以采用开辟数组的方式
希望读者们多多三连支持
小编会继续更新
你们的鼓励就是我前进的动力!

相关文章:

C++效率掌握之STL库:string底层剖析
文章目录 1.学习string底层的必要性2.string类对象基本函数实现3.string类对象的遍历4.string类对象的扩容追加5.string类对象的插入、删除6.string类对象的查找、提取、大小调整7.string类对象的流输出、流提取希望读者们多多三连支持小编会继续更新你们的鼓励就是我前进的动力…...

【Erdas实验教程】004:影像镶嵌拼接
文章目录 一、实验目标二、实验数据三、实验过程一、实验目标 掌握具有坐标系且有重叠的多个影像的镶嵌。 二、实验数据 本实验数据为2景landsat TM影像和1景mss影像,如下所示: 数据获取方式:订阅专栏后,从私信查收。 三、实验过程 (1)启动镶嵌工具 在erdas中,常用…...

SpringMVC 请求参数接收
目录 请求 传递单个参数 基本类型参数传递 未传递参数 ?传递参数类型不匹配 传递多个参数 传递对象 后端参数重命名 传递数组 传递集合 传递JSON数据 JSON是什么 JSON的优点 传递JSON对象 获取URL中的参数 文件上传 在浏览器与程序进行交互时,主要…...
[高等数学]换元积分法
一、知识点 (一) 第一类换元法 定理1 设 f ( u ) f(u) f(u) 具有原函数, u φ ( x ) u\varphi(x) uφ(x) 可导,则有换元公式: ∫ f [ φ ( x ) ] φ ′ ( x ) d x [ ∫ f ( u ) d u ] u φ ( x ) . \int f[\varphi(x)]\varphi (x)dx[\int f(u)du]…...

Redis简介、常用命令及优化
文章目录 一、关系数据库??与非关系型数据库概述 1. 关系型数据库2. 非关系型数据库3.关系数据库与非关系型数据库区别 二、Redis简介 1.Redis的单线程模式2.Redis 优点3.Redis 缺点 三、安装redis四、Redis 命令工具五、Redis 数据库常用命令六、Redis 多数据库常用命令七、…...

大模型训练为什么依赖GPU
近年来,随着人工智能技术的飞速发展,特别是深度学习领域的进步,大模型的训练逐渐成为研究和工业界的热点。作为大模型训练中的核心硬件,GPU(图形处理单元)扮演了至关重要的角色。那么,为什么大模…...

帕金森病与三叉神经痛的基因关联分析
帕金森病(Parkinsons Disease, PD)和三叉神经痛(Trigeminal Neuralgia, TN)是两种不同的神经系统疾病,前者主要影响运动功能,而后者则表现为剧烈的面部疼痛。尽管这两种疾病在临床表现上有显著差异…...

【Android开发】华为手机安装包安装失败“应用是非正式版发布版本,当前设备不支持安装”问题解决
问题描述 我们将Debug版本的安装包发送到手机上安装,会发现华为手机有如下情况 解决办法 在文件gradle.properties中粘贴代码: android.injected.testOnlyfalse 最后点击“Sync now”,等待重新加载gradle资源即可 后面我们重新编译Debug安装…...

栈与队列(C语言版)
文章目录 栈与队列1. 栈基本操作实现(基于链表)代码运行结果 应用场景 2. 队列基本操作实现代码运行结果 应用场景 栈与队列 1. 栈 栈是一种操作受限的线性结构。操作受限体现在,栈只能在一端添加和删除元素,符合后进先出 ( LIFO ) 的特性,…...
stl里的deque 中控map 假如用完了,该如何处理
在 C 的标准模板库(STL)中,std::deque(双端队列)使用一种分段连续的存储结构,通过一个中控器(通常称为中控 map)来管理多个固定大小的存储块(缓冲区)。当这个…...

Git GUI设置中文的方法及使用
链接: Git Bash和Git GUI设置中文的方法 链接: Git 基本操作...

代码书写常用快捷建
唤出剪切板 Windows 系统 :Win V Mac 系统: 在 Mac 电脑上,可以点击桌面菜单栏中的 “编辑”,在下拉菜单中选择 “显示剪贴板” 来打开剪贴板。 跳转和撤回跳转 在vscode软件中可以通过ctrl+鼠标左键可以进行跳转…...
MySQL 索引失效处理:原因分析与优化实战
MySQL 索引失效处理:原因分析与优化实战 MySQL 索引失效处理:原因分析与优化实战引言一、什么是索引失效?二、索引失效的常见原因2.1 查询条件中使用函数或表达式示例:原因: 2.2 数据类型不匹配示例:原因&a…...
基于Python的AI代码审计工具实现方案,结合DeepSeek API和商业化设计
以下是一个基于Python的AI代码审计工具实现方案,结合DeepSeek API和商业化设计,分为基础功能版和进阶扩展方向: 基础版实现代码 (命令行工具) import os import requests from dotenv import load_dotenv import hashlib import json from t…...

用Python实现线性回归:从数学原理到代码实战
一、前言:为什么线性回归是AI必修课? 作为机器学习领域的"Hello World",线性回归算法具有三大核心价值: 1️⃣ 理解监督学习的底层逻辑(特征工程→模型训练→预测输出) 2️⃣ 掌握梯度下降等优化…...

系统可观测性(1)基础概念
系统可观测性(1)基础概念(Log/Tracing/Metrics) Author: Once Day Date: 2025年2月8日 一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦… 漫漫长路,有人对你微笑过嘛… 全系列文章可参考专栏: 十年代码训练…...

Redis未授权访问漏洞导致getshell
一、漏洞信息 redis默认情况下会绑定在本地6379端口,如果没有进行采用相关的策略,就会将redis服务暴露到公网上,如果再没有设置密码认证(一般为空)的情况下,会导致任意用户可以访问到目标服务器的情况下未授权访问redis以及读取r…...
Electron 全面解析:跨平台桌面应用开发指南
引言 在当今多平台并存的数字时代,如何高效开发跨平台桌面应用成为开发者面临的重要挑战。Electron作为GitHub开源的跨平台框架,凭借其独特的Web技术融合能力,已成为构建桌面应用的热门选择。本文将深入探讨Electron的核心原理、开发实践及未…...

React进阶之React核心源码解析(一)
React核心源码解析 react 特点CPU卡顿IO 卡顿 新老 react 架构对比v15v16.8Scheduler 调度器Reconciler 协调器 React fiber原理更新dommount 构建过程 render阶段 — scheduler reconcilerreact源码解析react-domreact-dom/src/client/ReactDOMRoot.js react-reconcilerreact-…...

用大模型学大模型03-数学基础 概率论 条件概率 全概率公式 贝叶斯定理
要深入浅出地理解条件概率与贝叶斯定理,可以从以下几个方面入手,结合理论知识和实例进行学习: 贝叶斯定理与智能世界的暗语 条件概率,全概率公式与贝叶斯公式的推导,理解和应用 拉普拉斯平滑 贝叶斯解决垃圾邮件分类 …...

日语AI面试高效通关秘籍:专业解读与青柚面试智能助攻
在如今就业市场竞争日益激烈的背景下,越来越多的求职者将目光投向了日本及中日双语岗位。但是,一场日语面试往往让许多人感到步履维艰。你是否也曾因为面试官抛出的“刁钻问题”而心生畏惧?面对生疏的日语交流环境,即便提前恶补了…...

Spark 之 入门讲解详细版(1)
1、简介 1.1 Spark简介 Spark是加州大学伯克利分校AMP实验室(Algorithms, Machines, and People Lab)开发通用内存并行计算框架。Spark在2013年6月进入Apache成为孵化项目,8个月后成为Apache顶级项目,速度之快足见过人之处&…...

Mybatis逆向工程,动态创建实体类、条件扩展类、Mapper接口、Mapper.xml映射文件
今天呢,博主的学习进度也是步入了Java Mybatis 框架,目前正在逐步杨帆旗航。 那么接下来就给大家出一期有关 Mybatis 逆向工程的教学,希望能对大家有所帮助,也特别欢迎大家指点不足之处,小生很乐意接受正确的建议&…...

苍穹外卖--缓存菜品
1.问题说明 用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大 2.实现思路 通过Redis来缓存菜品数据,减少数据库查询操作。 缓存逻辑分析: ①每个分类下的菜品保持一份缓存数据…...

微服务商城-商品微服务
数据表 CREATE TABLE product (id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 商品id,cateid smallint(6) UNSIGNED NOT NULL DEFAULT 0 COMMENT 类别Id,name varchar(100) NOT NULL DEFAULT COMMENT 商品名称,subtitle varchar(200) NOT NULL DEFAULT COMMENT 商…...
C++.OpenGL (10/64)基础光照(Basic Lighting)
基础光照(Basic Lighting) 冯氏光照模型(Phong Lighting Model) #mermaid-svg-GLdskXwWINxNGHso {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GLdskXwWINxNGHso .error-icon{fill:#552222;}#mermaid-svg-GLd…...

IoT/HCIP实验-3/LiteOS操作系统内核实验(任务、内存、信号量、CMSIS..)
文章目录 概述HelloWorld 工程C/C配置编译器主配置Makefile脚本烧录器主配置运行结果程序调用栈 任务管理实验实验结果osal 系统适配层osal_task_create 其他实验实验源码内存管理实验互斥锁实验信号量实验 CMISIS接口实验还是得JlINKCMSIS 简介LiteOS->CMSIS任务间消息交互…...
根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的----NTFS源代码分析--重要
根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的 第一部分: 0: kd> g Breakpoint 9 hit Ntfs!ReadIndexBuffer: f7173886 55 push ebp 0: kd> kc # 00 Ntfs!ReadIndexBuffer 01 Ntfs!FindFirstIndexEntry 02 Ntfs!NtfsUpda…...

nnUNet V2修改网络——暴力替换网络为UNet++
更换前,要用nnUNet V2跑通所用数据集,证明nnUNet V2、数据集、运行环境等没有问题 阅读nnU-Net V2 的 U-Net结构,初步了解要修改的网络,知己知彼,修改起来才能游刃有余。 U-Net存在两个局限,一是网络的最佳深度因应用场景而异,这取决于任务的难度和可用于训练的标注数…...

通过MicroSip配置自己的freeswitch服务器进行调试记录
之前用docker安装的freeswitch的,启动是正常的, 但用下面的Microsip连接不上 主要原因有可能一下几个 1、通过下面命令可以看 [rootlocalhost default]# docker exec -it freeswitch fs_cli -x "sofia status profile internal"Name …...