string类

发布于:2025-05-31 ⋅ 阅读:(23) ⋅ 点赞:(0)

1. 为什么学习string类?   

    string叫串,它是一个管理字符串的类,现实中为什么要出一个管理字符串的类呢?现实中我们有很多类型,比如int、double、char等,但发现这个世界的一些复杂东西都是通过字符串表示的。像int、double、char等可以便是简单的数字和字符,但一个人的信息、名字、住址等都是用字符串表示,所以日常中字符串是非常多的,最好可以整理它们以便更好的用。C语言中有考虑管理字符串,出了str系列的库函数管理字符串,但是str系列其实不够好,它只是对字符串进行拷贝、测量长度等,但效率不够好。而且增删查改不能管,用起来也不够方便。因此C++出来string,从它以类的角度可体现它是面向对象的,它不仅仅可以对字符串有各种算法(拷贝、比较等),还管理增删查改。这就是为什么要学习string类,可以把它理解为是数据结构,是一个管理字符的数组。

2. 标准库中的string类

    下面就要开始看C++的文档了,如下图有两个文档:

左边的是山寨版,右边的是正版,这里推荐看山寨版,因为好读一些。

ctrl+f可以搜索string,也发现C++头文件没有带.h,某种程度上说明了是为了和C语言互相区分。下面来简单看看string的内容:

学stl这块首先包含头文件,stl属于标准库的一部分,标准库的东西都在std这个命名空间里面,如果展开了可以string s1这样用,没有展开就std::string s2这样用。先浅浅看一下什么样子:

上面左图string是类名,后面是对象名,在调用构造函数,现在就是啥也不初始化。还可以进行初始化,如上面右图,name对象就存储的有张三这个字符串,有一天像把张三这个名字换了,就可以直接通过赋值来改,这就是它的基本玩法。string是一个管理字符串的类,那我怎么知道这里可以用构造,可以赋值等,下面尝试看看string这一块的文档:

String class是string这个类的说明,如说明中第一句话说string是一个管理字符数组的顺序表。

Member functions部分是默认成员函数,第一个是它的构造,发现构造实现了7个;第二个是它的析构,只有1个;第三个是赋值,看到这里实现了3个。

Iterators部分是迭代器,Capacity部分是容器相关的,Element access是访问数据相关的,Modifiers部分是修改数据相关的,String operations是其他相关操作如查找比较等,Non部分是非成员函数部分,大致就是上述样子。下面先试着用一用,感受一下string舒服的地方,首先要用string就得学一下string的构造:

 这里不用都掌握,掌握最常用的就像,不常用的有个印象,到时候需要的时候查文档就行。如上图第1个就是无参的构造,string s1,比如我现在定义个字符串但我不知道要怎么初始化,那就可以用无参的构造,其实也就是默认构造。第4个就是我现在想给一些值先初始化,string s2("张三"),这时就可以给一个常量字符串进行初始化。(文档下面也有对每个函数的说明,如第一个说的是空string构造,第四个说的是可用C的字符串初始化,C的字符串的特点是最后有/0)第6个是还可以用n个字符去初始化,string s3(10, '*'),比如我有字符串s4,要求初始化为10个*。第2个是拷贝构造,string s5(s2),比如想用s2初始化s5。

以上就是最常用的,文档中有功能介绍,参数介绍,还有样例。还有几个没介绍的,这里浅浅的说一下它的功能(stl这里可以看名字去蒙功能),如2是完全拿string去拷贝,3是拿string的一部分去拷贝,是从某个位置开始拷len这么长,这里可以试一试:

比如现在有string s3("hello world"),现在想拷贝s3中的world去构造s6,string s6(s3,6,5),第七个位置的下标是6,要拷贝5个字符。string也是可以直接流插入和流提取的,因为Non里重载了,所以打印出来看到拷贝成功。也可以把前几个都打印出来看看:

以前C语言用strcmp比较,现在比较就可以:

这样可读性更强了。继续看刚才的,3这里有个len=npos,这是个缺省参数,那npos是什么意思呢?

可从文档中看到npos是string这个类中的静态成员变量,它给的值是-1,这个-1是整型的最大值。-1是int,赋值过去进行整型提升,提升为了size_t,-1是全1给无符号就是整型最大值。意味着如果直接string s7(s3, 6),这样没有给第三个参数,第三个参数用缺省参数表示一个很大的值,从pos开始向后取42亿多,这个长度远超过后面的,那怎么办?文档中有说明,如果len太长超过字符串的长度或者len取了npos,此时就直接取到结尾:

这里说一个这块比较有用的玩法:

比如有上图这样一个字符串,想把这个字符串进行一些分割,分割为3部分(以后可以结合find,这里为了演示就数着拆):

如果不给npos最后就还要计算很麻烦,这样看这里的缺省参数给的非常好。再看看其它几个构造函数,第4表示用一个常量字符串初始化,那第5表示拷贝数组的前n个初始化:

第7个暂时不用管,构造就介绍到这里。析构不用管,对象出了作用域会自动调用析构函数。

赋值这里最后两个很少用,但可以支持这样玩:

可以用string对象,常量字符串,一个字符来赋值。下面再看看string一些比骄爽的地方:

先看一下增,比如现在s1中有hello,假设我要再这个字符串中插入一个字符怎么插入?

可从文档中看到有push_back,它的功能是追加一个字符到字符串结尾。那再想插入一个world呢?

此时还有个函数叫append,append支持了很多,这里最常用第3个,它可以追加一个字符串。现在来实验一下上述:

发现还是比较好用的。C语言中有strcat也可以尾插,但它每次先找到/0才能添加效率比较低,而且strcat不能自动扩容。string最大的好处是只管追加,空间不够交给对象,这里先提一下,对象里面其实大概是这个样子:

其实就是一个管理字符的顺序表,空间不够会扩容。其实一般还是不喜欢用push_back和append,因为文档里还有个+=:

它可以加等一个str对象,字符串,字符,下面看看效果:

发现可以,其实它的本质还是尾插,只是再string类里面重载了operator+=,+=一个字符是去调用push_back,+=一个字符串是去调append的第三个函数,是一个复用,只不过这里从用的角度来说体验非常好。这里也不能完全替代了append,因为append实现了很多重载,如的第一个是追加一个对象,第二个是追加一个对象中从某个下标开始的len个字符,第三个是追加一个字符串,第一个是追加字符串的前n个,第五个是追加n和字符。了解了上述来看一个小问题,有一个size_t类型的x,输入一个值,要求把输入的值逆置并转成一个string对象怎么转?

就定义一个s1对象,每次取x的最后一位就模10,再取下一位就除10再模10,直到x是0就停止。用刚才的知识把每次取的值就直接加等到s1中,这里不能直接加val,因为s1中是字符,所以加字符零变成一个字符。如果C语言实现这个问题还要提前自己开一个大空间,现在就不需要了。继续看string的使用,现在有这样的场景:

我想要遍历string对象怎么遍历?这里的遍历有很多方式,如果仅仅是要简单的打印它就直接用流插入就可以了,cout << s1 << endl。下一种方式是string重载了一个非常重要的运算符,这个运算符叫operator[],[]这个运算符本质是一种解引用,是数组用来访问它的数据的,有了这个[],想访问它的每个字符就可以就可以结合循环用下标+[],但是要知道多长,就可以用一下size:

上图反应了两种方式效果是一样的,不一样的地方是第一种方式是直接打印,第二种方式是一个字符一个字符打印,第二种在控制上更自由。我们说过string底层会开数组去存储,去堆上new一个数组,为了和C语言更好的配合存完数据后,最后会添加一个'\0',那s1[i]访问时最后包含'\0‘吗?可以结合size来看看:

说明size不算最后的/0,因为/0是一个标识字符串结束的特殊字符。如果上面改为i <= s1.size()就可以访问到/0,只是不显示。如果想修改其中的字符可以吗?

可以,因为[]返回的是pos位置字符的引用,并且实现了两个版本。如果是普通对象要用普通方括号,返回普通的引用;如果是const对象,调用const版本返回const引用,且const对象是不能被修改的。下面修改一下:

所以可以像数组玩法一样修改每个字符,把修改后的字符都遍历一遍。所以重载了operator[]有一个非常便捷的玩法是可以像数组一样去使用,所以[]用来做增删查改是很方便的,当然还可以读,如把第0个位置字符改回去就:s1[0]--。虽然s1是自定义类型,但是用起来和内置类型感觉是一样的,还是要明白下图两个的本质区别:

看起来一样,其实底层天差地别。s2[0]中s2是数组名,代表首元素的地址,底层等价于*(s2+0)。s1[0]的底层是自定义用这个运算符时变为s1.operator[](1),这个s1[0]表达式读的时候读的是函数调用的返回值。可以看看底层:

这里s2只读编译器优化比较严重,可以++一下再看底层,可以看到s2是一些解引用计算,s1是一些函数调用。

    下一部分看一个没有接触过的东西,叫迭代器。迭代器是怎么访问的呢?迭代器再现阶段可以把它理解为一个像指针一样的类型,它可能是指针,有可能不是指针。比如现在有string s1("hello world"),string对象里有_str,_size,_capacity。_str肯定不是直接指向常量字符串,因为常量字符串是无法修改的,它是在堆上开了一块空间,_str指向堆上开的这块空间,空间不够就扩容,空间中拷贝了常量字符串的值,最后加个\0。那什么是迭代器呢?迭代器就是增加了一种访问方式,这里先看看迭代器的代码是什么样子的(任何容器的迭代器都是这样的,暂时记一记,后面会说):

可以理解为s1.begin()指向开头,会返回一个像指针一样的东西给it,s1.end()返回的是最后一个数据的下一个位置(如样例中是\0),*it就是指向位置的数据,++it就是往下一个位置走,当等于end就结束了。

迭代器属于这个类域,所以加类域作用限定符。迭代器是一个像指针一样的东西,begin()对任何一个容器都是第一个位置的迭代器,它返回给it,it不等于end就解引用++。那用迭代器能不能修改呢?可以,因为按指针的行为来说解引用是能修改的,由于它像指针一样,所以(*it)--和++it就类似于指针中--指向的数据和++指针本身:

这时有人可能会说这迭代器有什么意义呢?比如访问和修给数据既有下标加方括号,又有迭代器,肯定都愿意用下标加方括号,因为C语言阶段习惯了这样的方式,用起来看着很顺,其实string平时也不太用迭代器。那迭代器有什么意义呢?它有两大意义,第一个意义先看下图:

这还提供了第三种访问string的方式叫范围for,语法上依次获取s1的数据赋值给ch,这里是读。如果要写就:

这里注意直接++是不行的,因为相当于依次取值拷贝给ch,ch改变不影响s1里面,所以加个引用就可以了。此时就会有疑问说不是说的是迭代器的优点,怎么突然说到范围for了,这和迭代器有什么关系吗?其实范围for从底层角度来说底层是替换为迭代器的,也就是说表面我们看到的是范围for这样的代码,实际底层不是这个东西,底层就是迭代器。也就是说根本没有什么范围for,编译器在编译阶段会把范围for替换掉:

就相当于把*it给了ch,ch是*it的别名。从范围for汇编可以看到begin和end,也是在调这些函数。所以根本没有范围for,实际底层都是迭代器,有了迭代器才能支持范围for,如果一个类不支持迭代器也就不支持范围for,以上是第一个层面和迭代器有关系的。第二个层面是迭代器是通用的,任何容器都支持迭代器,并且用法类似,这里超前演示一下:

发现玩法都是类似的(下标+[]有些容器不一定可以用,只有连续的空间才能用),因此总结一下就是iterator提供了一种统一的方式访问和修改容器的数据。再来感受一个事,迭代器还有个价值就是和算法进行配合,这里浅浅的看两个算法(不是为了学算法,为了演示迭代器):算法要作用到数据上去,那怎么作用上去呢,数据在容器里面,那它可以直接访问容器吗?不能,因为容器里的数据是私有的,而且容器的样子也天差地别。这里看看reverse:

有逆置的意思,reverse是通用的,链表和树等都可以完成逆置。比如要逆置链表就传链表的迭代器区间,区间有规定是左闭右开。所以逆置直接上手,有了迭代器也就支持范围for:

再比如sort一下进行排序:

要求还是传迭代器(链表不能sort,以后说),所以就:

上述体现了迭代器可以很好的和算法配合,算法就可以通过迭代器,去处理容器中的数据。

    迭代器这还有些需要简单介绍的东西,它除了提供了普通迭代器,还提供了反向迭代器。就是有时候我们需要倒着遍历,此时就可以用反向迭代器(目前不管实现,先管怎么用):

此时就倒着遍历了,可以认为rbegin()在d,也就是最后的位置;rend()在第一个数据的前一个位置。也发现有了迭代器后auto的价值就体现出来了,string::reverse_iterator rit这样些有点长,就可以直接auto rit。这里也是可以修改的,比如给每个位置都加等3:

另一方面发现范围for也有它的局限性,范围for只能正着遍历不能倒着遍历,只有反向迭代器可以倒着遍历。迭代器还有一些场景:

有时候可能会像上面左图这样传参,如把s1传给了s。其实不愿意这样传,因为会调拷贝构造,所以最好加引用,不改变加const。现在用右边这样实现:

发现编译不过,因为const对象不能用普通迭代器,要用一个叫const迭代器。上图相当于把权限放大了,因为我是const对象就调用const begin,我是普通对象就调用普通begin。普通迭代器可读可写,const迭代器只读不能写,所以此时衍生出一种迭代器类型叫const_iterator:

如果要倒着遍历就:

此时也可以用auto变简洁一些。总的来说总共学了4种迭代器:

    下面看看容量相关的,第一个是size,这个前面已经见过了;还有个lenth,lenth是长度的意思,可以看到它们的解释是一样的:

为什么既要有和size又要有个length呢?

可以看到它们的值是一样的。这个和C++发展历史有关,比如我们在看文档的时候发现string不在容器里面:

其实严格的说string不属于stl,属于标准库。stl属于标准库的一部分,string属于标准库,string比stl产生的要早一些。可认为最开始有人觉得string用的多就专门写了数据结构到标准库,但光有string怎么够?后面惠普大佬就写了stl,发现string应该放到stl里面,但它们不是同一时期的。最开始string定义的是length接口,stl出现后被迫加了size(因为顺序表或链表等可以叫长度,像树就不可以),size是更通用的,以后建议用size。下一个提供的接口是max_size:

它可以返回string达到的最大长度:

这个值在不同的编译器下结果可能是不一样的,所以这个函数不太靠谱,不敢拿它当最大长度(也说明stl就是一个规范,它有很多版本,底层实现都大同小异),实际中也很少用。还有个接口叫capacity:

它可以用来看容量,可以看到vs和g++上结果是不一样的,因为底层一次扩多少不同版本实现有差别。下面再来看看clear和empty:

clear是清理数据,empy是用来判空:

可以看到clear后会让size改变,capacity不会变。下面看一个题目:

415. 字符串相加 - 力扣(LeetCode)415. 字符串相加 - 给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和并同样以字符串形式返回。你不能使用任何內建的用于处理大整数的库(比如 BigInteger), 也不能直接将输入的字符串转换为整数形式。 示例 1:输入:num1 = "11", num2 = "123"输出:"134"示例 2:输入:num1 = "456", num2 = "77"输出:"533"示例 3:输入:num1 = "0", num2 = "0"输出:"0"  提示: * 1 <= num1.length, num2.length <= 104 * num1 和num2 都只包含数字 0-9 * num1 和num2 都不包含任何前导零https://leetcode-cn.com/problems/add-strings/这个题目是字符串相加,这里所面对的问题是,我们整型可以表示的范围是有限的,如long long也是有限的,有些地方需要大数相加,这个题就是拆出了大数相加的问题。如果很长的整型数据long long都表示不下,此时有人提出把数据存在字符串里面,要进行运算就字符串和字符串相加,这个就是大数相加。有些地方有人会提供一个库叫BigInteger,题目中要求不能调库,自己实现,也就是两个string加了后返回一个string。现在比如1234+56怎么加?能想到的就是依次加,个位与个位加,十位与十位加,加完有进位就进位就行了(有人想先把它们转化为整型相加后再转化为字符串,这样不可以,可能数很大相加时就不对了)。因为要相加所以需要有个进位值,最开始进位值是0,4+6+进位值=10,10留下0,然后进位值变为1;然后再3+5+进位值,如果结果超过10继续保留进位值,没超过进位值就变为0;剩下的2和1没有值与它们相加不能直接拷贝下来,因为如果遇到999+2的话直接拷贝下来是不对的,所以还要依次和0和进位值相加并做判断。

比如1234+56,这些字符串得先从取尾部开始相加,所以要分别定义end1和end2指向尾部。然后从end1和end2指向的位置开始取值,取完后就向左走,都取完才结束;因为不确定有没有值,所以可以用个三目,如果有就取出字符,没有就给0;因为取出的值要和进位值一起相加,然后判断进位值需不需要改的问题,这里是整型运算,所以取出的字符记得减去字符0,最后结果经过模10和除10处理后进行是否修改进位值和放到一个新串里,放的时候记得变为字符;放到新串的值是从头开始放的,所以最后记得逆置一下:

这样提交后还有问题,如果两个串中只有一个值,那最后依然要把进位值加上,所以最后判断一下如果出来后carry是1,那就加字符1:

最后不想逆置也可以一开始就使用头插,string里面没有头插,因为效率很低,可以采用insert强行插入:

这样也可以完成,但效率明显低了。

    扩容这还有个问题是,扩容有时候是会付出代价的。扩容原地扩容还好,但不能保证每次都原地扩,那有什么好的方式可减轻这种代价吗?所以它还提供了一个接口函数叫reserve:

它会请求一个容量的改变。它的参数里有个n,意思是会对容量进行改变。比如我知道string要插入100个,就可以s.reserve(100):

此时就不会再有一系列扩容的消耗,因为空间提前开到了合适的要求。意味着reserve作用是在我知道我需要多少空间时提前把空间一把开好,这样就不存在扩容的概念了。那我想要100个它一定会开到100吗?这个是不一定的,但一定会开到大于等于100。所以reserve整体而言它在这就是扩容,扩容的意思是我1大概知道我要多少空间我提前开好,避免插入数据慢慢不断增容,这样代价低了很多。

文档对参数也有说明,如果n大于当前string的capacity,它会引起扩容到n字符或者更大。还有其它情况有一些不正常的请求,要求缩小容量,它就会执行去释放一些空间。可以验证一下看实际会不会缩:

可以看到直接缩不会缩,用clear清理一下数据后发现缩小了。除了reserve,还有个resize:

resize有两个,单纯给个n,resize就会对长度进行改变。也就是说reserve就是单纯扩容,resize是开空间加初始化:

调试时看到resize默认填\0。不想要字符0可以填写自己想要的值,如s2.resize(20, 'x')。resize这里的n如果大于当前空间就扩容并填充,n如果小于string长度会删数据:

相当于size一定会变到n值,但没有缩容。如果s1.size(0)也是不会缩容的,因为一般情况是不会缩容的,缩容的代价也很大(先要拷贝数据,再释放原来的)。那就想单纯缩小容量有没有方法呢?

C++11提供了shrink_to_fit,它会缩减到它的size:

这里底层可能考虑了其它原因使缩了后比size大,一般是不建议缩的,现在的内存都很大。

    下一部分就是关于方括号[]:

通过[]可以很方便的像数组一样使用,返回pos位置的字符,也是可以通过下标进行修改的,如果是const对象就不能修改,它们之间又构成了函数重载。这里还有一个at:

它的功能和方括号一样,但平时更多用的是方括号,因为比较直观,可以认为at是在没有运算符重载的时候出现的。它们真正的区别是它们对越界的检查不一样,at是抛异常,[]是断言:

    再看下一部分:

下一部分中前三个已经提过了,push_back是插入一个字符;append是一个系列,如可插入一个字符串(3),可插入一个字符串前n个字符(4),可插入n个字符(5),插入某一部分(2),插入一个string(1);但最好用的还是+=。再来看assign:

它是赋值的意思,它和上面三个有不一样的地方:

assign没有追加,直接进行了覆盖,assign也提供了一个系列,这里了解一下就行。string没有支持头插和中间插入,所以就提供了insert和erase:

insert也重载了很多,在pos位置插入一个string(1),在pos位置插入部分string(2),插入字符串(3),插入字符串的前n个(4),插入n个字符(5),迭代器版插入某个字符(6),迭代器版插入一段区间(7)。这里稍微演示几个:

假设我想用迭代器,但不想在最开始插入怎么给迭代器呢?可以s1.insert(s1.begin()+10, 10, 'y')。再看看erase,它可以支持从pos位置删除len个字符,如果len给多了或者不给就用npos,此时用多少删多少;还可以传迭代器,比如完成头删;下面演示一下:

下一个接口看replace:

它可以完成替换,也看到实现了很多:把pos位置开始的len个字符/迭代器区间的直接替换为后面的str(1);替换为str的一部分(2);替换为字符串(3);替换为字符串的前n个(4);替换为n个字符(5);下面简单演示一下:

上图把world先替换成了很多x,然后又把很多x替换成了yyy,这一来一回看似轻松,实则底层付出了很多。所以这里有这样一个题目:string s2("hello world hello xxx"),把所有的空格替换为%20。这里可以用replace,遍历遇到空格就replace一下,但效率太低了。但最好的方式是:

    再看下一部分,get_allocator是有关空间配置器的不用管,data和c_str是一样的,c_str就是返回底层的c型字符串:

所以打印值可以这样:

这两种写法的意义不一样,上面是调的string的流插入;下面是识别为char*,调的库里对这的支持。它的作用是和c的一些接口函数配合:

copy用的比较少这里不提,再看string的下一个部分:假设要求把网址三个大件(协议、域名、资源)部分分离出来,以前把这里写死了,换一个网址就不适用了,现在要求随便换个网址都能完成这个事。这里先看看find:

可以find一个字符(4),find一个字符串(2),find一个str(1),pos这里还给了一个缺省参数,意思是从pos位置开始搜索,默认是从0开始搜索。所有网址都有个特征是以://开头,所以就先size_t pos1 = url.find("://"),find会返回匹配到的第一个字符的位置,如果没有找到就返回npos。找到后先要取出协议部分,可以用构造里面拿某一部分来构造,除了构造还可以用substr:

可以取从pos位置开始的len个字符,所以定义好url后可以这样取,protocal = url.substr(0, pos1),此时就取到了协议部分。下一部分取域名,要找到com后面的/,还要继续find,不能直接size_t pos2 = url.find("/"),因为会找到前面有的/,所以用find可以控制查找起始位置的功能,从域名开始找,所以就size_t pos2 = url.find("/", pos1+3),同样和上述一样判断有没有找到。最后剩下的就直接从pos2+1开始有多少取多少:

这样从此以后任意的网址都可以这样拆出来了。

    有时候有些场景find并不能帮我们很方便搞定,所以有个rfind:

比如找文件后缀的场景,有多个点不好从前开始找,找最后一个点才合适,rfind就是从前后往前找的。还有些简单了解一下:

find_first_of是从头开始找第一次出现的所包含的任意一个字符,find_last_of是从后往前找所包含的任意一个字符。

find_first_not_of是从头开始找第一个不在字符串里面的任意一个字符,find_last_not_of是从后往前找没有包含的任意一个字符。compare不用管,因为有运算符重载。还有个getline,可以结合题目来看看:

字符串最后一个单词的长度_牛客题霸_牛客网对于给定的若干个单词组成的句子,每个单词均由大小写字母混合构成,单词间使用单个空格分隔。输。题目来自【牛客题霸】https://www.nowcoder.com/practice/8c949ea5f36f422594b306a2300315da?tpId=37&&tqId=21224&rp=5&ru=/activity/oj&qru=/ta/huawei/question-ranking这里的常规思路是这样的,用流提取输入完后,去找最后一个空格,size_t pos = str.rfind(' '),然后用size - (pos+1)就是最后一个单词的长度:

提交后发现报错了,如果没有空格就是字符串的长度,所以修改:

发现还是不对,预期是1,实际是5。放到vs中测试一下:

发现输入ABSIB T后,str里只有ABSIB,这是因为scanf和cin都有个特点,有时要输多个值,默认规定多个值间用空格或换行进行间隔,读取时遇到换行或空格就认为当前读取结束了,所以只读了前5个,此时T还在缓冲区中,除非再读一次是可以读到的:

所以要读一个字符串如果有空格是读不到的,所以要么手动读,但万一有很多空格太麻烦,所以想连续读一行遇到空格不结束就用getline:

它可以控制结束字符,默认是遇到换行结束:

    string这还有一些东西,C语言中整型转字符串和字符串转整型用itoa和atoi:

那如果是浮点数呢?

所以C++和string配合是非常好用的,to_string它是一个系列,传了就可以转,这个是C++11支持的。字符转整型有stoi,默认10进制转;还有字符转double,用stod。

    再看下一部分:

其实string也是模板,string是被typedef的,它里面传的是char,它本源的类是basic_string。为什么要typedef string呢?因为除了string还有其它:

所以写成模板是为了兼容其它类型字符串的管理,后面的这些是和编码有关的,了解就行。

3.string的模拟实现

    现在进行模拟实现string,暂时不写成模板的,就写一个简单的string。string需要的成员有:指针,用来指向开好的空间,还有size和capacity,其实就是之前的顺序表的套路:

下面继续加东西,那string如何进行初始化呢?对照文档看string的构造函数很多:

我们只实现最常用的,用一个字符串初始化是常用的,那string的构造函数怎么写呢?它的作用是完成初始化,可以这样初始化吗?

不可以,首先const char*到char*涉及权限的放大,其实万一成功传了常量字符串,_str这都不支持修改,所以string的构造函数不能直接用字符串去初始化,必须要去开空间:

这里capacity是指能存多少个有效字符,所以也包括\0。有时候有人也会换成上图右边的写法,因为strlen是O(N)的接口,为了节省消耗可能这样写。一般建议边写边测试,这里用右边测试一下:

发现_size和_capacity好着呢,但_str好像有问题,这是因为初始化列表初始化的顺序不是按照我们写的顺序,而是按照声明的顺序,所以先走了_str,此时的capacity是个随机值。这里有两种改法,第一种改法就是把顺序换过来保持一致:

调式看到没有字符,因为_str仅开了空间,并没有拷贝数据过去,所以用strcpy把str的东西拷过去:

此时也验证了之前说的尽可能使用初始化列表,但并不是说只有初始化列表就行,因为有些功能无法在初始化列表全部完成。这个写法的坑是有人要是随意调换了顺序这个代码就又有问题了,所以有人还选择这样初始化:

一部分放下来或全放下来都可以,因为内置类型放哪里初始化影响都不是很大,自定义类型最好在初始化列表初始化,因为自定义类型不写初始化列表也会去调它的默认构造。下面再顺势写一个析构函数:

就直接释放_str,把_str置空,把_size和_capacity置0。还可以提供一个函数支持看数据,不然每次看监视窗口不是很方便,有个接口叫c_str,可返回c形字符串:

有了这个后就可以边走边打印了。假设我们需要个无参的构造来构造一个对象:

此时说没有合适的默认构造可以用,默认构造有三种:无参的;全缺省的;我们不写编译器自动生成的。此时我们写了编译器就不会自动生成,其它两个也都1没有,所以自己写个无参的。那可以这样写吗:

不可以,因为这里_str指向空的,此时访问空指针打印就崩了。可以拿原来的string对比一下:

发现没参数初始化的时候里面放的是\0。所以实现时开一个空间,然后里面放\0:

此时打印就没有问题了。这里可以给缺省值把无参和带参合并一下:

上图左边的方式都可能有着一些问题,最优的写法是右边这样,这里给空串,因为常量字符串默认有\0。现在想遍历string怎么遍历?我们前面说了三种方式,第一种方式是下标+方括号:这种方式首先需要有个size,实现size返回_size就可以了(size和c_str一样,这两个成员函数都是建议加const的,这样普通对象和const对象都可以调用):

然后再实现operator[],可像数组一样去使用,断言pos的合法性,然后返回pos位置的字符。这里可以用引用返回,因为出了作用域对象还在。并且[]这里还要实现一个版本,因为当我是const对象时就不期望被修改了:

这样就可以测试一下完成第一种方式遍历了:

第二种遍历方式是迭代器,在学string时用迭代器要string::iterator,说明string里面应该有个叫iterator的类型,这个类型在string这个类的类域里面。如果直接用iterator这样写编译器是找不到的,因为有个原则是编译器需要用一个变量或类型默认会去全局找,去全局找只有string,没有iterator这个东西,所以一般在命名空间域或类里面都要指定命名空间域或类域,然后才会去那里面搜索。因为string::iterator这样用,所以iterator是string里面的类型,类中定义的类型有两种,一种是定义内部类,一种是考虑typedef,这里是这样的:

然后begin要返回第一个位置的迭代器,end返回最后一个有效位置的下一个位置的迭代器。实现begin就返回_str,_str就是第一个位置的指针;实现end返回_str+size:

此时又可以测试遍历了:

第三种方式是范围for:

感觉好像什么都没有写但范围for可以正常走,其实我们写了迭代器就支持范围for了,因为范围for表面看很智能,其实底层被替换成了迭代器,可以认为第二种和第三种方式没有区别。比如我们屏蔽.h中的end,.c中的范围for就用不了了:

并且发现这个替换对名字也是有要求的:

有时候可能遇到const对象:

所以普通的不能调,还要实现连两个版本,普通版本迭代器可读可写,const只读不能写。

    下一部分来看看增删查改,先看看push_back和append,实现常用的。我们要尾插数据,首先考虑扩容,当_size==_capacity的时候直接往2倍扩。push_back这里走2倍可以,append这走2倍扩容不一定够,至少要扩展到_size+len。这里的扩容逻辑怎么实现呢?之前说过一个函数叫reserve,它可以修改容量,但push_back这不能开始直接扩容到2倍,因为可能有用空串初始化的场景,所以判断一下,如果是0给个固定空间,否则往2倍扩容;append这里直接扩容到_size+len就行:

这里用reserve时不用再+1放\0,因为reserve参数指的是想要多少字符的有效空间,实际里面会多开一个加\0。现在先实现reserve,如果n>_capacity再扩容,此时别人可能再外部直接用这个函数,然后new多开一个空间给\0,然后把_str的数据拷贝过来,再释放旧空间,让_str指向新空间,再修改capacity:

然后就可以完整实现push_back和append了:(strcpy从\0位置开始拷贝)

下面测试一下:

但最喜欢用的还是operator+=,+=一个字符底层是push_back,+=一个字符串底层是append,只是好用,+=要返回这个对象:

    下面来实现insert,库中的insert实现了很多,这里还是只实现最核心的。要实现在pos位置插入n个字符,首先第一步考虑pos位置的合法性,然后考虑扩容。从pos位置插入数据要考虑挪动数据。定义end指向\0,把end挪给end+n:

依靠上面的思路代码实现完如上图,但这个代码问题是如果pos等于0,此时end走到pos位置时再--变成了-1,但size_t不会小于0,会变成整型的最大值。因此有人会考虑说把size_t换为int:

但还是挂了,调试时发现end等于-1的时候又进去了,因为在一个运算符两边当两个操作数类型不一样会发生整型提升,通常范围小的向范围大的提升,所以-1被提升成了size_t类型就又进去了。所以有这样几个方案可以改进:1.可以在这里强制类型转化一下end >= (int)pos;2.还可以这样考虑,平时end在\0开始挪会有end>=pos,没有=就不会有问题,所以如果开始end在后一个位置,然后把end-1挪给end+n - 1,此时就可以把等号去了,但这样代码缺点是很难读;

3.之前说过string里有个npos,可以附加条件补充end不能等于npos。这里用第三种:

这里不能直接static size_t npos = -1这样给缺省值,因为缺省值是给初始化列表的,静态成员变量不走初始化列表,静态成员变量并不属于某一个具体对象,而是属于一整个类或所有对象。(补充:这里有个奇怪的地方是const static size_t npos = -1又可以了,但换成double又不可以了,所以平时建议老老实实的声明和定义分离)。实现插入字符串和前面几乎类似,区别是n变len,把字符拷过去就可以:

    下面实现erase,这里主要实现第一个,从pos位置开始删除len个字符。首先检查pos位置的合法性,再判断len是否等于npos或pos+len是否大于等于_size,如果有说明后面要一把删完,所以直接在pos位置放一个\0就可以了,然后把_size改为pos就行了,再到_str[_size]位置放\0。如果不是一把删完,比如删除3个,就定义end从后往前挪着覆盖,结束条件是end<=_size,这样可以把\0挪过去,最后让_size再减去len就行:

    下面实现find,可以在pos位置开始查一个字符,也可以从pos位置开始查一个字符串。首先验证pos的合法性,查找一个字符就直接从pos位置开始找就可以了,找到后返回下标,没找到返回npos:

下一个就是找字串,可以直接用strstr暴力匹配查找,strstr找到了返回原串中匹配位置的指针,没有找到就返回空。但find要求返回位置,_str代表的是起始位置,所以返回ptr - _str就可以了:

    实现了上述后,可以验证一下前面实现的分割网址的功能是否能用,因此再实现一个substr,取从pos位置开始的len个字符。因为len可能很大,所以定义n记录实际取多少个,如果len等于npos或pos+len大于_size,说明后面有多少取多少,取_size-pos个字符,否则实际长度就是len。确定好实际取多少个后就开一个新串,大小是n,然后从pos位置开始取n个到新串里就可以了:

下面测试一下:

发现程序崩溃了且结果不符,因为走substr时最后返回tmp,这里是浅拷贝,出了作用域后原对象会析构。也就是现在的问题是tmp指向一块空间,这个空间上存的是我们要的字符串,在这不是用tmp返回,而是开了个tmp的拷贝,这个拷贝是浅拷贝(我们不写拷贝构造自定义会去调用它的拷贝构造,内置类型完成值拷贝),此时_str和tmp指向同一块空间,出作用域tmp销毁,指向的空间销毁,然后用临时对象去调c_str就报错了。所以我们不期望走浅拷贝,而是走深拷贝,就是开一块一样大的空间一样大的值:

    继续看resize的实现,resize给了两个版本,第一个版本是把size弄到n且默认填\0,第二个版本是把size弄到n同时添指定的字符。但其实string不太喜欢用resize,不过这里可以去实现一下,可以通过给缺省值直接让两个版本合二为一。怎么实现呢?下面来分析一下:有这样几种状况,比如现在_capacity是15,_size是10

此时分别resize(8)/(12)/(18)分别是什么状况呢?这个过程严格说可以分为两类,第一类是删除数据,resize是可以删除数据的,比如现在_size是10,我要resize(8),此时相当于在删除数据,相当于只保留这里的前8个。resize(12)相当于插入数据,resize(18)相当于扩容加插入数据。所以如果n小于_size,此时_size变为n,顺序表抹除数据没有意义,直接后面放\0。剩余一种情况可以直接用reserve,因为它会检查,比_capacity小时不会扩容的,大才扩容,容量合适后就填数据,从_size位置开始填到n,没有指定数据默认填\0,最后末尾也要加\0:

    下面来实现流插入和流提取,因为抢占第一个位置的原因,流插入和流提取不能写成成员函数,写成全局函数即可。这里不用引用返回是会报错的,因为ostream这个类做了个防拷贝:

防拷贝就是不想让我们用拷贝构造(先简单了解就行)。现在假设要对string进行打印该怎么打印呢?

可以按照上图左边定义个友元再访问,但最好的方式是像右边一个字符一个字符的去打印。下面来测试一下:

测试时报错了,本质是sring引起的,operator<<函数的实现我们没有放到bit命名空间里面,.h会在.cpp中包含,.h的参数string默认认为取的是std的string,所以报错说没有接收bit::string。因此这样改就可以了(指定或放在命名空间里面):

此时就可以打印了。用c_str和重载流插入常规情况下没有什么差别,但c_str会返回c形式的字符串,打印的是const char*,它的打印原则是遇到\0就终止。流插入的重载是不管\0的,和\0没有关系,长度实际多长就打印多长。下图可以体会到差异:

(有些编译器打印\0会有空格,有些没有)。因此也反映出有过有中途加\0这种情况就不敢用str系列了,否则可能会有问题,因为str系列遇到\0会终止。引出了这样的问题,就需要检查之前的代码中用str系列是否有问题,若有问题把strcpy换为memcpy,memcpy是有多少拷贝多少:

这里改为_size+1,因为有个\0。strlen不用换,因为用strlen的目的就是为了知道\0之前是多长。其实这个不换问题也不大,但为了保持统一性这里就换了。这里看个明显的:

上图就是提前遇到\0的问题,因此进行改进:

常量字符串肯定是遇到\0就截止了,只有string对象可能中间加了\0,若以\0为终止看待string对象就会有字符读不完问题。也就是c的字符以\0为终止算长度,string不看\0,以_size为终止长度,所以string拷贝时以_size为准。reserve遇到中间有\0扩容也有问题:

有汉字是因为调到了reserve,它拷贝到\0就停止了,恰好随机值对应的随机字符,所以改一下:

append这的strcpy不换也行,因为是把外部的常量字符串弄进去,常量字符串本身以\0截止,但换成memcpy肯定没问题,所以统一一下:

    再看下一个流提取,区别是参数对象哪里不加const,因为拿到的数据要放到s里面,那可以按照下图方式写流提取吗?

不可以,因为开始不一定有空间,可能是个空对象,就算提前要reserve一下也不知道要reserve多少。所以应该一个一个去提取字符,如果字符不是空格或换行就取(因为额流提取的本质是用空格或换行分割):

但测试的时候发现不可以,我们输入了\n,但ch好像没有拿到(流提取和scanf是一个阻塞型接口,会去缓冲区拿数据,如果没有数据就会陷入阻塞)。通过调式验证一下:

输入的是1空格2,但是第二次拿到了2,空格如同被忽略了一样。那为什么空格和换行拿不到呢?先看个例子理解一下:

假设要依次输入5个值存到buf数组里,默认是空格或换行分割,也就是说流里面默认就不会读取空格或换行,它认为空格和换行是多个值之间的分割,比如输入不同整型必须带空格或换行来区分是不同的整型。那该如何解决呢?cin是istream类型的对象,istream类型对象中有个get函数可以解决这样的问题:

get每次读一个字符,不管什么都可以拿出来:

这样再输入hello就可以显示了。如果显示带空格的句子可以把!=' '这个条件去掉,其实这也就类似于getline的实现。这块还有个问题 :

比如我再次输入的时候,库里的意思是要对之前的覆盖,但现在自己写的这里并没有完成覆盖,因此要clear一下原来的数据。再写一个clear,在0的位置填个\0就可以了,再把_size弄为0,这样也就可以和库保持一致了:

这个代码还可以优化一下,+=感觉不是很好,因为如果我输入特别长的字符就要从小到大不断的扩容。一次reserve很大空间也不好,因为可能用不到。因此可以提前开个大数组buff,每次获取一个字符就往buff里面放,如果buff满了就在最后放\0,再把buff整体+=到s里面(这样就可以一次扩好容),然后i置为0继续走。如果遇到空格和\0出来后,判断i不等于0,说明有类容,就在末尾填\0,然后把buff加到s里面:

这里就像有个桶一样先累计起来,到一定量再放进去。还有个问题是库中我们输入空格空格hello输出的还是hello,而我们自己写的拿不到:

所以写个循环,如果开始ch等于空格或换行,那就继续读下一个就行,这不处理就相当于消了:

    string也是可以实现去比较大小,它也是要提供一系列比较大小的(比较这样的函数都是只读函数,不用修改数据,加上const),先实现小于,string比较和c语言一样按ASCII码比较,用strcmp可能会遇到中间\0的问题,所以用memcmp按指定长度比较。memcmp这要给个_size,按小的那个去走,但遇到hello和helloxx比较就有问题了。因此用memcmp如果结果是相等还要看看长度是否相等,如果长度相等才相等,否则长的大。

这里还可以不用memcmp自己去写,定义i1和i2,它们都小于各自的_size,如果i1对应的字符比i2小就是真,大就是假,等于就都继续走。循环结束后可能都结束了(1),可能i2走完了(2),可能i1走完了(3)。这里分开讨论,如果是(1)和(2)的情况就返回假,是(3)情况返回真:

这样就实现完成了,如果想简便可以写成右图的样子直接返回。实现==就是在两者_size相等的前提下看比较出来结果是不是等于0:

剩下的就都可以复用了:

下面来测试一下:

    下面说说拷贝问题,我们没人写赋值时调了赋值,编译器会生成赋值,但默认的赋值是一个浅拷贝,浅拷贝有析构两次和内存泄漏的问题。赋值是两个已经存在的对象间发生的,现在把s3赋值给s1:

浅拷贝后s1中的_str直接指向了s3的_str的空间,程序结束后调了两次析构,s1中的_str原来指向的空间也没有被释放,所以也有了内存泄漏。所以赋值的深拷贝需要把s1中的_str释放了,再开一块和s3的_str一样大的空间,再把s3的_str的值拷贝到新空间,再让s1的_str指向新空间,最后再更新_size和_capacity:

再看一种新玩法,用std的赋值:

将s3赋值给s1,调用拷贝构造构造了tmp(自己前面实现的构造也是深拷贝),tmp和s3一样大的空间一样大的值,反正tmp出作用域就销毁了,于是s1和tmp进行了交换:

此时s1就有了和s3一样大的空间一样大的值。那可以直接std::swap(tmp,*this)交换对象吗?不可以,此时会有栈溢出的问题,因为本来现在就在实现赋值,赋值里面又去swap两个对象,然后两个对象又调赋值,然后又去调用swap,这样就死循环了:

所以不能按上述那样写,需要去挨个交换成员。string里面也提供了swap函数,因为两个string对象可能也要交换,摸你实现时交换_size,_capacity,_str就可以了,然后赋值可以实现的更加简化了:

再来看一个赋值的写法:

这里是传值传参,调用拷贝构造,tmp一上来就是s3的深拷贝,然后用tmp再和s1交换,tmp出作用域时会带走s1原来的空间。拷贝构造这也可以试着用一下新写法,构造一个tmp,然后一交换就可以:

运行时这段代码崩了,说明是有问题的。调式观察一下:

我们没有对成员变量进行处理,右边看到有处理是不能保证的,部分编译器才会处理,不能把这个当做常态。之前说过,没写在初始化列表对内置类型默认是不处理的。这段代码可能在我们这里可以跑跑到其它地方去可能会报错,所以我们要自己处理:

析构这里也可以加检查,不是空再释放:

再次运行的时候发现又好了,用出错时的代码再调试观察一下:

s3想赋值给s1,s1要释放,s1想要和s3一样大的空间,这走了拷贝构造,tmp想构造和s3一样大的空间一样大的值,但这里this中都是随机值,没有初始化,然后换给了拷贝构造里的tmp,tmp出作用域析构时出了问题。遇到hello\0world,新的拷贝构造写法还会有坑(构造哪里没有是因为规定以\0截止),新拷贝构造里的tmp只有hello,由下图可以验证:

所以新写法在这一块是有缺陷的,用以前的写法会相对好一些。

4.写时拷贝(了解)

    string这还有个小点,string有些地方玩的时候还有些其它写法,有个写法叫引用计数的写时拷贝(了解就可以)。就是有时候发现深拷贝的代价非常大,比如s1指向一块空间,s1拷贝构造s2,拷贝后我也不想做其它事情,那这样深拷贝代价是不是有些大。反正不敢别的就放那里,那能不能就做个浅拷贝?浅拷贝的问题有:1.共用一块空间会被析构两次;2.一个对象修改会影响另一个。有人引入写时拷贝计数解决这样的问题,加一个引用计数,只有s1指向空间时引用计数是1,s2指向时引用计数变2,假设还有s3指向就变3。数字代表有几个对象管理这块空间,因此可以解决析构2次的问题,析构前减减计数(s2析构时把计数减为1,s1再析构把计数减为0),变为0说明没人管理了再去释放。当我修改时发现引用计数是2,说明不敢直接修改,如果是1说明这空间是独立的,可以修改。此时做写时拷贝:写的时候引用计数如果不是1,则进行深拷贝再修改,同时改计数值。

linux中的编译器就引入了这样的方式(vs下没有用这样的方式):

还有个小点是关于std的string有个非常重要的特征:

正常说大小应该是26,但这里是40。原因是里面加了Buf数组,这个数组大小是16,存15个值,最后一个是\0:

也就是vs的设计是_size<16的时候,字符串存在数组中,没必要在堆上开空间;如果长度很长就存在ptr下,Buf数组相当于浪费了。

5.完整代码(string模拟实现)

//string.h

#pragma once

namespace ydx
{
	class string
	{

	public:
		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;
		}
	/*	string()
			:_size(0)
			,_capacity(0)
			,_str(new char[1])
		{
			_str[0] = '\0';
		}*/

		//string(const char* str = '\0')      类型不匹配
		//string(const char* str = nullptr)   给空strlen就崩了
		//string(const char* str = "\0")      可以,但没有必要
		string(const char* str = "")
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];
			memcpy(_str, str, _size + 1);
		}

		string(const string& s)
		{
			_str = new char[s._capacity + 1];
			memcpy(_str, s._str, s._size + 1);
			_size = s._size;
			_capacity = s._capacity;
		}

	/*	string(const string& s)
			:_str(nullptr)
			,_size(0)
			,_capacity(0)
		{
			string tmp(s._str);
			swap(tmp);
		}*/

		~string()
		{
			if (_str)
			{
				delete[]_str;
				_str = nullptr;
				_size = _capacity = 0;
			}
		}

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


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


		//string& operator=(const string& s)
		//{
		//	if (this != &s)  //自己给自己赋值就省略这些步骤
		//	{
		//		string tmp(s);
		//		swap(tmp);    //this->swap(tmp);
		//	}
		//	return *this;
		//}

		//string& operator=(const string& s)
		//{
		//	if (this != &s)  //自己给自己赋值就省略这些步骤
		//	{
		//		string tmp(s);
		//		std::swap(_str, tmp._str);
		//		std::swap(_size, tmp._size);
		//		std::swap(_capacity, tmp._capacity);
		//	}

		//	return *this;
		//}

		const char* c_str()const
		{
			return _str;
		}


		size_t size()const
		{
			return _size;
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

		const char& operator[](size_t pos)const
		{
			assert(pos < _size);
			return _str[pos];
		}

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

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

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

		void append(const char* str)
		{
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			memcpy(_str + _size, str, len + 1);
			_size += len;
		}

		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}

		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);
			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] = str[i];
			}
			_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;
			}
		}

		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 || pos + len > _size)
			{
				n = _size - pos;
			}

			string tmp;
			tmp.reserve(n);
			for (size_t i = pos; i < pos + n; i++)
			{
				tmp += _str[i];
			}

			return tmp;
		}

		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}
		//bool operator<(const string& s)const
		//{
		//	bool ret = memcpy(_str, s._str, _size < s._size ? _size : s._size);
		//	return ret == 0 ? _size < s._size : ret < 0;  //返回了ret,如果ret是0就看看第一个长度是不是小于第二个,不是0就看看ret是不是小于0
		//}

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

			return i1 == _size && i2 != s._size;
		}


		bool operator==(const string& s)const
		{
			return _size == s._size && memcmp(_str, s._str, _size) == 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);
		}
	/*	bool operator<(const string& s)const
		{
			size_t i1 = 0;
			size_t i2 = 0;
			while (i1 < _size && i2 < s._size)
			{
				if (_str[i1] < s._str[i2])
				{
					return true;
				}
				else if (_str[i1] > s._str[i2])
				{
					return false;
				}
				else
				{
					++i1;
					++i2;
				}
			}

			if (i1 == _size && i2 != s._size)
			{
				return true;
			}
			else
			{
				return true;
			}
		}*/


	private:
		size_t _size;
		size_t _capacity;
		char* _str;

	public:
		const static size_t npos;
	};

	const size_t string::npos = -1;

	ostream& operator<<(ostream& out, const string& s)
	{
		for (auto ch : s)
		{
			out << ch;
		}

		return out;
	}


	istream& operator>>(istream& in, string& s)
	{
		s.clear();
		char ch;
		ch = in.get();
		while (ch == ' ' || ch == '\n')
		{
			ch = in.get();
		}
		char buff[128];
		int i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 127)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}
			ch = in.get();
		}

		if (i != 0)
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;
	}

	/*istream& operator>>(istream& in, string& s)
	{
		s.clear();
		char ch;
		ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			s += ch;
			ch = in.get();
		}
		return in;
	}*/
};

//ostream& operator<<(ostream& out, const ydx::string& s)
//{
//	for (auto ch : s)
//	{
//		out << ch;
//	}
//
//	return out;
//}
//Test.c


#define _CRT_SECURE_NO_WARNINGS 1

#include <assert.h>
#include<iostream>
#include<string>
using namespace std;

#include "string.h"

void test_string1()
{
	ydx::string s1("hello world");
	cout << s1.c_str() << endl;

	ydx::string s2;
	cout << s2.c_str() << endl;
}

void test_string2()
{
	//ydx::string s1("hello world");
	//for (size_t i = 0; i < s1.size(); i++)
	//{
	//	s1[i]++;
	//	cout << s1[i] << " ";
	//}

	/*ydx::string s1("hello world");
	ydx::string::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << " ";
		it++;
	}*/
	/*ydx::string s1("hello world");
	for (auto ch : s1)
	{
		cout << ch << " ";
	}*/

	ydx::string s1("hello world");
	for (auto ch : s1)
	{
		cout << ch << " ";
	}
}

void test_string3()
{
	ydx::string s1("hello world");
	cout << s1.c_str() << endl;
	/*s1.push_back('!');
	s1.push_back('!');
	cout << s1.c_str() << endl;
	s1.append("wwwww");
	cout << s1.c_str() << endl;*/

	s1.insert(0, 3, '#');
}


//void test_string4()
//{
//	/*ydx::string s1("hello world");
//	cout << s1.c_str() << endl;
//	s1.insert(0, 3, '#');
//	cout << s1.c_str() << endl;*/
//
//	/*ydx::string s1("hello world");
//	cout << s1.c_str() << endl;
//	s1.insert(0, "****");
//	cout << s1.c_str() << endl;*/
//
//	ydx::string s1("hello world");
//	cout << s1.c_str() << endl;
//	s1.erase(2, 3);
//	cout << s1.c_str() << endl;
//	s1.erase(0);
//	cout << s1.c_str() << endl;
//}

//void test_string4()
//{
//	ydx::string s1("hello world");
//	cout << s1.c_str() << endl;
//	cout << s1.find("llo") << endl;
//
//	//cout << s1.find('q') << endl;
//}

//void test_string5()
//{
//	ydx::string url = "https://legacy.cplusplus.com/reference";
//	size_t pos1 = url.find("://");
//	if (pos1 != ydx::string::npos)
//	{
//		ydx::string protocol = url.substr(0, pos1);
//		cout << protocol.c_str() << endl;
//	}
//
//	size_t pos2 = url.find('/', pos1 + 3);
//	if (pos2 != ydx::string::npos)
//	{
//		ydx::string domain = url.substr(pos1 + 3, pos2 - pos1 - 3);
//		ydx::string uri = url.substr(pos2 + 1);
//		cout << domain.c_str() << endl;
//		cout << uri.c_str() << endl;
//	}
//
//}

//void test_string5()
//{
//	ydx::string url = "ftp://www.baidu.com/?tn=65081411_1_oem_dg";
//
//	size_t pos1 = url.find("://");
//	if (pos1 != ydx::string::npos)
//	{
//		ydx::string protocol = url.substr(0, pos1);
//		cout << protocol.c_str() << endl;
//	}
//
//	size_t pos2 = url.find('/', pos1 + 3);
//	if (pos2 != ydx::string::npos)
//	{
//		ydx::string domain = url.substr(pos1 + 3, pos2 - (pos1 + 3));
//		ydx::string uri = url.substr(pos2 + 1);
//
//		cout << domain.c_str() << endl;
//		cout << uri.c_str() << endl;
//	}
//}


void test_string6()
{
	ydx::string s("hello world");
	s += '\0';
	s += "!!!";
	cout << s.c_str() << endl;
	cout << s << endl;
	s += "xxxxxxxxxxxxxxxxxxxxxxxxxx";
	cout << s << endl;
	//ydx::string copy(s);
	//cout << s << endl;
	//cout << copy << endl;
	/*s.resize(8); 
	cout << s.c_str() << endl;
	s.resize(13, 'x');
	cout << s.c_str() << endl;
	s.resize(20, 'y');
	cout << s.c_str() << endl;*/

}

void test_string7()
{
	ydx::string s;
	cin >> s;
	cout << s << endl;

	/*cin >> s;
	cout << s << endl;*/
	//int buf[128];
	//for (size_t i = 0; i < 5; i++)
	//{
	//	cin >> buf[i];
	//}
	//for (size_t i = 0; i < 5; i++)
	//{
	//	cout << buf[i];
	//}
}

void test_string8()
{

	ydx::string s1("hello");
	ydx::string s2("hello");
	cout << (s1 < s2) << endl;
	cout << (s1 > s2) << endl;
	cout << (s1 == s2) << endl << endl;


	ydx::string s3("hello");
	ydx::string s4("helloxxx");
	cout << (s3 < s4) << endl;
	cout << (s3 > s4) << endl;
	cout << (s3 == s4) << endl << endl;


	ydx::string s5("helloxxx");
	ydx::string s6("hello");
	cout << (s5 < s6) << endl;
	cout << (s5 > s6) << endl;
	cout << (s5 == s6) << endl << endl;
}
//
//void test_string9()
//{
//	ydx::string s1("hello");
//	ydx::string s2(s1);
//	cout << s1 << endl;
//	cout << s2 << endl;
//
//	ydx::string s3("xxxxxxxx");
//	s1 = s3;
//	cout << s1 << endl;
//	cout << s3 << endl;
//
//	s3 += '\0';
//
//}



//void test_string10()
//{
//	ydx::string s1("hello");
//	s1 += '\0';
//	s1 += "world";
//	ydx::string s2(s1);
//	cout << s2 << endl;
//}

void test_string11()
{
	std::string s1("hello world");
	cout << sizeof(s1) << endl;
}

int main()
{
	test_string11();
	return 0;
}