【C++】深度解析:用 C++ 模拟实现 String 类,探索其底层实现细节

目录

了解string类

string的内存管理

VS下string的结构

​g++下string的结构

 string的模拟实现

string的构造函数

 浅拷贝

深拷贝

 string的遍历

重载 [] 下标访问

迭代器访问

reserve

resize

 增删查改

push_back()

append和+=

 insert和erase

find

substr

swap 

 流插入和流提取

getline

string其他基本功能


⭐了解string类

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) 的序列,这个类的所有成员( 如长度或大小 ) 以及它的迭代器,将仍然按照字节 ( 而不是实际编码的字符 ) 来操作。
总结:
1. string 是表示字符串的字符串类
2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作 string 的常规操作。
3. string 在底层实际是: basic_string 模板类的别名, typedef basic_string<char, char_traits, allocator>
string;
4. 不能操作多字节或者变长字符的序列。
使用 string 类时,必须包含 #include 头文件以及 using namespace std ;

⭐string的内存管理

✨VS下string的结构

string总共占28个字节 ,内部结构稍微复杂一点,先是 有一个联合体,联合体用来定义 string 中字
符串的存储空间
  • 当字符串长度小于16时,使用内部固定的字符数组来存放
  • 当字符串长度大于等于16时,从堆上开辟空间
union _Bxty
{ // storage for small buffer or pointer to larger onevalue_type _Buf[_BUF_SIZE];pointer _Ptr;char _Alias[_BUF_SIZE]; // to permit aliasing
} _Bx;
  1. 大多数情况下字符串的长度都小于16,当string对象创建好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。
  2. 还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量
  3. 有一个指针做一些其他事情。
  4. 故总共占16+4+4+4=28个字节。

✨g++下string的结构

g++ 下, string 是通过写时拷贝实现的, string对象总共占4个字节 ,内部只包含了一个指针,该指
针将来指向一块堆空间,内部包含了如下字段:
  • 空间总大小
  • 字符串有效长度
  • 引用计数
  • 指向堆空间的指针,用来存储字符串。
struct _Rep_base
{size_type _M_length;size_type _M_capacity;_Atomic_word _M_refcount;
};

 ⭐string的模拟实现

private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;

✨string的构造函数

// 为了和标准库区分,此处使用String
class String
{
public:/*String():_str(new char[1]){*_str = '\0';}*///String(const char* str = "\0") 错误示范//String(const char* str = nullptr) 错误示范String(const char* str = "")//默认包含 \0{// 构造String类对象时,如果传递nullptr指针,可以认为程序非法if (nullptr == str){assert(false);return;}_str = new char[strlen(str) + 1];strcpy(_str, str);}~String(){if (_str){delete[] _str;_str = nullptr;}}
private:char* _str;
};
// 测试
void TestString()
{String s1("hello bit!!!");String s2(s1);
}

 

上述 String 类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用 s1 s2 时,编译器会调用默认的拷贝构造。最终导致的问题是, s1 s2 共用同一块内存空间,在释放时同一块 空间被释放多次而引起程序崩溃 ,这种拷贝方式,称为 浅拷贝

 📖浅拷贝

浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来 。如果 对象中管理资源 ,最后就会 导致多个对象共 享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为 还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
举个例子,如果一个家庭里面有两个孩子,但是父母只给他们买了一个玩具,如果两个孩子都愿意玩这一个玩具,那就相安无事,否则就会鸡飞狗跳。
要解决这个问题,就可以直接给他们一人买一个玩具,这样各自安逸。
所以,要想解决浅拷贝的问题,可以使用深拷贝的方法, 每个对象都有一份独立的资源,不要和其他对象共享

📖深拷贝

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

 ✨string的遍历

📖重载 [] 下标访问

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

首先访问之前需要判断pos是否再合法访问之内,即小于等于size,然后直接返回字符串数组中对应的元素。由于存在const对象和非const对象,所以需要写两个重载版本。

📖迭代器访问

//迭代器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;}

 其实底层就是指针,所以直接返回对应的地址就可以了。

✨reserve

void reserve(size_t n)
{if (n > _capacity){char* tmp = new char[n+1];strcpy(tmp, _str);//释放旧空间,指向新空间delete[] _str;_str = tmp;//修改capacity,不用修改size_capacity = n;}
}

reserve是提前预留部分空间,它接收的空间大小不能比本来就有的容量小,如果n合法,则需要将原数组从旧空间移向一块更大的新空间,并释放掉旧空间。

✨resize

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

resize是设置字符串的大小,如果n比字符串原来的大小小,则会发生截断;如果比原来的大小大,则会reserve一块n大小的空间。

 ✨增删查改

📖push_back()

void push_back(char ch)
{//扩容2倍if (_size == _capacity){reserve(_capacity==0 ? 4 : 2 * _capacity);}_str[_size] = ch;_size++;_str[_size] = '\0';
}

直接尾插就可以,需要先判断空间是否足够,最后更新size的大小。

📖append和+=

void append(const char* str)
{//扩容//根据追加的字符串的长度扩容size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}strcpy(_str + _size, str);_size += len;
}
//  +=  
string& operator+=(char ch)
{push_back(ch);return *this;
} 
string& operator+=(const char* str)
{append(str);return *this;
}
string& operator+=(const string s)
{append(s._str);return *this;
}

append和+=都是在字符串的尾部追加字符或者字符串,需要先判断容量是否足够,不够则需要扩容, 根据追加的字符串的长度扩容。

 📖nsert和erase

对于insert,0位置的插入可能产生问题,end是int类型,pos是size_t类型,end变成-1与pos比较时会发生整型提升,所以pos需要先进行强制类型转换;也可以使用另一种解决方法,将end的初始值赋值为size+1,每次使用这种方法后移字符串 _str[end] = _str[end - 1];,则end最后不会变成-1。

//在pos之前插入
//插入字符
void insert(size_t pos,char ch)
{assert(pos <= _size);if (_size == _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity);}//int end = _size;0位置的插入可能产生问题,end变成-1与pos比较时会发生整型提升,所以pos需要先进行强制类型转换//while (end >= (int)pos)//{//	_str[end+1] = _str[end];//	end--;//}//第二种解决方法int end = _size + 1;while (end > pos){_str[end] = _str[end - 1];end--;}_str[pos] = ch;_size++;
}
//在pos之前插入
//插入字符串
void insert(size_t pos,const char* str)
{assert(pos <= _size);int len = strlen(str);if (_size +len > _capacity){reserve(_size + len+1);}//int end = _size;0位置的插入可能产生问题,end变成-1与pos比较时会发生整型提升,所以pos需要先进行强制类型转换//while (end >= (int)pos)//{//	_str[end+1] = _str[end];//	end--;//}//第二种解决方法//在pos之前插入int end = _size + len;//pos 1 2 endwhile (end > pos+len-1){_str[end] = _str[end - len];end--;}strncpy(_str + pos,str,len);_size+=len;
}
//释放删除
void erase(size_t pos, size_t len = npos)
{assert(pos < _size);// pos+len 存在溢出风险//if (len == npos || pos + len >= _size)if (len == npos ||len >= _size-pos){_str[pos] = '\0';_size = pos;}else{strcpy(_str + pos, _str + pos + len);_size -= len;}
}

对于erase,需要根据传递的参数的大小来判断需要删除多少个字符。

📖find

//寻找匹配
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
{assert(pos <= _size);const char* p=strstr(_str+pos, sub);if (p){return p - _str;}elsereturn npos;
}

实现方法比较简单,就是普通的暴力查找。 

📖substr

截取子串,需要注意len的大小。

        string substr(size_t pos = 0, size_t len = npos){string sub;if (len == npos|| len >= _size - pos){for (size_t i = pos; i < _size; i++){sub += _str[i];}}else{for (size_t i = pos; i < pos + len; i++){sub += _str[i];}}return sub;}

✨swap 

众所周知,C++算法库里面存在swap这个函数模板,但是为什么string内部自己也有一个swap呢?

如果用std::swap交换两个string对象,将会发生1次构造和2次赋值,也就是三次深拷贝;

而string内部的swap仅仅只交换成员,代价较小。

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

 为了符合算法库里面swap的用法,可以再将swap重载成全局函数。

	void swap(string& x, string& y){x.swap(y);}

✨ 流插入和流提取

//重载成全局是为了调整顺序//流插入ostream& operator<<(ostream& out, const string& s){//这里不需要写成友元函数,因为不需要直接访问私有成员for (auto ch:s){cout << ch;}return out;}//流提取//C++ 流插入,流提取可以支持自定义类型使用istream& operator>>(istream& in, string& s){s.clear();char ch;char buff[128];//in >> ch;//默认把空格当作分隔符、换行,不读取ch = in.get();//C++中读取一个字符size_t i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 127){buff[127] = '\0';s += buff;i = 0;}//s += ch;//重复+=,会重复扩容,消耗较大ch = in.get();}if (i > 0){buff[i] = '\0';s += buff;}return in;}

因为在这里不需要直接访问类的私有成员,所以流插入和流提取可以不用重载成string类的友元函数。

对于流提取,如果频繁的尾插,会造成频繁扩容。而且C++的扩容和C语言的扩容不一样,C++使用new不能原地扩容,只能异地扩容,异地扩容就会导致新空间的开辟、数据的拷贝、旧空间释放。为了防止频繁扩容,我们可以创建一个可以存储128字节的数组,作为缓冲,如果数组满了,则将这个字符数组追加到s上,如果没慢,但是遇到空格或者换行了也需要追加。

另外由于C++的标准输入流默认把空格和换行当作分隔符,不读取,所以这里要用in.get()来接收字符。

✨getline

基本上可以直接复用流提取的代码。

//读取空格istream& getline(istream& in, string& s){s.clear();char ch;//in >> ch;//默认把空格当作分隔符、换行,不读取ch = in.get();//C++中读取一个字符while ( ch != '\n'){s += ch;ch = in.get();}return in;}

✨string其他基本功能

        size_t size() const{return _size;}size_t capacity() const{return _capacity;}void clear(){_size = 0;_str[_size] = '\0';}

重载运算符,要写成全局的函数。

bool operator==(const string& a ,const string& b){int ret = strcmp(a.c_str(), b.c_str());return ret == 0;}bool operator<(const string& a, const string& b){int ret = strcmp(a.c_str(), b.c_str());return ret < 0;}bool operator<=(const string& s1, const string& s2){return  (s1 < s2) || (s1 == s2);}bool operator>(const string& s1, const string& s2){return  !(s1 <= s2);}bool operator>=(const string& s1, const string& s2){return  !(s1 < s2);}bool operator!=(const string& s1, const string& s2){return  !(s1 == s2);}

____________________

⭐感谢你的阅读,希望本文能够对你有所帮助。如果你喜欢我的内容,记得点赞关注收藏我的博客,我会继续分享更多的内容。⭐

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

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

相关文章

SpringBoot集成Seata分布式事务OpenFeign远程调用

Docker Desktop 安装Seata Server seata 本质上是一个服务&#xff0c;用docker安装更方便&#xff0c;配置默认&#xff1a;file docker run -d --name seata-server -p 8091:8091 -p 7091:7091 seataio/seata-server:2.0.0与SpringBoot集成 表结构 项目目录 dynamic和dyna…

智慧公厕管理系统的四层架构:感知层、传输层、平台层和应用层

智慧公厕管理系统是一种利用先进技术实现智能化管理和优化厕所体验的创新解决方案。该系统采用复杂的架构&#xff0c;涵盖了多个应用子系统&#xff0c;致力于提高公厕的卫生状况、资源利用效率、安全性以及用户体验。本文将以智慧公厕源头实力厂家广州中期科技有限公司&#…

汇编语言程序设计-2-访问寄存器和内存

2. 访问寄存器和内存 文章目录 2. 访问寄存器和内存2.0 导学2.1 寄存器及数据存储2.2 mov和add指令2.3 确定物理地址的方法2.4 内存的分段表示法2.5 Debug的使用2.6 【代码段】CS、IP与代码段2.7 【代码段】jmp指令2.8 【数据段】内存中字的存储2.9 【数据段】用DS和[address]实…

【自动驾驶技术栈学习】1-硬件《大话自动驾驶》| 综述要点总结 by.Akaxi

----------------------------------------------------------------------------------------------------------------- 致谢&#xff1a;感谢十一号线人老师的《大话自动驾驶》书籍&#xff0c;收获颇丰 链接&#xff1a;大话自动驾驶 (豆瓣) (douban.com) -------------…

618购物狂欢不知道怎么买?请收下这份好物清单,直接闭眼入!

在繁忙的618购物狂欢节来临之际&#xff0c;面对琳琅满目的商品&#xff0c;你是否感到无从下手&#xff1f;别担心&#xff0c;我们精心整理了一份好物清单&#xff0c;汇聚了各类热销与口碑兼具的精品。无论你是追求品质生活的消费者&#xff0c;还是寻找实惠好物的网购达人&…

实时网络监控 - 一目了然网络状况

网络问题排查一直是IT管理员头痛的问题。随着网络规模的不断扩大和业务复杂度的提升&#xff0c;如何快速定位和解决网络故障变得尤为关键。本文详细介绍了一款名为 AnaTraf 的网络流量分析工具,它能提供全流量回溯分析、实时网络监控、性能分析等功能,助力企业快速诊断和解决各…

AI绘画Stable Diffusion换脸插件ReActor 不香了,新一代换脸神器 InstantID!

前 言 之前我介绍了 SD 中的一款换脸插件 ReActor&#xff0c;虽然好使&#xff0c;但是安装还是有些许麻烦的。 今天给小伙伴们介绍一款新型的换脸插件&#xff1a;InstantID&#xff0c;主要是使用 ControlNet 和 IP-Adapter 的组合来控制扩散过程中的面部特征。 一句话&a…

Hive的join操作

假设有三张表&#xff0c;结构和数据如下&#xff1a;-- 创建表 test_a,test_b,test_c CREATE TABLE test_a( id int, name string ) ROW FORMAT DELIMITED FIELDS TERMINATED BY \t;--分别导入数据到三个表中 --test_a 1 a1 2 a2 4 a4 --test_b 1 b1 3 b3 4 b4 --…

【Vue探索之旅】初识Vue

文章目录 前言 渐进式框架​ 入门案例 完结撒花 前言 Vue (读音 /vjuː/&#xff0c;类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是&#xff0c;Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层&#xff0c;不仅易于上手&#x…

XWiki 服务没有正确部署在tomcat中,如何尝试手动重新部署?

1. 停止 Tomcat 服务 首先&#xff0c;您需要停止正在运行的 Tomcat 服务器&#xff0c;以确保在操作文件时不会发生冲突或数据损坏&#xff1a; sudo systemctl stop tomcat2. 清空 webapps 下的 xwiki 目录和 work 目录中相关的缓存 删除 webapps 下的 xwiki 目录和 work …

angular13 自定义组件全项目都可用 自存

1.定义自定义组件 使用命令创建一个组件 但删除它在你的module里的声明&#xff0c;因为会报错只能引用一次 在本组件中创建一个module文件&#xff0c;引入刚才的组件component.ts import { NgModule } from angular/core; import { CommonModule } from angular/common; im…

[ES] ElasticSearch节点加入集群失败经历分析主节点选举、ES网络配置 [publish_address不是当前机器ip]

背景 三台CentOS 7.6.1虚拟机&#xff0c; 每台虚拟机上启动一个ElasticSearch 7.17.3&#xff08;下面简称ES&#xff09;实例 即每台虚拟机上一个ES进程&#xff08;每台虚拟机上一个ES节点&#xff09; 情况是&#xff1a; 之前集群是搭建成功的, 但是今天有一个节点一…

函数编辑器调研及设计开发

前言&#xff1a;在产品研发中需要一款可嵌入web开发的代码及函数编辑器&#xff0c;本文从功能&#xff0c;扩展&#xff0c;外观/交互&#xff0c;维护/社区&#xff0c;兼容性&#xff0c;开源与否等方面考虑&#xff0c;进行对比筛选 1、编辑器统计数据 市面上编辑器有很…

Windows电脑使用Docker安装AList网盘神器并配置公网地址打造私人云存储空间

文章目录 前言1. 使用Docker本地部署Alist1.1 本地部署 Alist1.2 访问并设置Alist1.3 在管理界面添加存储 2. 安装cpolar内网穿透3. 固定Alist公网地址 前言 本文和大家分享如何在Windows系统使用Docker本地部署Alist全平台网盘神器&#xff0c;然后结合cpolar内网穿透工具实现…

USE INDEX/FORCE INDEX/IGNORE INDEX使用的好,sql查询速度提升一倍

前言 在实际工作中有些时候数据库的执行就很奇怪&#xff0c;要么不用索引&#xff0c;要么用了错误的索引&#xff0c;那么在这种情况下你就可以考虑使用这些索引提示来纠正数据库的行为。 早期我们有一个表结构定义&#xff0c;上面有 A、B 两个索引。原本按照预期&#x…

SCQA表达模型:让你的表达更具吸引力(2)

一、引言 站在巨人的肩膀上&#xff0c;思维模型是前人智慧结晶&#xff0c;当我们面对相似挑战时&#xff0c;借鉴与模仿这些模型&#xff0c;往往能为我们带来意想不到的结果。 在信息爆炸的时代&#xff0c;如何高效、准确地传达信息成为了人们关注的焦点。SCQA表达模型作为…

网络网络层之(5)IPv6协议

网络网络层之(5)IPv6协议 Author: Once Day Date: 2024年5月12日 一位热衷于Linux学习和开发的菜鸟&#xff0c;试图谱写一场冒险之旅&#xff0c;也许终点只是一场白日梦… 漫漫长路&#xff0c;有人对你微笑过嘛… 全系列文档可参考专栏&#xff1a;通信网络技术_Once-Day…

「JavaEE」多线程案例分析2:实现定时器

&#x1f387;个人主页&#xff1a;Ice_Sugar_7 &#x1f387;所属专栏&#xff1a;JavaEE &#x1f387;欢迎点赞收藏加关注哦&#xff01; 实现定时器 &#x1f349;简介&#x1f349;模拟实现定时器 &#x1f349;简介 定时器类似一个闹钟&#xff0c;时间到了之后就会执行…

重生我是嵌入式大能之串口调试UART

什么是串口 串口是一种在数据通讯中广泛使用的通讯接口&#xff0c;通常我们叫做UART (通用异步收发传输器Universal Asynchronous Receiver/Transmitter)&#xff0c;其具有数据传输速度稳定、可靠性高、适用范围广等优点。在嵌入式系统中&#xff0c;串口常用于与外部设备进…

MTATLAB--一元线性回归分析

一文让你彻底搞懂最小二乘法&#xff08;超详细推导&#xff09; 在进行一元线性回归分析时&#xff0c;使用最小二乘法进行解题&#xff0c;关于最小二乘法具体看上述文章。 数据文件在文章顶部可见&#xff0c;将第一列数据作为自变量x&#xff0c;第二列数据作为应变量y。建…