【c++深入系列】:万字string详解(附有sso优化版本的string模拟实现源码)

🔥 本文专栏:c++

🌸作者主页:努力努力再努力wz


💪 今日博客励志语录当你想放弃时,想想为什么当初坚持走到了这里

★★★ 本文前置知识:

类和对象(上)

类和对象(中)
类和对象(下)


STL

那么在我的上一篇博客中,我着重讲解了模版,那么有了模版之后,那么c++的设计者在模版的基础上引入创建了STL,那么想必你在学习c++之前也一定听说过STL的鼎鼎大名,那么所谓的STL,就是c++为我们提供的一个算法与数据结构的标准库,那么其中有我们很多常见的数据结构的实现比如顺序表以及链表还有栈和队列等等,那么在以前学习c语言的时候,这些数据结构都需要我们自己来手写实现,但是对于c++来说,那么它就已经帮你把这些工作给完成了,也就是说,你听说过的绝大部分的数据结构,那么c++的标准库中都有对应的实现,那么我们要做的就是站在巨人的肩膀上前行,也就是调用前人为我们写好的数据结构来解决更多复杂的问题,而不需要我们再去浪费时间在自己去造轮子了,那么STL中的各种数据结构有一个更为专业并且形象的名字来称呼,那么便是容器

那么STL除了能够提供容器,那么它还会提供各种针对于相关容器的一些算法,其中最为熟知的便是sort函数也就是排序算法,那么它底层是采取快速排序来实现,并且默认是以升序进行排列,并且能够针对不同的容器来进行排序,除了sort,那么STL还提供了swap函数,swap是交换不同数据类型的任意两个数,那么其实底层采取的就是模版实现,还有返回两个数的较大值的max函数,那么STL中相关的算法都在algorithm头文件中,我在往后的博客中也会依次来介绍

那么STL除了大家最为熟悉的容器以及算法,那么还提供了各种迭代器,那么对于没学习或者没接触过STL的初学者来说,那么可能会对迭代器感到陌生,那么这里迭代器我们可以简单理解为它就是以一种统一接口来遍历各个不同的容器,那么至于底层如何实现,那么我会在往后的博主介绍,那么这里我们主要就是来初步的认识STL

那么STL还会提供分配器以及适配器和函数对象,那么这部分内容就是作为了解即可,那么对于初学者来说,目前还接触不到,那么目前我们学习STL的内容核心是围绕容器以及算法和迭代器这三个内容的学习上,那么有了STL之后,那么你会发现STL对于初学者带来的最大的一个帮组之一就是体现在写算法题上,因为算法题经常会用到各种数据结构比如顺序表以及栈等等,那么如果你用c语言来写算法题的话,那么你第一步肯定得是先实现这个数据结构以及这个数据结构相关操作的函数,那么你还在磨刀的时候,而对于其他用c++的人来写算法题的话,那么此时他们就已经在砍柴了,他们只需要引入STL库中包含特定容器的头文件即可,所以STL的意义不言而喻

那么本文作为STL系列的第一篇博客,那么要介绍的第一个容器便是string,那么string严格意义上来说其实并不属于STL库中的容器中的一种,由于历史原因,那么string的诞生是早于STL库的出现,那么所以就会导致string有一些内容是不符合STL的规范,那么看到下文你便会知道string究竟是哪部分内容不满足STL的要求,那么string为了适应STL库,采取了向下兼容的方式,所以string在广义上是可以属于STL库的,那么了解了string的一个历史背景之后,那么接下来我们就来全方位的来认识string

那么这里推荐一个学习STL很好用的一个网站(这里不是打广告):https://cplusplus.com/reference/,该网站是c++的官方文档网站,并且该网站的排版以及涉及到的内容都很全面,而且稳定也没什么广告,那么当我们对于某一个容器,或者容器中的某个成员函数感到陌生的话,那么我们可以打开这个网站,像查字典一样去搜索即可

string的意义

那么在想必读者在好奇string如何使用以及其底层实现之前,那么首先的第一个疑问肯定是为什么要有string

那么我们知道string就是字符串,那么我们现实生活中很多内容比如一个人的姓名以及一个商品的名称,那么这些现实生活中的内容在计算机中的映射就是以字符串的形式存在,那么由于计算机底层只能存储0和1组成的二进制序列而不会存储诸如ABCD这样的字符符号,所以为了表示出字符,那么采取的就是一个编码的方式,那么所谓的编码其实就是将建立一个所谓的映射表,那么将这些特定的字符给映射对应一个特定值的二进制序列,其中最为常见的编码便是ASCII,那么ASCII码将26个英文字符以及一些常见的符号比如'*','$'等都建立了对应的映射,比如小写的a字符在底层对应的就是值为97的二进制序列,那么你会发现阿斯克码只是解决了美国人关于他们表达所需要的符号的映射关系,而没有包含汉字等其他语言符号的映射,所以之后便有诞生了UTF-8编码等等来解决中文符号的映射的需求,那么对于char类型的数据,那么它采取的就是阿斯克码

那么对于c语言来说,那么它表示一个字符串,采取的就是一个字符数组的形式,那么数组的每一个元素是char类型,其中在最后一个字符的结尾会添加一个\0作为标记,因为c语言的很多针对字符串操作相关的函数会获取到到字符串的起始位置,但是它不知道该字符串的结束位置,所以就以\0来作为结尾位置的标记

而我们有两种方式来存储字符串,那么第一种方式便是定义一个固定大小的静态数组来存储字符串,那么在定义的时候,就需要我们显示的在字符串末尾添加\0,其次我们定义完静态数组之后,那么我们要对该字符串进行比如增删改查等等操作,那么我们只能使用c语言为我们提供的str系列函数,比如你现在有一个字符串拼接的需求,那么此时你可以调用strcat函数来满足这个需求

c 复制代码
char *strcat(char *dest, const char *src);

但是对于strcat函数来说,那么它会接收两个参数,第一个参数是指向被拼接的字符串的首元素的字符指针,第二个参数则是指向拼接的字符串的首元素的字符指针,但是strcat函数在设计的时候明显有一个缺陷,因为strcat工作的原理,就是将被拼接的字符串依次拷贝到原字符串的末尾,直到遇到拷贝到拼接字符串的\0位置结束,但是该函数不会检查被拼接字符串剩余空间的容量是否能够容纳拼接的字符串的长度,所以就会有越界的风险,所以相较于strcat函数来说,那么更为安全的函数则是strncat函数

c 复制代码
char *strncat(char *dest, const char *src, size_t n);

那么它还会接收第三个参数,也就是指定要拷贝拼接字符串的长度,那么它通过第三个参数来及时截断,避免越界的风险

那么知道c语言如何实现字符串的增功能,那么对于字符串的拷贝,那么这里c语言则是提供了strcpy函数,那么这个函数会接收一个指向拷贝的目标字符串的首元素的的字符指针以及一个指向被拷贝的字符串的首元素的字符指针,那么它的工作原理,就是遍历被拷贝的字符串,然后将拷贝的字符串的字符拷贝到对应位置直到遇到\0结束

同样对于strcpy函数来说,它也有一个明显的缺陷,就是该函数也不会检查目标字符串的容量是否能够完整拷贝新的字符串,所以也会有越界的风险

其次如果我们要对字符串进行分割的话,那么c语言则是提供了strtok函数,那么它接收一个分割的目标字符串以及分隔符字符串,那么这里strtok函数的调用就比其他的str系列的函数比如strcpy以及strcat函数要复杂,而strtok函数的内部会维护一个静态的指针,来保存下一次分割的起始地址,然后从分割的起始地址开始,来对每一个字符进行遍历,如果该字符是分隔符,那么就将该字符设置为\0,然后返回分割的起始地址

所以第一次调用strtok函数的时候就需要我们传一个指向原本字符串的起始地址,然后下一次调用的时候就不需要传指针了,只需传递一个空指针NULL即可,因为内部通过静态的指针保存了下一次要分割的位置,而如果你不熟悉strtok函数的实现原理的话,那么这里如果你调用了strtok函数分割了字符串str1,然后再调用strtok函数去分割字符串str2的话,那么此时第二次调用就会覆盖之前第一次调用的静态指针所保存的str1下一次分割的起始位置的内容,那么你再一次调用strtok函数来分割str1的话,就会导致分割出错

c 复制代码
//错误用法
char* token1=strtok(str1,"|");
char* token2=strtok(str2,"|");
token1=strtok(NULL,"|");

其次对于strtok,那么如果你分割的字符串的长度不为1,比如是这种情况:

c 复制代码
char* token=strtok(str1,"*|");

那么注意,此时这里strtok函数会将'*'和'|'视作两个独立的分隔符来处理,如果str1是这种形式的字符串:

c 复制代码
"hello*k*k*|woo"

那么此时就会被分割成[hello\0,k\0,k\0,woo\0]这四个部分,那么你本来的需求是想要以",|"整体作为分割符,但是这里strtok的行为则是将每个字符拆分作为单独的分隔符,只要匹配到其中任意一个分割符就进行返回


所以刚才上文讲了我认为c语言中str系列函数中最容易使用出错的三个函数,不仅刚才带你回顾了这三个函数如何使用以及注意的坑,更重要的是我想传达的一个观点,那么就是c语言为我们提供的str系列函数其实是并不完美的,其中是有很多的缺陷的,比如strcat以及strcpy的越界问题,那么就十分考验我们对于这些str函数底层的原理实现的一个理解以及掌握程度,其次我们如果采取的静态数组来存储字符串的话,第一要注意的点是就是得手动设置字符串结尾的\0,如果忘记设置\0,那么后果不用多说,其次对于是静态固定大小的数组的话,那么你还会面临一个问题,比如我添加一个字符串,此时这个该静态数组的容量不够又该怎么办,而静态数组无法进行扩容,而如果你采取的是动态数组的话,那么c语言的str系列函数都没有支持检查容量以及扩容的逻辑,那么意味着你要实现所谓的扩容的话,那么你得手动造轮子了


所以在这样的c语言的字符串这样的大背景下,那么string类就诞生了,首先string类采取的就是动态数组的方式实现,从上文我们便知道静态数组实现肯定是不可取的,其次string类内部为自动维护字符串的合法性,也就是它无论进行什么操作,比如字符串的增以及删或者改,那么它都会自动的在字符串末尾添加\0,并且字符串增删改查的相关成员函数在string内部都有提供,并且它们都支持容量的检查以及扩容的逻辑,意味着其功能实现的十分完善并且安全,所以string的诞生就是解决了c语言的字符串的各种缺陷,并且无需我们自己手动来维护,那么这些工作在底层都有对应的实现,那么我们只需要放心的对字符串做增删改查等操作即可

2.string如何使用

那么这里知道了string的意义之后,那么接下来我就会从两个维度来你认识string,首先就是我会给你展示string的一些常见的成员函数如何使用,那么展示完之后,我们再来模拟实现string,不仅能够帮组我们更清楚string的底层来加深我们对于string的理解和使用,还可以来锻炼一下类和对象的相关知识

那么对于string类来说,那么它内部的成员函数超过了50个,所以这里我们学习string的时候没必要全部掌握所有的成员函数,只需要掌握最常用的那几个就可以了,那么既然是string类,那么第一首先要谈到肯定则是构造函数

构造函数

那么对于string来说,那么它提供了多个重载版本的构造函数来满足用户不同的初始化需求,那么这里我们我们关照使用频率最高的几个重载版本的构造函数,那么第一个则是接收一个C风格的字符串

cpp 复制代码
string(const char* str);

使用示例:

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s1("WangZhuo");
 cout<<s1<<endl;
}

其次是拷贝构造函数:

cpp 复制代码
string(const string& str);

使用示例:

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;
int main()
{
 string s1("Pengyuyan");
  string s2(s1);
 cout<<s2<<endl;
  return 0;
}

最后就是一个无参数的构造函数,那么它则是构建一个空字符串

cpp 复制代码
string();

2.size函数

那么由于string是采取动态数组的方式来实现的,那么内部会封装一个指向在堆上开辟的动态数组的首元素地址的字符指针以及一个记录字符串有效长度的size变量还有记录动态数组当前容量的capacity,那么size函数的作用就是返回其中的size成员变量

cpp 复制代码
size_t size();

使用示例:

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;
int main()
{
    string s1("WangZhuo");
    cout<<s1.size()<<endl;
}

那么这里其实还有一个length函数,那么它的功能和size函数是一模一样,那么上文我们就说过由于历史原因,string的诞生是早于STL的出现,那么当时的的设计者认为length能够更好的表达字符串的长度的含义所以选择了这个名字,但是随着的STL出现,其统一了容器的命名规范,那么容器中存储的有效数据的大小都用size来表示,而这里string为了适应STL,那么添加了size函数,但是也向下兼容了length,所以这就是上文为什么说在严格意义上string不属于的容器的一部分,那么这里还是建议读者统一使用size函数

3.capacity函数

那么capacity函数的作用就是返回动态数组目前的容量,这里就不在过多赘述了

cpp 复制代码
str.capacity();

4.reserve函数

那么reserve函数的使用频率则很高,那么它的作用就是我们可以调用reserve预先开辟好一定大小的一个动态数组,来减少扩容,那么它则是接收一个size_t的参数,那么该参数就是我们要开辟的空间的大小

cpp 复制代码
void reserve(size_t n);

那么要注意的就是,如果我们申请的空间比原来的capacity要大的话,那么此时就会扩容,那么如果申请的空间比capacity小,比如现在的capacity是100,结果你调用reserve申请50的空间,那么注意这里不会有缩容的行为

5.push_back函数

那么push_back函数的作用就是尾插一个字符,那么这里将字符插入到字符串的末尾

cpp 复制代码
void push_back(char ch);

6.append函数

push_back函数则是追加字符到原字符串的末尾,那么append的函数则是追加一个字符串到原字符串的末尾,那么这里append也提供了多个重载版本,那么其中最为常用的便是追加一个c风格的字符串以及追加一个string

cpp 复制代码
void append(const char* str);
void appned(const string& str);

7.运算符重载函数

下标访问运算符重载函数

那么这里string还提供了多个运算符重载函数,那么我们知道string的底层是采取的数组的方式来实现的,那么我们访问一个string内的字符串的每一个字符,那么通过[]运算符来访问就非常的直观与形象,所以这里string重载了[]运算符,那么我们可以按照数组的方式来访问遍历字符串的每一个字符或者修改字符串的每一个字符

cpp 复制代码
char& operator[](size_t pos);
const char& operator[](size_t pos);

那么这里[]运算符重载函数那么支持了两个重载版本,分别针对普通的string类型以及const修饰的string类型,那么对于第一种则是可读可写,那么对于第二种则是只能读不能写

使用示例:

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;
int main()
{
    string s1="WangZhuo";
for(int i=0;i<s1.size();i++)
{
    cout<<s1[i];
}
    return 0;
}

加等运算符重载函数:

其次string还重载了加等运算符,那么其功能就是拼接字符串,那么对于加等重载运算符来说,string提供了两个重载版本,那么加等运算符重载函数能直观的表示出字符串的拼接,提供代码的可读性

cpp 复制代码
string& operator+=(const char* str);
string& operator+=(const string& str);

使用示例:

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;
int main()
{
   string s1;
    s1+="Pengyuyan";
    cout<<s1<<endl;
    return 0;
}

加运算符重载函数:

那么这里加运算符重载函数与加等运算符重载函数的区别就是加等运算符是在原字符串的空间中拼接字符串,而对于加运算符来说,则不是在原字符串上拼接,而是会创建一个原字符串拷贝得到的临时对象,用该临时对象来进行字符串的拼接,然后将其返回拷贝左值

cpp 复制代码
string operator+(const char*);
string operator+(const string& str);

赋值运算符重载函数:

那么string也重载了赋值运算符重载函数,那么要注意的就是string内部是采取动态数组的方式实现的,所以赋值运算符重载函数底层的实现原理采取的是深拷贝而不是浅拷贝,那么这里赋值运算符也提供了多个重载版本,那么作为常见的就是用字符串或者string类来赋值

cpp 复制代码
string operator=(const char* str);
string operator(const string& str);

8.迭代器

那么在学习各种容器的过程中,我们一定会接触到迭代器,那么所谓的迭代器就是访问容器的一个接口,也就意味着我们可以通过迭代器来访问容器中存储的各个元素,那么对于string来说,由于string内部将动态数组等成员变量定义为了私有成员变量,那么我们不可以通过实例化的对象来访问私有的成员变量,所以string内部就提供了一个接口来满足我们读取或者修改string内部保存的字符串的各个字符的需求,那么就是通过迭代器的方式来访问,那么string内部的迭代器本质上就是一个字符指针,由于string底层的构造很简单,采取的就是动态数组的方式来实现,所以迭代器的构造也很简单,本质就是一个字符指针,只不过typedef重命名成了iterator,那么对于其他容器比如链表乃至后面会学的set和map等容器,那么他们本身的结果就很复杂,所以这些容器所对应的迭代器的实现甚至封装成了一个类

cpp 复制代码
typedef char* iterator;
typedef const char* const_iterator;

那么这里string内部提供了两种迭代器,第一种是针对访问非const修饰的string对象,那么可以通过该迭代器来访问并且可以修改string对象中字符串的各个字符的内容,而第二个迭代器则是针对的是const修饰的string对象,那么该迭代器就只能访问const修饰string对象的字符串中的各个字符,也就是只能读,不能写

那么我们知道了迭代器的本质是一个字符指针,所以定义的时候就得初始化指向字符串的某个位置,所以string内部提供了两个接口,分别是begin函数以及end,那么从名字就可以看出这两个函数的作用,那么这两个函数分别返回字符串的起始位置以及字符串的结束位置或者说\0所在的位置,那么这样我们就可以通过begin函数以及end函数初始化迭代器,从而来遍历字符串

cpp 复制代码
char* begin();
const char* begin() const;
char* end();
const char* end() const;

那么这里begin以及end也重载两个版本分别满足非const修饰以及const修饰的string对象

要注意的是迭代器的声明是放在string类的类域中,所以我们在使用迭代器的时候,也要指定类域

cpp 复制代码
string::iterator it=s1.begin();
while(it!=s1.end())
{
    cout<<*it;
    it++;
}

所以在引入了迭代器之后,那么此时我们有两种方式来遍历一个string对象中保存的字符串,第一种则是通过下标访问运算符[]重载函数来遍历,第二种则是这里的迭代器,但是我更推荐第一种方式来访问,因为更为直观并且可读性更高,虽然迭代器使用上来说确实没有下标访问运算符来的方便,但是也不要忽视迭代器的作用

,比如在下文所说的一个场景:

那么我们知道c++支持一个语法糖,也就是范围for,范围for不需要设置for循环遍历的次数,那么范围for的底层就是利用迭代器来实现,那么编译器会将其处理为调用迭代器来接收一个begin函数的返回值,然后遍历直到匹配end函数的返回值结束,那么这里我们可以通过查看汇编代码,或者在下文我会模拟实现一个string类,也可以来验证,当使用自己实现的一个string类来实现范围for,如果我将begin函数或者end函数给注释掉,那么看编译器此时还能否编译通过,甚至我们都可以不需要注释,也就是稍微改一下函数的名字,比如将end改成End函数,那么你会发现看似编译器很智能很聪明,其实对于范围for的底层实现来说,还是很死板的,那么这里我就先埋下一个伏笔,那么读者可以心里可以先记住,那么我在后文的彩蛋部分,来验证这个情况

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;
int main()
{
    string s1="WangZhuo is good";
    //你的视角
    for(auto ch:s1)
    {
        cout<<ch;
    }
    //编译器的视角
    /*
    string::iterator it=s1.begin();
    while(it!=s1.end())
    {
    cout<<*it;
    it++;
    }
    */
    return 0;
}

9.insert函数

之前的append函数以及push_back函数都只能是在原始字符串的尾部插入一个字符串或者一个字符,那么这里insert函数本质上也是插入字符,但是它可以在字符串有效长度内的任意位置插入字符或者字符串,那么这里string内部提供了多个insert函数的重载版本,那么这里我就讲解使用频率最高的几个insert函数的重载版本

cpp 复制代码
void insert(size_t pos,const char* str);
void insert(size_t pos,const string& str);
void insert(size_t pos,size_t n,char ch);

那么这里第一个重载版本就是在字符串有效长度内的任意位置插入一个C风格的字符串,第二个则是插入string对象中保存的字符串,第三个则是在字符串有效长度内插入任意n个字符


那么insert的底层实现会涉及到元素的移动以及复制,那么z字符串有效长度内的任意一个位置插入一个长度为k的字符串,而在第一个位置插入的时间复杂度是最坏的,因为之后的所有字符串全部得往后移动,那么每个字符串移动的距离都是k,那么意味着insert函数的时间复杂度是O(n*k),最坏能达到o(N^2)级别,而对于尾插来说,那么不会涉及到元素的移动,那么时间复杂度是o(1),但是我们已经有尾插的相关函数了比如push_back以及append,不会专门用insert来实现尾插,所以这里的建议就是尽量少去使用insert函数去插入元素

10.erase函数

那么既然有插入,那么肯定就会有删除函数,那么erase函数就是用来删除字符串有效长度内的任意长度的子串,那么这里erase函数也提供了多个重载版本,那么我认为使用频率最高的就是以下两个重载的erase函数

cpp 复制代码
void erase(size_t pos,size_t len=npos);
void erase(iterator first,iterator last);

那么第一种方式就是从pos位置开始删除n个字符,那么这里注意这里第二个参数len提供了一个npos的缺省值,那么这里npos是string内部的一个静态的size_t的成员变量,其值为-1,而我们知道size_t是无符号整形,那么-1对应的无符号整形就是整数最大值,所以npos的意义就是用来表示一个无效值,那么这里如果我们没有提供第二个参数,那么第二个参数会使用缺省值也就是npos,那么npos肯定远远大于从pos位置开始的剩余的字符串的长度,所以此时就会被解读为直接从pos位置删除之后所有的内容

其次如果我们给的len大于pos位置之后的字符串的长度的话,那么也会采取删除pos位置之后所有的字符来处理

那么对于第二种方式,传递了是两个迭代器,那么上文我们说过可迭代器本质是一个字符指针,那么这里我们传递两个字符指针一个指向删除的起始位置,另一个指向删除的结束位置,那么这两个指针构成一个左闭右开的区间,那么erase则是删除这个左闭右开的区间中的所有内容

使用示例:

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;
int main()
{
    string s1="WangZhuo";
//第一个重载版本
s1.erase(0,2);
   cout<<"s1"<<s1<<endl;
//第二个重载版本
string::iterator first=s1.begin();
string::iterator last=s1.end();
s1.erase(first,last);
 cout<<"s1:"<<endl;
    return 0;
}

11.resize函数

那么这里的resize函数的作用就是来修改字符串的有效内容,那么这里假设该string对象中的字符串的长度是20,那么此时我们希望该字符串的有效长度为10的话,那么此时我们就可以调用resize函数来将字符串的长度给截取为10,而我们知道string底层是采取动态数组的方式来实现的,那么此时resize函数不会去缩小动态数组的容量,只是去修改size成员变量的大小,并且截取后的字符换末尾添加\0

而如果我们此时是这种场景,那么字符串的长度是20,而capacity是50,那么我们修改字符串的长度为40,那么此时注意此时resize的行为,那么它不会去扩容,因为容量足够,那么这时候resize会更新size的值,那么之后新增加的20个长度的内容默认以\0来填充,当然我们也可以指定字符来填充这新增加的20个长度的内容

而如果此时capacity是50,你resize的值是大于50的话,比如你resize的长度是80,那么此时就会涉及到扩容

cpp 复制代码
resize(size_t len);
resize(size_t len,char ch);

使用示例:

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;
int main()
{
    string s1="WangZhuo";
    cout<<"s1 size is "<<s1.size()<<endl;
//第一个重载版本
s1.resize(3);
  cout<<"s1 size is "<<s1.size()<<endl;
    cout<<s1<<endl;
//第二个重载版本
s1.resize(10,'x');
    cout<<"s1 size is "<<s1.size()<<endl;
    cout<<s1<<endl;
    return 0;
}

12.substr函数

那么最后想说的就是substr函数,那么它是用来分割子串的函数,那么它会接收两个参数,第一个参数就是分割子串的起始位置,那么该位置一定得在字符串的有效长度之内,那么第二个参数便是分割子串的长度

cpp 复制代码
string substr(size_t pos,size_t len=npos);

那么这里注意的是第二个参数len提供了缺省值,那么当我们传递第二个参数的时候,那么len会采取npos,那么npos是远大于pos位置之后的字符串的长度的,所以这里substr所做的行为就是将pos位置之后的子串给全部提取出来,同理如果你的len长度大于pos位置之后的子串长度所做的行为也是一样

使用示例:

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;
int main()
{
    string s1="WangZhuo";
    cout<<s1.substr(2,3)<<endl;
    return 0;
}

那么上面这12个关于string类的成员函数的介绍以及展示就到此结束,那么上面的这些成员函数肯定没有涵盖完所有的string的成员函数,但是上面的这些成员函数就已经涵盖了关于字符串的操作的基本功能,那么掌握这些就基本上能够可以把string给玩起来了,那么对string感兴趣的读者下来还可以去自己查看更多的成员函数

那么第一个展示的环节就已经结束,而第二个环节那么就是我们来尝试模拟实现一个string类,来帮组我们加深对于string的理解与使用

string的模拟实现

前置知识准备

那么在具体实现string的各个函数模块之前,那么我们得有一个前置知识的储备

1.短字符串优化

那么我们知道string在底层是采取的动态数组的方式来实现字符串的存储,但是现代的编译器对string做了一定的优化,该优化就是sso短字符串优化

我们知道对于动态数组会调用new来申请空间,那么其中new申请空间就会涉及到查找相应的空闲的连续的内存块,然后将其分配给相应的对象,并且一旦string对象被销毁,还涉及到调用析构函数来清理释放在堆上申请的空间,也就是说调用new是有一定的成本的,而string的设计者肯定是希望减少new的调用的,所以这里创建一个string对象的时候,那么可以调用new来预先分配一定大小的空间,从而避免后序的扩容,但是这个方式面临的问题就是不知道string对象具体保存的字符串有多长,那么如果预先开辟的数组大小不合适,那么就会造成空间的浪费

所以这里采取了更为合理的设计就是如果你的字符串是小于某个标准,比如字符串长度小于22或者15,那么其实没必要创建一个动态数组,因为你开辟了大小为22或者15甚至大小为2的动态数组,那么后序如果该string对象进行比如字符串拼接或者插入的行为,那么一定会涉及到扩容,所以我们更希望是那种较长的字符串使用堆数组,这样能够分配较大的空间,可以减少扩容,而对于较小的字符串,那么采取的就是用栈数组来存储即可,那么这就是string的短字符串的优化

那么在VS平台下,采取的是如果你的字符串的长度是小于15的话,那么就会采取短字符串优化,采取栈数组来存储,其他平台可能采取的是字符串长度可能是22

我们可以写一段简答的代码来验证在各个平台下具体设置的短字符串的长度:

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;
int main()
{
    string s1 ;
    cout << s1.capacity() << endl;
    return 0;
}

VS平台:

那么这里我们如果要模拟实现短字符串优化的话,那么我们就得利用匿名联合体,那么可能有些小伙伴可能对于联合体就很陌生,那么这里我来简单回顾一下联合体,那么对于联合体很熟悉的小伙伴可以跳过,那么匿名联合体是string类的实现的一个关键

那么我们知道对于结构体来说,那么它内部的成员变量是按照其在内部声明的顺序,根据内存对齐的规则,按照一定的偏移量在对象中存储的,而对于联合体来说,那么联合体中的所有的成员变量的偏移量都在联合体对象的起始地址,也就意味着,此时联合体中的所有的成员变量是共享联合体的内存空间,所以联合体对象的内存布局和结构体是完全不同的,那么既然联合体中所有的成员变量的偏移量都是从联合体的起始地址开始,那么意味着一个问题,那么就是联合体只能有一个成员变量的数据是有效的,假设存在这样的场景:

c 复制代码
union wz

{

  int tp1;

double tp2;

};
union wz s1;

那么这里对于联合体wz来说,那么它的成员变量tp1以及tp2的起始地址都是在联合体对象的起始地址,那么意味着如果你此时对tp1的成员变量赋值,那么它必然会覆盖tp2的值,同理你对tp2赋值,那么同样会覆盖tp1,所以这就是为什么联合体中的只能有一个成员变量有效,那么我写了一份简单的代码来验证联合体的特性:

cpp 复制代码
#include<iostream>
 using namespace std;
 union wz
 
 {
 
     int tp1;
 
     double tp2;
 
 };
 int main()
 {
     union wz s1;
     s1.tp1 = 4;
     cout <<"before tp1:" <<s1.tp1 << endl;
     s1.tp2 = 10.95;
     cout << "after tp1:" << s1.tp1 << endl;
     cout << "tp2: " << s1.tp2 << endl;
   return 0;
}

那么会出现这样的运行结果,那么大家可以从联合体的内存布局,就能够清晰的认识到:

那么其次就是对于联合体对象的所占据的空间大小,那么它的大小一般是等于最大的成员变量的大小,但前提是其满足是默认对齐数的整数倍,那么VS的默认对齐数是4,那么在上面的例子中该联合体的大小就是8,而对于下面这个联合体来说:

cpp 复制代码
union wz
 {
     char tp1[5];
     int tp2;
 };

那么最大的成员变量的大小是5,那么默认对齐数是4,那么这里整体的大小不满足是4的整数倍,所以最终该联合体的大小就是8:


这里我们采取的就是匿名联合体来实现,那么匿名联合体和命名联合体不同的是,匿名联合体中定义的成员变量是直接展开到其所处的作用域,那么直接通过成员变量名来访问,而我们这里我们就可以定义两个成员变量,一个是栈数组buffer,大小为15,还有一个则是匿名结构体,里面封装了一个指向动态数组的指针ptr,以及记录有效字符串长度的大小size和一个数组的容量capacity

其次由于联合体共享内存的原因,那么这里就得定义一个成员变量来标记,来记录当前string对象采取的是栈数组还是堆数组,那么这里我在类中定义了一个bool类型的成员变量

cpp 复制代码
#define sso_size 15
namespace wz{
class string
	{
	private:
		union
		{
			struct
			{
				char* ptr;
				size_t _size;
				size_t _capacity;
			};
			char buffer[sso_size];
		};
		bool is_short;
    .......
}
}

2.memcpy函数

那么string类中很多的成员函数都会涉及到拷贝,那么这里选择拷贝的函数就有讲究,这里我选择的都是memcpy函数而不是strcpy函数,因为strcpy函数拷贝到\0结束,但是我们有的string对象保存的可能是这样的字符串:"hello\0wz"

那么如果你采取strcpy,那么最终只能拷贝"hello",所以这就是为什么string内部要设置一个size来记录有效字符串的长度,那么memcpy函数则是接收一个拷贝的目标地址和拷贝的源地址以及一个拷贝的字节数,那么它会按照该字节数将源地址的内容以字节为单位拷贝到目标地址处,那么memcpy函数的返回值就是目标地址

cpp 复制代码
void* memcpy(void* des,void* src,size_t count);

那么有了这两个前置知识之后,我们就可以动手来实现我们的string类了,那么为了避免与库中的string类冲入,那么这里我都是将其定义在了wz命名空间中

string类实现

1.构造函数

那么这里构造函数有三个,第一个是无参的构造函数,那么上文,我们知道了string会采取短字符串优化,所以无参的构造函数采取的就是栈数组,那么我们要做的就是将标记变量is_short设置为true,并且将栈数组的首元素给设置为\0代表空串

cpp 复制代码
string()
			:is_short(true)
		{
			buffer[0] = '\0';
		}

而这里还可以用c风格的字符串来初始化,那么这里得注意判断字符串的长度,如果字符串的长度小于15,那么我们采取的就是栈数组来存储,如果字符串的长度大于15,那么采取的就是对堆数组,然后设置size以及capacity的值

cpp 复制代码
string(const char* str)
		{
			size_t len = strlen(str);
			if (len >= sso_size)
			{
				ptr = new char[len + 1];
				memcpy(ptr, str, len);
				_size = len;
				ptr[_size] = '\0';
				_capacity = len + 1;
				is_short = false;
			}
			else
			{
				memcpy(buffer, str, len);
				buffer[len] = '\0';

			}
		}

第三个则是拷贝构造函数,那么这里由于存在短字符串优化,那么接受的string对象的值可能是栈数组,也可能是堆数组,那么这里我们就得进行判断,然后进行相应的拷贝逻辑

cpp 复制代码
string(const string& str)
		{
			if (str.is_short)
			{
				memcpy(buffer, str.buffer, sso_size);
				is_short = true;
			}
			else
			{
				ptr = new char[str._capacity];
				memcpy(ptr, str.ptr, str._size);
				_size = str._size;
				_capacity = str._capacity;
				is_short = false;
			}
		}

2.reserve函数

那么reserve函数是来申请空间,那么首先我们就得先判断当前的string对象的状态,也就是采取的是栈数组还是堆数组来存储,那么如果是栈数组,我们在来比较新的申请的空间与栈数组的大小,如果小于则延续当前的栈数组,如果大于15,那么意味着栈数组到堆数组的切换,就要将栈数组的内容拷贝给堆数组,这里实现的时候,注意由于栈数组和堆数组是联合体内的成员变量,那么这里我们不能直接将栈数组buffer拷贝给堆数组,因为内存共享的原因,这里就得准备一个临时变量,将栈数组的内容拷贝给临时变量,然后在通过临时变量拷贝给堆数组,而如果当前string对象已经是堆数组,那么我们就只需比较新申请的空间与capacity的大小,如果小于就不管,大于就扩容然后再拷贝数据

cpp 复制代码
void reserve(size_t n)
		{
			if (is_short)
			{
				if (n > sso_size)
				{
					size_t len = strlen(buffer);
					char temp[sso_size];
					memcpy(temp, buffer, sso_size);
					ptr = new char[n + 1];
					memcpy(ptr, temp, sso_size);
					_size = len;
					_capacity = n + 1;
					is_short = false;
				}
			}
			else
			{
				if (n + 1 > _capacity)
				{
					_capacity = std::max(_capacity * 2, n + 1);
					char* temp = new char[_capacity];
					memcpy(temp, ptr, _size);
					delete[] ptr;
					ptr = temp;
					temp = nullptr;
				}
			}
		}

3.insert函数

那么对于insert函数,那么我们先来认识插入一个c风格的字符串的实现原理,那么首先我们得判断插入的位置的合法性,如果插入的位置合法,那么接下来判断当前string对象的状态,如果当前string对象是栈数组,那么就得判断当前栈数组的容量能否容纳新插入后的字符串的长度,不能就会涉及到栈数组到堆数组的切换,可以的话,那么就在栈数组的基础上插入字符串,涉及到元素的移动与字符串的拷贝,而如果当前是堆数组,那么我们就比较新插入后的字符串的长度是否会超过capacity,会的话则涉及到扩容,那么接下来的内容就是元素的移动与字符串的拷贝,然后更新size值

cpp 复制代码
void insert(size_t pos, const char* str)
		{
			if (is_short)
			{
				int len = strlen(buffer);
				assert(pos <= len);
			}
			else {
				assert(pos <= _size);
			}
			int len = strlen(str);
			if (is_short)
			{
				if (pos + len >= sso_size)
				{
					reserve(_size + len + 1);
				}
			}
			else
			{
				if (_size + len >= _capacity)
				{
					reserve(_size + len + 1);
				}
			}
			if (is_short)
			{
				size_t end = strlen(buffer);
				while (end >= pos && end != npos)
				{
					buffer[end + len] = buffer[end];
					end--;
				}
				for (int i = 0;i < len;i++)
				{
					buffer[pos++] = str[i];
				}
			}
			size_t end = _size;
			while (end >= pos && end != npos)
			{
				ptr[end + len] = ptr[end];
				end--;
			}
			for (int i = 0;i < len;i++)
			{
				ptr[pos++] = str[i];
			}
			_size += len;
		}

那么这里要注意的就是当我们插入的位置是首元素的时候,也就是pos是0,那么我们元素移动的while循环的条件是end>=pos,但是当end减到-1的时候,由于end的数据类型是size_t,那么此时的-1会被转换成整数最大值,那么就会导致数组越界从而程序崩溃,而string类还有一个静态的成员变量npos,其值就是-1,所以我们这里可以在加一个条件来专门对这种情况进行特判

而对于插入一个string对象,那么逻辑就几乎一样,这里我不在赘述

cpp 复制代码
void insert(size_t pos, const string& str)
		{

			if (is_short)
			{
				int len = strlen(buffer);
				assert(pos <= len);
			}
			else {
				assert(pos <= _size);
			}
			int len = str.size();
			if (is_short)
			{
				if (pos + len >= sso_size)
				{
					reserve(_size + len);
				}
			}
			else
			{
				if (_size + len >= _capacity)
				{
					reserve(_size + len);
				}
			}
			if (is_short)
			{
				size_t end = strlen(buffer);
				while (end >= pos && end != npos)
				{
					buffer[end + len] = buffer[end];
					end--;
				}
				for (int i = 0;i < str.size();i++)
				{
					buffer[pos++] = str[i];
				}
			}
			size_t end = _size;
			while (end >= pos && end != npos)
			{
				ptr[end + len] = ptr[end];
				end--;
			}
			for (int i = 0;i < str.size();i++)
			{
				ptr[pos++] = str[i];
			}
			_size += len;
		}
		void erase(size_t pos, size_t len = npos)
		{

			if (is_short)
			{
				int _len = strlen(buffer);
				assert(pos <= _len);
			}
			else {
				assert(pos <= _size);
			}
			int remain = _size - pos;
			if (len >= remain || len == npos)
			{
				if (is_short)
				{
					buffer[pos] = '\0';
				}
				else
				{
					_size = pos;
					ptr[_size + 1] = '\0';
				}
				return;
			}
			if (is_short)
			{
				size_t end = pos + len;
				int _len = strlen(buffer);
				while (end <= _len)
				{
					buffer[end] = buffer[pos++];
					end++;
				}
				//0 1 2 3 \0  pos:1 
			}
			size_t end = pos + len;
			while (end <= _size)
			{
				ptr[end] = ptr[pos++];
				end++;
			}
			_size -= len;
		}

那么这里我就具体讲解了我认为实现最容易出错的三个函数,那么其他成员函数的实现就很简单了,只不过要添加一个判断栈数组以及处理栈数组的逻辑

源码

mystring.h:

cpp 复制代码
#pragma once
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<assert.h>
#include<algorithm>
#define sso_size 15
namespace wz
{
	class string
	{
	private:
		union
		{
			struct
			{
				char* ptr;
				size_t _size;
				size_t _capacity;
			};
			char buffer[sso_size];
		};
		bool is_short;
		static size_t npos;
	public:
		typedef char* Iterator;
		typedef const char* const_Iterator;
		string()
			:is_short(true)
		{
			buffer[0] = '\0';
		}
		string(const char* str)
		{
			size_t len = strlen(str);
			if (len >= sso_size)
			{
				ptr = new char[len + 1];
				memcpy(ptr, str, len);
				_size = len;
				ptr[_size] = '\0';
				_capacity = len + 1;
				is_short = false;
			}
			else
			{
				memcpy(buffer, str, len);
				buffer[len] = '\0';

			}
		}
		string(size_t n, char ch)
		{
			if (n >= sso_size)
			{
				ptr = new char[n + 1];
				for (int i = 0;i < n;i++)
				{
					ptr[i] = ch;
				}
				ptr[n] = '\0';
				_size = n;
				_capacity = n + 1;
				is_short = false;
			}
			else
			{
				for (int i = 0;i < n;i++)
				{
					buffer[i] = ch;
				}
				buffer[n] = '\0';
			}
		}
		string(const string& str)
		{
			if (str.is_short)
			{
				memcpy(buffer, str.buffer, sso_size);
				is_short = true;
			}
			else
			{
				ptr = new char[str._capacity];
				memcpy(ptr, str.ptr, str._size);
				_size = str._size;
				_capacity = str._capacity;
				is_short = false;
			}
		}
		~string()
		{
			if(!is_short)
			{
				delete[] ptr;
				ptr=nullptr;
			}
		}
		size_t size() const
		{
			if (is_short)
			{
				return strlen(buffer);
			}
			return _size;
		}
		size_t capacity() const
		{
			if (is_short)
			{
				return sso_size;
			}
			return _capacity;
		}
		char& operator[](size_t num)
		{
			if (is_short)
			{
				size_t len = strlen(buffer);
				assert(num < len);
				return buffer[num];
			}
			assert(num < _size);
			return ptr[num];
		}
		const char& operator[](size_t num) const
		{
			if (is_short)
			{
				size_t len = strlen(buffer);
				assert(num < len);
				return buffer[num];
			}
			assert(num < _size);
			return ptr[num];

		}
		void reserve(size_t n)
		{
			if (is_short)
			{
				if (n > sso_size)
				{
					size_t len = strlen(buffer);
					char temp[sso_size];
					memcpy(temp, buffer, sso_size);
					ptr = new char[n + 1];
					memcpy(ptr, temp, sso_size);
					_size = len;
					_capacity = n + 1;
					is_short = false;
				}
			}
			else
			{
				if (n + 1 > _capacity)
				{
					_capacity = std::max(_capacity * 2, n + 1);
					char* temp = new char[_capacity];
					memcpy(temp, ptr, _size);
					delete[] ptr;
					ptr = temp;
					temp = nullptr;
				}
			}
		}
		void clear()
		{
			if(iss_short)
			{
				buffer[0]='\0';
			}else{
				 _size=0;
				 ptr[_size]='\0';
			}
		}
		void append(const wz::string& str)
		{
			if(is_short)
			{
				size_t len=strlen(buffer);
				size_t remain=sso_size-_len;
                 if(remain<=str._size)
				 {
					 reserve(len+str._size+1);
					 for(int i=0;i<str._size;i++)
					 {
						ptr[len++]=str[i];
					 }
					 ptr[len]='\0';
				 }else{
					  for(int i=0;i<str._size;i++)
					  {
						 buffer[len++]=str[i];
					  }
					  buffer[len]='\0';
				 }
			}else{
				  size_t remain=_capacity-_size;
				  if(remain<=str._size)
				  {
					  reserve(_size+str.size+1);
				  }
				  for(int i=0;i<str._size;i++)
				  {
					  ptr[_size++]=str[i];
				  }
				  ptr[_size]='\0';
			}
		}
		void append(const char* str)
		{
			size_t len = strlen(str);
			if(is_short)
			{
				int _len=strlen(buffer);
				size_t remain=sso_size-_len;
				if(remain<=len)
				{
					reserve(len+_len+1);
					for(int i=0;i<len;i++)
					{
						ptr[_len++]=str[i];
					}
					ptr[_len]='\0';
					_size=_len;
				}else
				{
					for(int i=0;i<len;i++)
					{
						buffer[_len++]=str[i];
					}
				}
			}else
			{
				if(_size+len>=_capacity)
				{
					reserve(_size+len+1);
				}
				for(int i=0;i<len;i++)
				{
					ptr[_size++]=str[i];
				}
				ptr[_size]='\0';
			}
		}
		string& operator+=(const string& str)
		{
			append(str);
			return *this;
		}
		string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}
		string operator+(const string& str)
		{
			string temp;
			temp += str;
			return temp;
		}
		void push_back(char ch)
		{
			if (is_short)
			{
				int len = strlen(buffer);
				if (len == sso_size)
				{
					reserve(len + 1);
					ptr[len] = ch;
					ptr[len + 1] = '\0';
					_size = len + 1;
				}
				else
				{
					buffer[len] = ch;
					buffer[len + 1] = '\0';
				}
			}
			else
			{
				if (_size == _capacity)
				{
					reserve(_size + 1);
				}
				ptr[_size] = ch;
				ptr[_size + 1] = '0';
				_size++;
			}
		}
		void pop_back()
		{

			if (is_short)
			{
				int len = strlen(buffer);
				if (len == 0)
				{
					return;
				}
			}
			else {
				if (_size == 0)
				{
					return;
				}
			}
			if (is_short)
			{
				int len = strlen(buffer);
				if (len != 0)
				{
					buffer[len - 1] = '\0';
				}
			}
			_size--;
			ptr[_size] = '\0';
		}
		string operator+(const char* str)
		{
			string temp;
			temp += str;
			return temp;
		}
		string& operator=(const string& str)
		{
			if (str.is_short)
			{
				if (is_short) {
					memcpy(buffer, str.buffer, sso_size);
				}
				else {
					delete[] ptr;
					memcpy(buffer, str.buffer, sso_size);
					is_short = true;
				}
			}
			else
			{
				if (is_short)
				{
					ptr = new char[str._capacity];
					memcpy(ptr, str.ptr, str._size);
					_size = str._size;
					_capacity = str._capacity;
					is_short = false;
				}
				else {
					if (_capacity < str._capacity)
					{
						reserve(str._capacity);
						memcpy(ptr, str.ptr, str._size);
						_size = str._size;
						_capacity = str._capacity;
					}
					else
					{
						memcpy(ptr, str.ptr, str._size);
						_size = str._size;
					}
				}
			}
		}
		string& operator=(const char* str)
		{
			size_t len = strlen(str);
			if (is_short)
			{
				if (len > sso_size)
				{
					reserve(len + 1);
					memcpy(ptr, str, len);
					ptr[len] = '\0';
					_size = len;
				}
				else
				{
					memcpy(buffer, str, len);
					buffer[len] = '\0';
				}
			}
			else
			{
				if (len >= _capacity)
				{
					reserve(len + 1);
				}
				memcpy(ptr, str, len);
				ptr[len] = '\0';
				_size = len;
			}
			return *this;
		}
		const char* c_str() const
		{
			if (is_short)
			{
				return buffer;
			}
			return ptr;
		}
		char* begin()
		{
			if (is_short)
			{
				return buffer;
			}
			return ptr;
		}
		const char* begin() const
		{
			if (is_short)
			{
				return buffer;
			}
			return ptr;
		}
		char* end()
		{
			if (is_short)
			{
				size_t len = strlen(buffer);
				return buffer + len;
			}
			return ptr + _size;
		}
		const char* end()const
		{
			if (is_short)
			{
				size_t len = strlen(buffer);
				return buffer + len;
			}
			return ptr + _size;
		}
		void insert(size_t pos, const char* str)
		{
			if (is_short)
			{
				int len = strlen(buffer);
				assert(pos <= len);
			}
			else {
				assert(pos <= _size);
			}
			int len = strlen(str);
			if (is_short)
			{
				if (pos + len >= sso_size)
				{
					reserve(_size + len + 1);
				}
			}
			else
			{
				if (_size + len >= _capacity)
				{
					reserve(_size + len + 1);
				}
			}
			if (is_short)
			{
				size_t end = strlen(buffer);
				while (end >= pos && end != npos)
				{
					buffer[end + len] = buffer[end];
					end--;
				}
				for (int i = 0;i < len;i++)
				{
					buffer[pos++] = str[i];
				}
			}
			size_t end = _size;
			while (end >= pos && end != npos)
			{
				ptr[end + len] = ptr[end];
				end--;
			}
			for (int i = 0;i < len;i++)
			{
				ptr[pos++] = str[i];
			}
			_size += len;
		}
		void insert(size_t pos, const string& str)
		{

			if (is_short)
			{
				int len = strlen(buffer);
				assert(pos <= len);
			}
			else {
				assert(pos <= _size);
			}
			int len = str.size();
			if (is_short)
			{
				if (pos + len >= sso_size)
				{
					reserve(_size + len);
				}
			}
			else
			{
				if (_size + len >= _capacity)
				{
					reserve(_size + len);
				}
			}
			if (is_short)
			{
				size_t end = strlen(buffer);
				while (end >= pos && end != npos)
				{
					buffer[end + len] = buffer[end];
					end--;
				}
				for (int i = 0;i < str.size();i++)
				{
					buffer[pos++] = str[i];
				}
			}
			size_t end = _size;
			while (end >= pos && end != npos)
			{
				ptr[end + len] = ptr[end];
				end--;
			}
			for (int i = 0;i < str.size();i++)
			{
				ptr[pos++] = str[i];
			}
			_size += len;
		}
		void erase(size_t pos, size_t len = npos)
		{

			if (is_short)
			{
				int _len = strlen(buffer);
				assert(pos <= _len);
			}
			else {
				assert(pos <= _size);
			}
			int remain = _size - pos;
			if (len >= remain || len == npos)
			{
				if (is_short)
				{
					buffer[pos] = '\0';
				}
				else
				{
					_size = pos;
					ptr[_size + 1] = '\0';
				}
				return;
			}
			if (is_short)
			{
				size_t end = pos + len;
				int _len = strlen(buffer);
				while (end <= _len)
				{
					buffer[end] = buffer[pos++];
					end++;
				}
				//0 1 2 3 \0  pos:1 
			}
			size_t end = pos + len;
			while (end <= _size)
			{
				ptr[end] = ptr[pos++];
				end++;
			}
			_size -= len;
		}
		void resize(size_t n)
		{
			if (is_short)
			{
				int len = strlen(buffer);
				if (n < len)
				{
					buffer[n] = '\0';
				}
				if (n > sso_size)
				{
					reserve(n);
				}
			}
			else
			{
				if (n < _size)
				{
					ptr[n] = '\0';
					_size = n;
				}
				else if (n > _size && n <= _capacity)
				{
					memset(ptr + _size, '\0', n - _size);
					_size = n;
				}
				else
				{
					reserve(n);
					memset(ptr + _size, '\0', n - _size);
					_size = n;
				}
			}
		}
		string substr(size_t pos, size_t len = npos)
		{
			if (is_short)
			{
				size_t _len = strlen(buffer);
				assert(pos < _len);
			}
			else
			{
				assert(pos < _size);
			}
			if (len == 0)
			{
				return string();
			}
			string temp;
			if (is_short)
			{
				size_t _len = strlen(buffer);
				size_t remain = _len - pos;
				if (len > remain || len == npos)
				{
					for (size_t i = pos;i <= _len;i++)
					{
						temp.push_back(buffer[i]);
					}
				}
				else
				{
					for (size_t i = pos;i < pos + len;i++)
					{
						temp.push_back(buffer[i]);
					}
				}
				return temp;
			}
			else
			{
				size_t remain = _size - pos;
				if (len > remain || len == pos)
				{
					for (int i = pos;i <= _size;i++)
					{
						temp.push_back(ptr[i]);
					}
				}
				else
				{
					for (size_t i = pos;i <= pos + len;i++)
					{
						temp.push_back(ptr[i]);
					}
				}
				return temp;
			}
		}
	};
	size_t npos = -1;
}
std::ostream& operator<<(std::ostream& out, wz::string& s1)
{
	out << s1.c_str();
	return out;
}

main.cpp:

cpp 复制代码
#include"mystring.h"
using namespace std;
int main()
{
	
	wz::string s1;
	cout << "start size is " << s1.capacity() << endl;
	s1 = "WangZhuo";
	s1 += " is beautiful";
	cout << s1.capacity() << endl;
	wz::string::Iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << (*it);
	    it++;
	}
	cout << endl;
	s1 = "WangZhe";
	s1.append("is hadsome");
	cout << s1 << endl;
	s1.clear();
	cout << s1 << endl;
	cout << "s1 size is:" << s1.size() << endl;
	s1.push_back('w');
	s1.push_back('a');
	s1.push_back('n');
    s1.pop_back();
	cout << s1 << endl;
	cout << s1.capacity() << endl;
	s1.reserve(200);
	cout << s1.size() << endl;
	return 0;
}

运行截图:

彩蛋

那么我这里我来验证范围for的底层实现,我当时说过编译器之所以能够支持范围for,底层采取的就是迭代器,并且使用了begin以及end函数来遍历,那么这里我自定义了string类,那么我将我自定义的string类的end函数的函数名给修改为End,那么看看此时编译器还能不能支持我自定义的string的范围for

cpp 复制代码
#include"mystring.h"
using namespace std;
int main()
{
	wz::string s1 = "WangZhuo";
	for (auto ch : s1)
	{
		cout << ch;
	}
	return 0;
	}

那么你会发现编译器底层对于范围for的实现是很死板的,它会去专门找end以及begin函数

结语

那么这就是本文关于string的全部内容了,那么从多个维度带你全面认识string,那么也希望读者下来可以自己去实现一个string类,那么对你的帮组很大,那么这就本文全部的内容,那么我的下一期博客将会介绍vector,那么我会持续更新,希望你能够多多关注,如果本文有帮组到你的话,还请多多三连加关注哦,你的支持就是我创作的最大动力!

相关推荐
程序猿chen几秒前
JVM考古现场(二十四):逆熵者·时间晶体的永恒之战
java·jvm·git·后端·程序人生·java-ee·改行学it
AronTing2 分钟前
单例模式:确保唯一实例的设计模式
java·javascript·后端
level_xiwei8 分钟前
Linux之信号
linux·运维·服务器
AronTing10 分钟前
模板方法模式:定义算法骨架的设计模式
java·后端·面试
AronTing14 分钟前
迭代器模式:统一数据遍历方式的设计模式
java·后端·面试
AronTing14 分钟前
策略模式:动态切换算法的设计智慧
java·后端·面试
爱的叹息15 分钟前
查看Spring Boot项目所有配置信息的几种方法,包括 Actuator端点、日志输出、代码级获取 等方式,附带详细步骤和示例
java·spring boot·后端
蜗牛031420 分钟前
Redis在SpringBoot中的使用
java·spring boot·redis
工业互联网专业27 分钟前
基于springboot+vue的医院管理系统
java·vue.js·spring boot·毕业设计·源码·课程设计·医院管理系统
硬匠的博客32 分钟前
C++IO流
c++