【C++杂货铺】探索string的底层实现

在这里插入图片描述

文章目录

  • 一、成员变量
  • 二、成员函数
    • 2.1 默认构造函数
    • 2.2 拷贝构造函数
    • 2.3 operator=
    • 2.4 c_str()
    • 2.5 size()
    • 2.6 operator[ ]
    • 2.7 iterator
    • 2.8 reserve
    • 2.9 resize
    • 2.10 push_back
    • 2.11 append
    • 2.12 operator+=
    • 2.13 insert
    • 2.14 erase
    • 2.15 find
    • 2.16 substr
    • 2.17 operator<<
    • 2.18 operator>>
    • 2.19 operator<
    • 2.20 operator==
    • 2.21 <=、>、>=、!=
  • 三、结语

一、成员变量

private:char* _str;//用来存储字符串size_t _size;//用来表示有效字符数size_t _capacity;//用来表示可以存储有效字符的容量
public:static size_t npos;//要在类外面定义

string本质上是一个动态顺序表,它可以根据需要动态的扩容,所以字符串一定是通过在堆上动态申请空间进行存储的,因此_str指向存储字符串的空间,_size用来表示有效字符数,_capacity用来表示可以存储有效字符的容量数。

二、成员函数

2.1 默认构造函数

string(const char* str = ""):_str(new char[strlen(str) + 1])//strlen计算的是有效字符的个数,而我们存储的时候要在字符串的最后存一个'\0',_size(strlen(str)),_capacity(_size)
{//memcpy(_str, str, _size);//strcpy(_str, str);//常量字符串就是遇到'\0'终止,所以直接用strcpy也可以memcpy(_str, str, strlen(str) + 1);
}

注意:默认构造函数需要注意的地方是:首先形参必须加上 const 修饰,这样才能用 C 语言中的常量字符串来初始化 string 类对象,形参的的缺省值直接给一个空字符串即可,注意空字符串是用""表示,该字符串只有结尾默认的一个 '\0'"\0"并不表示空字符串,它表示该字符串有一个字符 '\0' ,它的结尾还有一个默认的 '\0',因此有两个 '\0'nullptr也不能表示空字符串,他表示的是空指针。其次需要注意初始化列表的顺序,应该严格按照成员变量的出现顺序。strlen 计算的是字符串中有效字符的个数,不算 '\0',而常量字符串的结尾默认有一个 '\0',因此在用 new开辟空间的时候需要多开一个用来存储结尾的 \0_capacity表示的是可以存储有效字符的容量,而字符串结尾默认的 '\0' 并不算作有效字符,因此最初的 _capacity 就是形参 str 的长度。最后记得在构造函数体内将形参 str 的字符拷贝到动态申请的空间中。

小Tips:涉及到字符串拷贝的地方,建议使用 memcpystrcpy 默认遇到 \0 就终止,但是不排除 \0 就是 string 对象中的有效字符。但是 strcpy 会默认在结尾加 \0,而 memcpy 不会,因此使用 memcpy 的时候需要注意拷贝得到的字符串结尾是否有 \0

2.2 拷贝构造函数

//传统写法
string(const string& str):_str(new char[str._size + 1]),_size(str._size),_capacity(_size)
{memcpy(_str, str._str, str._size + 1);
}//现代写法
string(const string& str):_str(nullptr), _size(0),_capacity(0)
{string tmp(str._str);swap(tmp);
}

注意:现代写法不需要我们亲自去申请空间初始化,而是调用构造函数去帮我们完成。最后再将初始化好的 tmp 交换过来,这里一定要通过初始化列表对 *this 进行初始化,不然交换给 tmp 后,里面都是随机值,最终出了作用域 tmp 去销毁的时候就会出问题。现代写法的坑点在于,如果 string 对象中有 '\0',只会把 '\0' 前面的字符拷贝过去。

2.3 operator=

//传统写法
string& operator=(const string& s)
{if (this != &s){char* tmp = new char[s._capacity + 1];memcpy(tmp, s._str, s._size + 1);delete[] _str;_str = tmp;_size = s._size;_capacity = s._capacity;}return *this;
}

注意:这种写法需要我们自己去开辟空间新空间 tmp,自己去释放旧空间 _str,下面将对这种写法加以改进,通过已有的接口来帮我们完成这些工作。

//现代写法
string& operator=(const string& s)
{if (this != &s){string tmp(s);//通过调用拷贝构造来创建空间//tmp是局部变量,出了作用于会自动销毁,把待销毁的资源通过交换,给tmpstd::swap(_str, tmp._str);std::swap(_size, tmp._size);std::swap(_capacity, tmp._capacity);//std::swap(*this, tmp);//错误的写法}return *this;
}//现代写法优化
void swap(string& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}
string& operator=(string s)
{swap(s);return *this;
}
//优化版本,连拷贝构造函数也不需要我们自己去调用啦,直接通过形参去调用

注意:这种写法通过调用拷贝构造来帮我们申请空间,在利用局部对象出了作用就会被销毁的特点,将需要释放的资源通过 swap 交换给这个局部变量,让这个局部变量帮我们销毁。这里不能直接用 swap 交换两个 string 类对象,会导致栈溢出,因为 swap 函数中会调用赋值运算符重载,而赋值运算符重载又要调用 swap 成了互相套娃。我们可以不用库里面的 swap,自己实现一个 Swap 用来交换两个 string 对象。

2.4 c_str()

char* c_str() const
{return _str;
}

注意:记得加上 const,这样普通的 string 类对象可以调用,const 类型的 string 类对象也可以调用,普通对象来调用就是权限的缩小。

2.5 size()

size_t size() const
{return _size;
}

2.6 operator[ ]

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

注意:这两个运算符重载函数构成函数重载,对象在调用的时候会走最匹配的,普通对象会调用读写版本,const 对象会调用只读版本。

2.7 iterator

iteratorstring 类的内嵌类型,也可以说是在 string 类里面定义的类型,在一个类里面定义类型有两种方法,typedef 和 内部类。string 类的 iterator 是通过前者来实现的,即对字符指针 char* 通过 typedef 得到的。

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

2.8 reserve

void reserve(size_t n = 0)
{if (n > _capacity){char* tmp = new char[n + 1];//strcpy(tmp, _str);memcpy(tmp, _str, _size + 1);_capacity = n;delete[] _str;_str = tmp;}
}

2.9 resize

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

注意reserve 函数不会进行缩容,因此在扩容前要先进程判断,只有当形参 n 大于当前容量的时候才扩容。

2.10 push_back

void push_back(char ch)
{//先检查容量,进行扩容if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size++] = ch;_str[_size] = '\0';
}

注意:需要注意对空串的追加,空串的 _capacity = 0 ,因此在调用 reserve 函数进行扩容的时候,不能简单传递 _capacity*2,要先进行判断,当 capacity == 0 的时候,给它一个初始大小。

2.11 append

void append(const char* str)
{if (_size + strlen(str) > _capacity){reserve(_size + strlen(str));}//strcpy(_str + _size, str);//常量字符串就是遇到'\0'终止,所以直接用strcpy也可以memcpy(_str + _size, str, strlen(str) + 1);_size += strlen(str);
}

2.12 operator+=

//追加一个字符串
string& operator+=(const char* str)
{append(str);return *this;
}
//追加一个字符
string& operator+=(char ch)
{push_back(ch);return *this;
}

注意:+= 需要有返回值。

2.13 insert

//插入n个字符
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 != npos && end >= pos){_str[end + n] = _str[end--];}//插入数据size_t i = pos;while (i < pos + n){_str[i++] = ch;}_size += n;
}

注意:这里需要注意挪动数据时的判断条件,因为 endpos 都是 sizt_t 类型,所以当 pos = 0 的时候 end >= pos 永远成立,此时就会有问题,只把 end 改成 int 也解决不了问题,在比较的时候会发生整形提升,最终还是永远成立。一种解决方法就是想上面一样,加一个 size_t 类型的成员变量 npos,把它初始化成 -1,即整形最大值,判断 end 是否等于 npos,等于说明 end 已经减到 -1 了,就应该停止挪动。解决上面的问题还有一种方法,上面的问题出现在 pos = 0 时,end 会减到 -1,最终变成正的无穷大,导致判断条件永远成立,那我们可以将 end 初始化成 _size + n,把 end - n 上的字符挪到 end 位置上,此时计算 pos = 0,也不会出现 end 减到 -1 的情况,代码如下:

//插入n个字符
void insert(size_t pos, size_t n, char ch)
{assert(pos <= _size);//检查容量,扩容	if (_size + n > _capacity){reserve(_size + n);}//挪动数据size_t end = _size + n;while (end >= pos + n){_str[end] = _str[end - n];--end;}//插入数据size_t i = pos;while (i < pos + n){_str[i++] = ch;}_size += n;
}

小Tipsnpos作为一个静态成员变量,必须在类外面进行初始化(定义),并且不能在声明时给默认值,默认值是给初始化列表用的,而静态成员变量属于该类所有对象共有,并不会走初始化列表。但是!但是!!,整形的静态成员变量变量在加上 const 修饰后就可以在声明的地方给默认值,注意!仅限整形。其他类型的静态成员变量在加 const 修饰后仍需要在类外面定义。

const static size_t npos = -1;//可以
//const static double db = 1.1//不可以
//插入一个字符串
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;}//插入for (size_t i = 0; i < len; i++){_str[pos + i] = str[i];}_size += len;
}

2.14 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 end = pos + len;while (end <= _size){_str[end - len] = _str[end++];}_size -= len;}
}

注意pos 将整个数组划分成两部分,[0,pos-1]是一定不需要删除的区域,[pos,_size-1]是待删除区域,一定不需要删除的区域有 pos 个元素,我们希望删除 len 个字符,当一定不会删除的字符数加我们希望删除的字符数如果大于或等于全部的有效字符数,那就说明待删除区域的所有字符都要删除,即当 pos + len >= _size 的时候就是要从 pos 位置开始删除后面的所有字符,删完后加的把 pos 位置的字符置为 \0

2.15 find

//查找一个字符
size_t find(char ch, size_t pos = 0)
{assert(pos < _size);for (size_t i = 0; 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, str);if (ptr == NULL){return npos;}else{return ptr - _str;}
}

2.16 substr

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

2.17 operator<<

ostream& operator<<(ostream& out, const wcy::string& str)
{for (auto e : str){out << e;}return out;
}

注意:因为涉及到竞争左操作数的原因,流插入和流提取运算符重载要写在类外面。其次,不能直接打印 str._str 或者通过 str.c_str() 来打印,因为 string 对象中可能会有 \0 作为有效字符存在,前面两种打印方法,遇到 \0 就停止了,无法完整将一个 string 对象打印出来,正确的做法是逐个打印。

小Tips:无论是形参还是返回值,只要涉及到 ostreamistream 都必须要用引用,因为这俩类不允许拷贝或者赋值的。

2.18 operator>>

istream& operator>>(istream& in, wcy::string& str)
{if (str._size != 0){str.erase(0);}//in >> str._str;//这样写是错的,空间都没有char ch;ch = in.get();while (ch == ' ' || ch == '\n')//清除缓冲区{ch = in.get();}while (ch != ' ' && ch != '\n'){str += ch;ch = in.get();}return in;
}

注意:空格符 ' ' 和换行符 \n 作为输入时分割多个 string 对象的标志,是不能直接用 istream 对象来读取的,即 cin >> ch 是读不到空格符和换行符。需要借助 get() 成员函数才能读取到空格符和换行符。其次库中对 string 进行二次流提取的时候会进行覆盖,所以我们在插入前也要先进行判断。上面这种写法,在输入的字符串很长的情况下会多次调用 reserve 进行扩容,为了解决这个问题,我们可以对其进行优化。

//优化版本
istream& operator>>(istream& in, wcy::string& str)
{/*if (str._size != 0){str.erase(0);}*///in >> str._str;//这样写是错的,空间都没有str.clear();char buff[128] = { '\0' };char ch;ch = in.get();while (ch == ' ' || ch == '\n'){ch = in.get();}size_t i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 127){str += buff;i = 0;}ch = in.get();}if (i != 0){buff[i] = '\0';str += buff;}return in;
}

注意:这里的做法是,先开辟一个数组,将输入的字符存储到数组中,然后从数组中拷贝到 string 对象当中。

2.19 operator<

bool operator<(const string& s) const
{size_t i1 = 0;size_t i2 = 0;while (i1 < _size && i2 < s._size){if (_str[i1] < s[i2]){return true;}else if (_str[i1] > s[i2]){return false;}else{i1++;i2++;}}if (i1 == _size && i2 == s._size){return false;}else if (i1 < _size){return false;}else{return true;}
}

注意string 类对象是按照 ASCII 进行比较的。其次,这里不能直接复用 strcmp 或者 memcmp,前者遇到 '\0' 就会终止,后者只能比较长度相等的部分。所以我们可以自己来写比较逻辑,也可以复用 memcmp 然后进行补充。

//复用memcpy
bool operator<(const string& s) const
{int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);return ret == 0 ? _size < s._size : ret < 0;
}

2.20 operator==

bool operator==(const string& s) const
{return _size == s._size&& memcmp(_str, s._str, _size < s._size ? _size : s._size) == 0;
}

有了 < 和 ==,剩下的直接复用即可。

2.21 <=、>、>=、!=

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

三、结语

今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,春人的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是春人前进的动力!

在这里插入图片描述

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

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

相关文章

【微服务】spring 条件注解从使用到源码分析详解

目录 一、前言 二、spring 条件注解概述 2.1 条件注解Conditional介绍 2.2 Conditional扩展注解 2.2.1 Conditional扩展注解汇总 三、spring 条件注解案例演示 3.1 ConditionalOnBean 3.2 ConditionalOnMissingBean 3.2.1 使用在类上 3.2.2 使用场景补充 3.3 Condit…

jvm-类加载子系统

1.内存结构概述 类加载子系统负责从文件系统或网络中加载class文件&#xff0c;class文件在文件开头有特定的文件标识 ClassLoader只负责class文件的加载&#xff0c;至于它是否运行&#xff0c;则由Execution Engine决定 加载的类信息存放于一块称为方法区的内存空间&#xff…

什么是安全测试报告,怎么获得软件安全检测报告?

安全测试报告 软件安全测试报告&#xff1a;是指测试人员对软件产品的安全缺陷和非法入侵防范能力进行检查和验证的过程&#xff0c;并对软件安全质量进行整体评估&#xff0c;发现软件的缺陷与 bug&#xff0c;为开发人员修复漏洞、提高软件质量奠定坚实的基础。 怎么获得靠谱…

Hadoop学习:深入解析MapReduce的大数据魔力之数据压缩(四)

Hadoop学习&#xff1a;深入解析MapReduce的大数据魔力之数据压缩&#xff08;四&#xff09; 4.1 概述1&#xff09;压缩的好处和坏处2&#xff09;压缩原则 4.2 MR 支持的压缩编码4.3 压缩方式选择4.3.1 Gzip 压缩4.3.2 Bzip2 压缩4.3.3 Lzo 压缩4.3.4 Snappy 压缩4.3.5 压缩…

学会Mybatis框架:让你的代码更具灵活性、可维护性、安全性和高效性【二.动态SQL】

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于Mybatis的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.Mybatis动态SQL如何应用 1.需求 2.…

分布式锁 总结

分布式锁 在应用开发中&#xff0c;特别是web工程开发&#xff0c;通常都是并发编程&#xff0c;不是多进程就是多线程。这种场景下极易出现线程并发性安全问题&#xff0c;此时不得不使用锁来解决问题。在多线程高并发场景下&#xff0c;为了保证资源的线程安全问题&#xff0…

五、修改官方FreeRTOS例程(STM32F1)

1、官方源码下载 (1)进入FreeRTOS官网&#xff1a;FreeRTOS官网 (2)下载FreeRTOS。(选择带示例的下载) 2、删减目录 (1)下载后解压的FreeRTOS文件如下图所示。 (2)删除下图中红框勾选的文件。 FreeRTOS-Plus&#xff0c;FreeRTOS的生态文件&#xff0c;非必需的。tools&…

【数据库】表操作 习题总结

目录 关系建表 数据库sql的执行顺序 内外连接的写法 1.设计一张商品表 2.设计一张老师表 3.设计一张图书表 4.查询练习 5.查询练习 6.设计一个考勤系统 7.设计一个学校宿舍管理系统 8.设计一个车辆违章系统 9.设计一个学校食堂管理系统 10.有一张员工表emp&#xf…

开源TTS+gtx1080+cuda11.7+conda+python3.9吊打百度TTS

一、简介 开源项目&#xff0c;文本提示的生成音频模型 https://github.com/suno-ai/bark Bark是由Suno创建的基于变换器的文本到音频模型。Bark可以生成极为逼真的多语种演讲以及其他音频 - 包括音乐、背景噪音和简单的声音效果。该模型还可以产生非言语沟通&#xff0c;如…

Android 11 Display亮灭屏

系统休眠唤醒的时候会涉及到亮灭屏&#xff0c;下面分析下系统&#xff08;高通8155平台&#xff09;Display亮灭屏流程 1. 点亮屏幕 点亮屏幕入口在framework/base/下面LightsService.java&#xff0c;然后通过调用SurfaceControl.java ,最终调用到framework/native下面的Sur…

【洁洁送书第五期】为什么我们要了解可观测性工程

导读 可观测性已成为一个热门话题&#xff0c;并广受关注。随着它的普及&#xff0c;“可观测性”不幸被误作“监控”或“系统遥测”的同义词。可观测性是软件系统的一个特征。而且&#xff0c;只有当团队采用新的实践进行持续开发时&#xff0c;才能在生产软件系统中有效利用这…

Day14-2-NodeJS后端开发流程

Day14-NodeJS后端工程化流程 一 apifox工具 apifox是目前最好的接口调试工具 1 环境搭建 安装登录创建项目接口里面创建对应文件夹在指定的文件夹里面创建接口2 GET请求 1 apifox发送GET请求 2 后端接收GET请求 router.get("/getUserinfo"

Azure防火墙

文章目录 什么是Azure防火墙如何部署和配置创建虚拟网络创建虚拟机创建防火墙创建路由表&#xff0c;关联子网、路由配置防火墙策略配置应用程序规则配置网络规则配置 DNAT 规则 更改 Srv-Work 网络接口的主要和辅助 DNS 地址测试防火墙 什么是Azure防火墙 Azure防火墙是一种用…

EndNote(四)【文献引文格式、同步、智能分组、引文报告、文献笔记导出】

参考文献格式&#xff1a;&#xff08;官网引文格式下载文章引文格式更新-word&#xff09; 官网引文格式下载 网址&#xff1a;Downloads | EndNote 点击output styles&#xff1a; 下下来之后&#xff0c;放在这个路径下&#xff1a; 双击看一下&#xff1a; ok. 比如我们要…

引领行业高质量发展|云畅科技参编《低代码开发平台创新发展路线图(2023)》

8月8日-9日&#xff0c;中国电子技术标准化研究院于北京顺利召开《低代码开发平台创新发展路线图&#xff08;2023&#xff09;》封闭编制会。云畅科技、浪潮、百度、广域铭岛等来自低代码开发平台解决方案供应商、用户方、科研院所等近30家相关单位的40余位专家参与了现场编制…

mybatis动态SQL的运用

一、mybatis动态SQL update 表名 set name?,age? where id? 如果我们的前台没有传参&#xff0c;比如没有传入我们的name值&#xff0c;name就会把字段值改为null&#xff0c;这就违背了我们编码的初衷。 许多人会使用类似于where 1 1 来作为前缀&#xff0c;在代码中会用i…

STL---vector

目录 1.vector的介绍及使用 2.vector接口说明及模拟实现 2.1vector定义 2.2vector迭代器的使用 2.3vector容量 2.4vector增删查改 3迭代器失效 4.使用memcpy拷贝 5.模拟实现 1.vector的介绍及使用 vector的文档介绍 1. vector是表示可变大小数组的序列容器。 2. 就像数…

战略定位、战略咨询、战略定位咨询:一站式解决您的困惑

战略定位、战略咨询和战略定位咨询是三个密切相关但又不同的概念。它们都与企业的发展战略有关&#xff0c;但各自的侧重点不同。在这篇文章中&#xff0c;我们将详细介绍这三个词的定义&#xff0c;并为您提供一些实用的建议&#xff0c;帮助您更好地理解和应用这些概念。 战略…

ardupilot开发 --- 仿真篇

环境 安装wsl2&#xff0c;win11自带wsl&#xff0c;win10需要安装&#xff1b;git clone ardupilot 源码&#xff1b;安装 Linux下的build环境&#xff1b;安装 flightgear&#xff08;非必须&#xff09; sudo apt-get install flightgearbuild 想要仿真的载具类型&#xff…

漏洞挖掘和安全审计的技巧与策略

文章目录 漏洞挖掘&#xff1a;发现隐藏的弱点1. 源代码审计&#xff1a;2. 黑盒测试&#xff1a;3. 静态分析工具&#xff1a; 安全审计&#xff1a;系统的全面评估1. 渗透测试&#xff1a;2. 代码审计&#xff1a;3. 安全策略审查&#xff1a; 代码示例&#xff1a;SQL注入漏…