【C++心愿便利店】No.12---C++之探索string底层实现
文章目录
- 前言
- 一、写实拷贝(了解)
- 二、string类常用接口实现
- 2.1 成员变量
- 2.2 默认构造函数
- 2.3 拷贝构造函数
- 2.4 operator==
- 2.5 operator[]
- 2.6 c_str
- 2.7 size()
- 2.8 capacity()
- 三、迭代器的实现
- 3.1 begin()和end()
- 3.2 范围for
- 四、string类增删查改
- 4.1 reserve():增容函数
- 4.2 push_back():尾插字符
- 4.3 append():追加字符串
- 4.4 operator+=
- 4.5 insert
- 4.6 erase
- 4.7 resize
- 4.8 find
- 4.9 substr
- 五、string类运算符重载
- 5.1 operator< == <= > >= !=
- 5.2 operator<<
- 5.3 operator>>
前言
👧个人主页:@小沈YO.
😚小编介绍:欢迎来到我的乱七八糟小星球🌝
📋专栏:C++ 心愿便利店
🔑本章内容:探索string底层实现
记得 评论📝 +点赞👍 +收藏😽 +关注💞哦~
提示:以下是本篇文章正文内容,下面案例可供参考
一、写实拷贝(了解)
写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。
二、string类常用接口实现
2.1 成员变量
class string
{
public:private:char* _str;size_t _size;size_t _capacity;const static size_t npos;——————————————————————————const static size_t npos=-1;//静态的成员变量是不可以给缺省值,必须在类外面进行初始化但是const静态的整形可以(特例)
};
const size_t string::npos = -1;//支持在类外面初始化
2.2 默认构造函数
string():_str (new char[1]{'\0'}),_size(strlen(0)),_capacity(0)
{}
//常量字符串规定后面默认就有\0
//strlen 计算的是字符串中有效字符的个数,不算 '\0',而常量字符串的结尾默认有一个 '\0',用 new开辟空间的时候需要多开一个用来存储结尾的 \0
string(const char* str=""):_size(strlen(str))//0,_capacity(_size)//0
{_str = new char[_capacity + 1];//1strcpy(_str, str);
}
对于上述代码中形参上必须加 const 修饰,这样才能用 C 语言中的常量字符串来初始化 string 类对象,上面两种初始化的方式都可以一个是无参的一个是有缺省值的,形参的的缺省值直接给一个空字符串即可如上述,要注意初始化列表是按照声明的顺序来初始化的。_capacity表示的是可以存储有效字符的容量,而字符串结尾默认的 ‘\0’ 并不算作有效字符,因此最初的 _capacity 就是形参 str 的长度。
🌟对于为什么不可以用 ‘\0’ “\0” 和nullptr当缺省值?
答案:
- 首先对于’\0’:str是一个char*类型,而’\0’是一个char类型的 类型不匹配
- 其次对于给缺省值nullptr:strlen是不会去检查空的,它是一直找到 \0为止的,也就相当于直接对这个字符串进行解引用了,这里的字符串又是空,所以会引发空指针问题。
- 最后对于"\0":它表示该字符串有一个字符 ‘\0’ ,它的结尾还有一个默认的 ‘\0’,因此有两个 ‘\0’
2.3 拷贝构造函数
//传统写法
string(const string& s)//拷贝构造
{_str = new char[s._size+1];strcpy(_str,s._str);_size = s._size;_capacity = s._capacity;
}
上述我们称为传统写法下面这种我们称为现代写法,对于现代写法不需要亲自去申请空间初始化,而是调用构造函数去完成。最后再将初始化好的 tmp 交换过来
还要注意:一定要通过初始化列表对 *this 进行初始化,不然交换给 tmp 后,里面都是随机值(不同编译器有的会处理有的不会),最终出了作用域 tmp 去销毁的时候就会出问题。
//现代写法:下面代码是两种不同的现代写法
不过要注意如果 string 对象中有 '\0',只会把 '\0' 前面的字符拷贝过去
void swap(string& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}
string(const string& s):_str(nullptr)//拷贝构造,_size(0),_capacity(0)//不处理tmp里面是随机值析构就会发合适呢个错误
{string tmp(s._str);//构造swap(tmp);
}
___________________________________________________________________________________________
void swap(string& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}
string(const string& s)//拷贝构造
{string tmp(s._str);swap(_str, tmp._str);swap(_size, tmp._size);swap(_capacity, tmp._capacity);
}
2.4 operator==
//传统写法
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;
}
对比于上述代码写法,下述这种写法通过调用拷贝构造来申请空间,在利用局部对象出了作用就会被销毁的特点,将需要释放的资源通过 swap 交换给这个局部变量,让这个局部变量帮我们销毁。
//现代写法:这里不能直接用 swap 交换两个 string 类对象,会导致栈溢出
void swap(string& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}
string& operator=(const string& s)
{if (this != &s){//string tmp(s);//调用拷贝构造string tmp(s._str);//调用构造swap(tmp);//这里s2换给了tmp本来s2要析构现在tmp出了作用域调用析构也就意味着原始的s2的空间释放了}return *this;
}
下述这种写法不用我们去调用构造或者拷贝构造,直接通过形参去调用,传值传参会调用拷贝构造,tmp是它的实参调用拷贝构造构造的一个一摸一样的空间。
//现代写法的优化版本
void swap(string& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}
string& operator=(string tmp)
{swap(tmp);return *this;
}
2.5 operator[]
这两个运算符重载函数构成函数重载,对象在调用的时候会找最匹配的,非const对象会调用非const版本,const 对象会调用const版本。
//可读可写版本
char& operator[](size_t pos)
{assert(pos < _size);return _str[pos];
}
//只可以读版本
const char& operator[](size_t pos) const
{assert(pos < _size);return _str[pos];
}
2.6 c_str
返回的是一个const char*的数组指针,只读不写,这个数组包含的字符序列与string对象的值相同,另外还包含一个以空字符(‘\0’)结尾的字符串,加上 const,这样普通的 string 类对象可以调用,const 类型的 string 类对象也可以调用,普通对象来调用就是权限的缩小。
const char* c_str()
{return _str;
}
2.7 size()
size_t size()const
{return _size;
}
2.8 capacity()
size_t capacity()const
{return _capacity;
}
三、迭代器的实现
iterator 是 string 类的内嵌类型,也可以说是在 string 类里面定义的类型,在一个类里面定义类型有两种方法,typedef 和 内部类。
3.1 begin()和end()
//非const调用
typedef char* iterator;//string 类的 iterator 是通过typedef来实现的
iterator begin()
{return _str;
}
iterator end()
{return _str+_size;
}
——————————————————————————————————————————————————————————
//const调用
typedef const char* const_iterator;
const_iterator begin()const
{return _str;
}
const_iterator end()const
{return _str + _size;
}
3.2 范围for
支持范围for写法,范围for的底层是迭代器实现的,但是范围for不是万能的,范围for遇上const类型的对象,会报错,因此要提供const迭代器:typedef const char* const_iterator;
string s1("hello world");
for (auto ch : s1)
{ch++;cout << ch << " ";
}
cout << endl;
四、string类增删查改
4.1 reserve():增容函数
reserve 函数不会进行缩容,因此在扩容前要先进程判断,只有当形参 n 大于当前容量的时候才扩容。
void reserve(size_t n)
{if (n > _capacity){char* tmp = new char[n + 1];//开n+1个是因为n个有效字符,另一个是'\0'strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}
}
4.2 push_back():尾插字符
对于尾插首先检查是否需要增容,如果需要就调用我们上面实现的 reserve 函数来进行扩容(选择2倍扩容),
扩容后将ch加到str上,然后 _size++ 最后手动添加一个新的 \0 。
void push_back(char ch)
{if (_size == _capacity){//reserve(_capacity * 2);//对于这里可以采用三目来判断_capacity是否为0,若不进行判断空串的_capacity是0,进行扩容0*0=0就会发生越界访问reserve(_capacity==0?4:_capacity * 2);}_str[_size] = ch;_size++;_str[_size] = '\0';
}
4.3 append():追加字符串
void append(const char * str)
{size_t len = strlen(str);if (_size +len> _capacity){reserve(_size+len);//这里就不需要担心_capacity为0的情况}strcpy(_str + _size, str);//strcpy会把'\0'也拷贝过去size += len;
}
4.4 operator+=
+=要有返回值返回*this
//追加一个字符复用push_back()
string& operator+=(char ch)
{push_back(ch);return *this;
}//追加一个字符串复用append()
string& operator+=(const char* str)
{append(str);return *this;
}
4.5 insert
在pos位置插入字符
void insert(size_t pos, char ch)
{assert(pos <= _size);if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}size_t end = _size;while (end >= pos){_str[end] = _str[end-1];--end;}_str[pos] = ch;_size++;
}
注意:上述代码挪动数据时的判断条件中,end 和 pos 都是 sizt_t 类型,例如当 pos = 0 的时候 end >= pos,end–,一直减到end=-1但是end是一个无符号整形,所以循环条件一直成立还可以进入循环, 所以下面有两种修改方式:
为了防止整型提升有以下两种写法:
void insert(size_t pos, char ch)
{assert(pos <= _size);if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}size_t end = _size+1;while (end > pos){_str[end] = _str[end-1];--end;}_str[pos] = ch;_size++;
}
——————————————————————————————————————————————————————————————————————————————————
void insert(size_t pos, char ch)
{assert(pos <= _size);if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}int end = _size;换成有符号也会报错因为操作符的两边两个数据的类型不同时会发生类型提升,end变成有符号类型也会被提升到无符号类型因为pos是无符号类型,所以可以强转pos->(int)poswhile (end >=(int) pos){_str[end] = _str[end-1];--end;}_str[pos] = ch;_size++;
}
在pos位置插入字符串
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+len;while (end >= pos+len){_str[end] = _str[end-len];--end;}strncpy(_str + pos, str,len);_size += len;}
}
4.6 erase
void erase(size_t pos, size_t len=npos)
{assert(pos < _size);if (len == npos||pos+len>=_size){_str[pos] = '\0';_size = pos;}else{size_t begin = pos + len;while (begin <= _size){_str[begin - len] = _str[begin];begin++;}_size -= len;}
}
4.7 resize
size_t find(char ch,size_t pos=0)//这里给了一个半缺省
{for (size_t i = pos; i < _size; i++){if (_str[i] == ch){return i;}}return npos;
}
4.8 find
size_t find(const char* str, size_t pos = 0)//半缺省
{const char* p = strstr(_str+pos, str);//strstr找到返回所在位置指针否则返回空if (p){return p - _str;//返回下标}else{return npos;}
}
4.9 substr
这里就表明我们一定要手写一个拷贝构造,编译器默认生成的拷贝构造是一个浅拷贝,会发生调用两次析构的问题所以要手写一个深拷贝
string substr(size_t pos, size_t len = npos)
{string s;size_t end = pos + len;if (len == npos || pos + len >= _size){len = _size - pos;end = _size;}s.reserve(len);for (size_t i = pos; i < pos + len; i++){s += _str[i];}return s;//s是一个浅拷贝出了作用域s销毁
}
五、string类运算符重载
5.1 operator< == <= > >= !=
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);
}
5.2 operator<<
因为类函数有this指针传参数容易发生错误匹配原因,>>和<<运算符重载要写在类外面
无论是形参还是返回值,只要涉及到 ostream 或 istream 都必须要用引用
//有以下两种写法:
ostream& operator<<(ostream& out,const string& s)
{for (size_t i = 0; i < s.size(); i++){out << s[i];}return out;
}
_______________________________________________________________________
范围for(这里使用范围for要调用const迭代器)
ostream& operator<<(ostream& out,const string& s)
{for (auto ch : s)//s是一个const对象要用const迭代器out << ch;return out;
}
5.3 operator>>
空格符 ’ ’ 和换行符 \n不能直接用 istream 对象来读取的,in >> ch 是读不到空格符和换行符。需要借助 get() 成员函数才能读取到空格符和换行符。
istream& operator>>(istream& in, string& s)
{s.clear();//清掉原始数据不然就变成尾插了char ch;//in >> ch;//拿不到空格或者换行例如sacnf拿不到空格所以出现了getcharch = in.get();while (ch!=' '&&ch!='\n'){s += ch;ch = in.get();}return in;
}
对于上面这种写法,在输入的字符串很长的情况下会多次调用 reserve 进行扩容,所以可以采用下述优化版本来实现:先开辟一个数组,将输入的字符存储到数组中,然后从数组中拷贝到string对象中,数组出了作用域就会销毁
istream& operator>>(istream& in, string& s)
{s.clear();//清掉数据不然就变成尾插了char buff[128];size_t i = 0;char ch;ch = in.get();while (ch!=' '&&ch!='\n'){buff[i++] = ch;if (i == 128){buff[i] = '\0';s += buff;i = 0;}}if (i != 0){buff[i] = '\0';s += buff;}return in;
}
相关文章:

【C++心愿便利店】No.12---C++之探索string底层实现
文章目录 前言一、写实拷贝(了解)二、string类常用接口实现2.1 成员变量2.2 默认构造函数2.3 拷贝构造函数2.4 operator2.5 operator[]2.6 c_str2.7 size()2.8 capacity() 三、迭代器的实现3.1 begin()和end()3.2 范围for 四、string类增删查改4.1 reser…...

Android Studio(列表视图ListView)
前言 前面在适配器章节,已经介绍了ListView的作用(干什么的),这节将主要介绍如何去设计ListView页面视图。 思考 列表视图需要些什么? 1. 列表项容器(装载各列表项的容器):<ListView/> 2. 列表项布局…...

让深度神经网络绘画以了解它们是如何工作的
一、说明 深度学习如此有效,这真是一个谜。尽管有一些关于深度神经网络为何如此有效的线索,但事实是没有人完全确定,并且深度学习的理论理解是一个非常活跃的研究领域。 在本教程中,我们将以一种不寻常的方式触及问题的一个小方面…...
https://www.jianshu.com/p/34bf240b85a9
https://www.jianshu.com/p/34bf240b85a9 https://www.eccee.com/soft-platform/991.html...

如何导出PPT画的图为高清图片?插入到world后不压缩图像的设置方法?
期刊投稿的时候,需要图片保持一定的清晰度数,那么我们怎么才能从PPT中导出符合要求的图片呢? 对于矢量图绘图软件所画的图,直接导出即可。 而PPT导出的图片清晰度在60pi,就很模糊。 整体思路: PPT绘图——…...

【Spring】Spring IOC DI
Spring IOC & DI IOC DI入门什么是Spring什么是容器什么是IOC IOC介绍传统程序开发解决方案 DI IOC详解Bean的存储Controller(控制器存储)Service(服务存储)Repository(仓库存储)Component(组件存储)Configuration(配置存储) 为什么需要这么多类注解类注解之间的关系方法注…...

一招解密网络流量瓶颈!
前言 我们曾介绍过观测云提供全面的基础设施监测方案(参见《全方位监控基础设施,坚实守护您的业务稳定!》),能够高效全面地帮助您实时观测所有的基础设施对象及云产品等,赋能您的业务稳定发展。今天我们将…...
某校帮签到小程序m 加密参数解析
小程序解密清参考我以前的文章 VX小程序逆向 js版本 function n(e, a) {var t (65535 & e) (65535 & a);return (e >> 16) (a >> 16) (t >> 16) << 16 | 65535 & t };function i(e, a, t, n, r, i, s) {return o(a & n | t &…...

Node.js |(六)express框架 | 尚硅谷2023版Node.js零基础视频教程
学习视频:尚硅谷2023版Node.js零基础视频教程,nodejs新手到高手 文章目录 📚express使用🐇初体验🐇express路由⭐️路由的使用⭐️获取请求参数⭐️获取路由参数🔥练习:根据路由参数响应歌手信息…...

包教包会:Mysql主从复制搭建
笑小枫的专属目录 一、无聊的理论知识1. 主从复制原理2. 主从复制的工作过程3. MySQL四种同步方式 二、docker下安装、启动mysql1. 安装主库2. 安装从库 三、配置Master(主)四、配置Slave(从)五、链接Master(主)和Slave(从)六、主从复制排错1. 错误:error connectin…...
Subset Selection
白话解释:https://www.geeksforgeeks.org/feature-subset-selection-process/ 貌似有一种比较常见的方法,称为多元逐步回归有3种筛选自变量的方法 (1)向前法:n个因变量情况,慢慢增加因变量到方程中&#x…...
【测开求职】面试题:计算机网络 精简版整理
本篇文章整理的是在秋招过程中遇到的计算机网络高频面试题,应付部分中小厂的测试开发工程师面试完全没有问题,如果时间充足的话,建议再看一下笔者的另外一篇文章:【测开求职】面试题:计算机网络 详细版整理,会让你对整个计算机网络有足够全面深刻的理解,亲测应付各个大厂…...
设计模式-代理模式(delegate)
什么是代理? 代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能. 这里使用到编程中的一个思想:不要随意去修改别人已经写好的代码或者方…...
MongoDB 安装与配置
MongoDB 安装与配置 MongoDB 是一个高性能、开源的 NoSQL 数据库,它提供了丰富的查询功能和高可用性。本文将详细讲解 MongoDB 的安装与配置过程。 1. MongoDB 安装 1.1 Windows 平台安装 下载 MongoDB 安装包 访问 MongoDB 官方下载页面(https://w…...

rabbitMq创建交换机,以及路由键绑定队列教程
创建交换机: 创建队列: 创建路由,绑定到交换机:...
odoo16前端框架源码阅读——ormService.js
odoo16前端框架源码阅读——ormService.js 路径:addons\web\static\src\core\orm_service.js 简单翻译一下代码中的注释: ORM服务是js代码和python的ORM层通信的标准方法。 然后讲了One2many and Many2many特使的指令格式,每个指令都是3元…...
详谈滑动窗口算法与KMP算法区别以及二者在什么场景下使用
什么是滑动窗口算法 滑动窗口算法是一种用于解决数组(或字符串)中子数组(或子字符串)问题的算法。该算法通过维护一个固定大小的窗口(通常是两个指针),该窗口在数组上滑动,以寻找符…...

k8s、数据存储
数据存储的概念 容器磁盘上的文件的生命周期是短暂的,这就使得在容器中运行重要应用时会出现一些问题。首先,当容器崩溃时,kubelet 会重启它,但是容器中的文件将丢失——容器以干净的状态(镜像最初的状态)…...

Vue生命周期全解析:从工厂岗位到任务执行,一览无遗!
🎬 江城开朗的豌豆:个人主页 🔥 个人专栏 :《 VUE 》 《 javaScript 》 📝 个人网站 :《 江城开朗的豌豆🫛 》 ⛺️ 生活的理想,就是为了理想的生活 ! 目录 ⭐ 专栏简介 📘 文章引言 一、生…...

常见产品结构四大类型 优劣势比较
一般,我们通过产品架构来构建用户体验,这样可以提供更清晰的导航和组织、优化用户流程和交互、增强产品的可扩展性和可维护性,提升用户的满意度和忠诚度。如果没有明确的产品结构,可能会导致功能冗余或功能缺失、交互流程混乱等问…...

华为云AI开发平台ModelArts
华为云ModelArts:重塑AI开发流程的“智能引擎”与“创新加速器”! 在人工智能浪潮席卷全球的2025年,企业拥抱AI的意愿空前高涨,但技术门槛高、流程复杂、资源投入巨大的现实,却让许多创新构想止步于实验室。数据科学家…...
java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别
UnsatisfiedLinkError 在对接硬件设备中,我们会遇到使用 java 调用 dll文件 的情况,此时大概率出现UnsatisfiedLinkError链接错误,原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用,结果 dll 未实现 JNI 协…...

【Redis技术进阶之路】「原理分析系列开篇」分析客户端和服务端网络诵信交互实现(服务端执行命令请求的过程 - 初始化服务器)
服务端执行命令请求的过程 【专栏简介】【技术大纲】【专栏目标】【目标人群】1. Redis爱好者与社区成员2. 后端开发和系统架构师3. 计算机专业的本科生及研究生 初始化服务器1. 初始化服务器状态结构初始化RedisServer变量 2. 加载相关系统配置和用户配置参数定制化配置参数案…...
蓝桥杯 2024 15届国赛 A组 儿童节快乐
P10576 [蓝桥杯 2024 国 A] 儿童节快乐 题目描述 五彩斑斓的气球在蓝天下悠然飘荡,轻快的音乐在耳边持续回荡,小朋友们手牵着手一同畅快欢笑。在这样一片安乐祥和的氛围下,六一来了。 今天是六一儿童节,小蓝老师为了让大家在节…...

Python爬虫(一):爬虫伪装
一、网站防爬机制概述 在当今互联网环境中,具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类: 身份验证机制:直接将未经授权的爬虫阻挡在外反爬技术体系:通过各种技术手段增加爬虫获取数据的难度…...
Device Mapper 机制
Device Mapper 机制详解 Device Mapper(简称 DM)是 Linux 内核中的一套通用块设备映射框架,为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程,并配以详细的…...
Go 并发编程基础:通道(Channel)的使用
在 Go 中,Channel 是 Goroutine 之间通信的核心机制。它提供了一个线程安全的通信方式,用于在多个 Goroutine 之间传递数据,从而实现高效的并发编程。 本章将介绍 Channel 的基本概念、用法、缓冲、关闭机制以及 select 的使用。 一、Channel…...

DingDing机器人群消息推送
文章目录 1 新建机器人2 API文档说明3 代码编写 1 新建机器人 点击群设置 下滑到群管理的机器人,点击进入 添加机器人 选择自定义Webhook服务 点击添加 设置安全设置,详见说明文档 成功后,记录Webhook 2 API文档说明 点击设置说明 查看自…...

springboot 日志类切面,接口成功记录日志,失败不记录
springboot 日志类切面,接口成功记录日志,失败不记录 自定义一个注解方法 import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;/***…...

VisualXML全新升级 | 新增数据库编辑功能
VisualXML是一个功能强大的网络总线设计工具,专注于简化汽车电子系统中复杂的网络数据设计操作。它支持多种主流总线网络格式的数据编辑(如DBC、LDF、ARXML、HEX等),并能够基于Excel表格的方式生成和转换多种数据库文件。由此&…...