目录
operator>>和operator<<以及clear()
基础框架
首先设计出string的框架,如下图。
一般在源码里,只要一种数据不可能为负数,那么整形的类型就是size_t,这也是为什么咱们的_size等变量的类型为size_t。
上图红框在默认构造处为什么要开1字节,带参构造为什么要额外加1呢?
因为string类中的_size和_capacity分别表示string中目前有多少有效字符、string目前可存储多少有效字符。注意这里的有效字符是不包括\0的,所以多出来1字节就是为了存储\0。
这里还有一个细节,在上图第一个红框处明明只要开一个char类型的空间,为什么不直接new char呢?为什么写成了new char【1】呢?这是因为方便在析构函数时统一写成delete 【】_str。
那我们默认构造时,可不可以如下图红框把_str设置成nullptr呢?
答案是不能,这里如果把_str设置成nullptr则会导致自己实现的c_str()接口无法使用,什么意思呢?(下图c_str右边的const表示该成员函数是一个const函数,这样const对象才可以调用这个接口,否则无法调用。注意非const对象也是可以调用const函数的。)
c_str接口是直接返回string内部的字符指针_str的,_str一般指向我们new出来的位于堆上的空间,空间上就是数据。这里如果_str为nullptr,则会在cout<<打印的时候出错,如下图右边红框处,其实这里已经崩溃了,为什么呢?
虽然_str的值为nullptr,但它毕竟是char*类型的数据,在<<string的介绍和使用>>一文中说过:scanf和operator<<()对于char*类型的数据进行输入输出时,是遇到\0才停止的,现在既然要打印数据,那肯定要访问数据,相当于cout打印char*的数据时会解引用,那么解引用_str时,因为_str是空指针,所以解引用肯定会崩溃的,所以在默认构造中不能把_str直接初始化成nullptr。
其实还可以把默认构造写成下图的样子,下图方案是给_str设置缺省值实现的。注意缺省值也不可为nullptr,否则依然不可使用cout打印c_str返回的C语言字符串,并且下图strlen(nullptr)也是会报错的。带双引号 “” 的数据表示常量字符串,常量字符串是以\0结尾的,所以“”看上去是空串,但实际上是\0,如果是 “\0”,则表示 \0\0。
这里补充一点:其实在实际开发中,上图的构造函数在初始化列表中初始化成员是不太好的,因为它们之间存在依赖关系,什么意思呢?比如上图在初始化列表中我就用了_size的值初始化_capacity,这样就叫做存在依赖关系,注意这样写需要格外注意在声明各成员时的顺序,即使你没有写错,但将来别人可能随便修改一下你声明成员时的结构,代码就会出现问题,所以不推荐这种写法。但我又不想太笨的一直在初始化列表里频繁用strlen计算,该怎么办呢?
正确做法是返璞归真,直接写在函数体内,如下图示例。
拷贝构造和赋值运算符函数
接下来说一说拷贝构造和赋值运算符函数
我们说如果不自己编写,会生成默认的拷贝构造,此时它会完成对内置类型的值拷贝(浅拷贝),对自定义类型去调用自定义类型的拷贝构造。
对于简单的类比如日期类来说,默认生成的就够用了,但对于string类来说是不能满足需求的,而且会产生各种错误。
我们知道string内部有成员_str,它是一个指针,默认的拷贝构造只完成了值的拷贝,相当于两个string对象的_str成员会指向同一块空间,那么当其中一个string完成析构函数delete 【】_str后,此时另一个string对象的_str成员就是一个悬空指针,因为它指向的空间已经被释放过了,当该对象析构时delete 【】_str就相当于释放悬空指针,所以程序就崩溃了。(悬空指针就是这块空间不属于用户,但你却释放它)
同理,默认的赋值运算符函数也有释放悬空指针的问题,并且还存在内存泄漏的问题。如下图利用默认生产的赋值运算符函数把s的值赋值给s1时,此时s和s1内部的_str就会指向同一块空间,s和s1析构时就会各自delete【】_str,此时就出现了释放悬空指针的问题;因为赋值后s1的_str指向和s的_str相同,导致s1之前的_str指向的那块空间就丢失了(注意默认构造是为s1的_str在堆上开了1字节空间的,用于存储\0,不要以为s1最开始时的_str为nullptr),所以导致内存泄漏。所以因为种种原因,赋值运算符函数也需要自己编写。
(注意下图的string是我们自己在mine命名空间中实现的string,test_string函数在mine的命名空间中,所以string如果不加std::,则默认调用我们写的string)
接下来开始编写拷贝构造和赋值运算符函数
编写operator=时,是需要考虑自己给自己赋值的情况的,其实上图逻辑即使没有if判断,也是正确的,但为了减少消耗,避免执行无效代码,我们加了一层判断,如果是自己给自己赋值,则什么也不做,直接返回即可。当自己给自己赋值时,也有一种情况是必须加if判断的,那就是你编写代码时先delete【】_str再memcpy,你一上来就把自己的数据清空了,那之后memcpy拷贝到新空间的数据就是清空后的脏数据,它们的01序列根据编码表显示出来可能就是烫烫烫了。
在operator=()时有个小细节,最好先new char【】,再delete【】_str。如果你首先就delete【】_str了,那么当new失败时抛异常直接跳出operator=函数体,也就是说此时s1=s2不仅赋值失败了,而且还把s1的数据delete了,这有点坑,但也无伤大雅,因为既然new失败了那代码本身就出了问题。
拷贝构造和operator=函数都在new char【】时+1是为了给\0预留位置。
其实在operator=中,return *this之前赋值已经完成了,只不过考虑到=可能会被连续调用,比如s1=s2=s3,所以才添加了返回值
上图中的拷贝构造和赋值运算符函数都是传统写法,它们还有现代写法。
拷贝构造的现代写法如下图。
string s1(s2)时,形参str就代表s2,首先用str.c_str()返回C语言字符串,并用这个C语言字符串构造出一个临时的string对象temp,那么此时temp就相当于s2,最后分别把temp的所有成员都和s1的成员交换,此时s1内部成员的值就等于temp的成员的值,temp内部成员的值就和交换前的s1一样了,因为temp对象是在拷贝构造函数体内定义的,所以出了拷贝构造,temp对象就会调用析构函数释放自己_str指向的资源,s1和temp交换后,temp的_str指向原来s1的_str指向的空间,如果不在初始化列表把s1(也就是str)的_str设置成nullptr,则此时temp的_str就指向一块未知的空间,temp的_str也就是野指针,那么temp调用析构函数delete 【】_str时,就会产生释放野指针的问题,导致程序崩溃,这也是为什么要在初始化列表将s2的_str设置成nullptr。 如果你在编写代码时,即使不设置成nullptr也不崩溃,那说明编译器自作主张帮你把s2的_str设置成nullptr了。
还有一点值得注意,交换string对象的值时,必须把成员分别拉出来进行交换,就如同下图代码的做法1,这里绝对不可以像下图做法2一般直接swap整个对象,程序会崩溃,为什么呢?
首先我们看看库中swap的逻辑,如下图红框处。
按照上图红框处swap的逻辑,如果交换的是两个string的对象,如swap(s1,s2),swap内部会首先调用一次拷贝构造,然后调用两次operator=函数进行赋值。
注意当前我们是在模拟实现拷贝构造时调用的swap函数,即执行string s1(s2)时内部有swap(s1,s2)语句,swap(s1,s2)内部又去调用拷贝构造string c(s1),拷贝构造c对象时,首先需要利用s1的_str构造一个string temp对象,但在当前场景下,s1的_str为nullptr【这是我们在拷贝构造初始化列表设置的值,因为string s1(s2)构造s1时需要创建temp对象,让temp对象去释放原来的s1的_str指向的空间,所以为了temp析构时不会delete野指针,这里设置成nullptr】所以在构造temp对象时,程序会因为strlen(nullptr)计算长度时崩溃,如下图,所以这里直接swap整个对象,程序是会崩溃的。
库里的string也是不能用nullptr来构造string对象的,也会崩溃,说明库里在实现时也是通过strlen计算该开多大的空间。
而且就算你把strlen的问题解决了,还存在一个无法解决的问题,因为拷贝构造会调用swap,swap内部又会调用拷贝构造,这样会无限的反复调用对方函数导致栈溢出,如下流程图所示。
而如果把s1和s2的成员单独拉出来swap,如下图做法1,则不需要考虑各种可能会导致程序崩溃的小bug。
赋值运算符函数的现代写法如下图。
和拷贝构造同理,这里也不可以直接swap整个string对象,比如swap(*this,temp)就会导致栈溢出,因为swap内部会调用operator=,就是说,我调operator=就会调用swap,而调用swap又会调用operator=,这也会无限的反复调用对方导致栈溢出,如下流程图所示。
所以综上所述,拷贝构造或者赋值运算符函数会深拷贝的类,使用swap交换它们的类对象时一定要小心。这里建议库中的swap最好只用于交换内置类型的对象,不然容易出现各种莫名其妙的问题。
size()以及operator【】
接下来编写下图的三个接口。
将size()函数设置成const函数是为了让const string对象也能够调用size()接口。
两个operator【】()在这里是构成重载的,因为this指针的类型不同,因为const函数的this指针类型为const string* ,而非const函数的this指针类型为string*。(*左边的const定值,*右边的const定向)
那么为什么要有两个operator【】()呢?
为了让const string对象调用const版本的【】,让非const string对象调用非const版本的【】,而这么做的原因是const对象肯定是不能修改_str指向的数据的,所以const对象调用const版的operator【】的返回值为const char&类型,而非const对象调用非const版的operator【】时是有可能修改_str内部的数据的,所以返回值为char&。
string的迭代器类
接下来为string类编写出string类专属的迭代器类
在<<string的介绍和使用>>一文中简单介绍过迭代器,为string实现的迭代器类实际上就是原生指针,如下图。
和上文中的operator【】()同理,这里的begin或者end也都构成函数重载,因为this指针的类型不同。进行重载的理由也相同,为了让const string对象调用const版本的begin或者end,这样在类外别人就无法利用解引用迭代器对_str的数据进行修改;让非const对象调用非const版的begin或者end,这样在类外别人就可以通过解引用迭代器对_str的数据进行修改。
额外一提,这里我们实现了迭代器类,因为范围for底层就是迭代器,所以是可以支持范围for的。
接下来开始编写增删查改的接口
reserve()
涉及到增加数据,则一定会有扩容的问题,所以首先编写扩容的接口。下图红框处+1是为了给\0预留一块空间,因为我们_capacity表示可存储有效字符的总量。
resize
push_back
有了reserve可以扩容后,我们选择先编写push_back接口,push_back一次只尾插一个元素。
在插入字符后,一定要记得在插入字符的后面添加\0,如上图红框处,否则在cout以C语言字符串打印string对象时,就会因为读取不到\0导致一直读取,导致打印乱码,直到恰好内存中有一个随机值刚好为\0才会停止打印,如下图打印出乱码就是因为push_back中少写了上图红框处代码。
operator+=
有了push_back,我们可以复用它实现operator+=()函数,如下图。
注意是可以连续+=的,比如有string s1,s2,s3,是可以s1+=s2+=s3的。
append
接下来我们实现append接口,append和push_back不同,它一次可以插入多个元素,注意append只支持尾插,不支持指定位置插入。
append插入时如果需要扩容,那么这里扩容的时候不能简单的认为扩容2倍或者扩容一个固定的大小就可以了,因为追加的C语言字符串的长度是未知的,扩容2倍或者扩容一个固定值都是可能不够的,所以这里我们采用直接计算C语言字符串的长度,然后把该长度和string中_str的长度相加,然后扩容到相加的这个值。
注意strcpy在拷贝字符串时,是会把\0也拷贝过去的,所以这里最后无需_str【_size】= '\0',所以上图代码中,_size+=len后,_size已经指向\0了。
这里拷贝C语言字符串时不推荐使用strcat,表示在原字符串后追加字符串,该函数是一个失败的设计,因为每次追加都得在原字符串中寻找\0,找到后再从\0开始追加,该函数最后也会把\0拷贝过去。如果非要使用strcat,可以算出原字符串\0的位置,避免内部逻辑去从头寻找,提高效率。
append对string类的重载如上图,只需要复用append对C语言字符串重载函数即可。
对于上图接口,值得一提的是,在for循环内部连续push_back时,有可能因为n过大,导致频繁扩容,这样效率就太低了,所以在for循环外面,在插入字符前,可以首先扩容_size+n个空间,这样在插入时则无需考虑扩容的问题了。
insert
接下来编写insert接口。该功能支持在指定的下标pos插入字符,所以得把pos以及pos后面的数据依次往后挪动一次。
_size下标指向\0,所以当pos的值等于_size时,相当于尾插,所以assert中是包含=的。
值得注意的是,上图在while的条件比较时,必须将两个size_t类型的变量end和pos都转换成int,为什么呢?
因为pos为0时,按照我们 _str[end + 1] = _str[end] 后end--的逻辑,当最后end==pos时,end此时为0了,按照我们的期望,end--之后就为-1,然后不符合循环条件退出循环,但end和pos都是size_t类型啊,所以end--后实际上的值是整形的最大值,导致一直死循环,对于检查越界严格的编译器下还会直接崩溃,因为此时已经严重越界了,所以这里得把两个变量都强转成int类型。至于为什么不直接用int类型,前面也说过,只要是该变量值不可能为负数,则库中源码一般都会使用size_t而非int。
那我能不能只把其中之一转换为int类型呢?答案是不行,因为size_t和int进行比较时会整形提升,把int的值提升成size_t的值,比如size_t x=1,int y=-1;此时cout<<(x>y) 的值为0,表示false。
如果不想如上图强转,也有其他方式解决问题,比如下图的做法2。上图中做法本质是每次循环让end指向的数据往后挪,现在的做法是每次循环让end直接指向数据应该挪到的位置,即end下标上的数据是不挪动的,比如我们让end先指向\0的后一个位置,第一次循环时,\0就应该挪到到end下标对应的位置。
接下来编写insert插入字符串的版本。
和insert插入字符的版本同理,这里不想在while判断时强转也有其他的解决方案。
如上图注释所说,这里拷贝字符串时不可以用strcpy,但可以使用strncpy或者memcpy,因为后两个接口都可以指定拷贝的字节数,这样可以避免拷贝\0到原串。
在面试中,如果时间紧急,可以先实现insert接口,其他所有和插入有关的接口都可以复用insert接口,如下图所示。
erase
接下来编写erase接口。
注意string或者其他容器的任何删除接口,比如erase()或者clear()等,它们都不会把物理空间释放,而只是处理数据,不让你访问到它们,并修改_size,这里_capacity一般是不会修改的。只有容器对象调用析构函数时,它们占用的物理空间才会被释放。
首先定义一个静态变量npos,方法有如下图两种。下图中有点问题,npos应该是公有数据,因为库中的npos在类外也能调用。
然后在删除的时候需要考虑两种情况,第一种是从pos位置开始,把往后所有字符全部删除,对应下图中上方红色箭头部分,第二种是从pos位置开始,把往后部分字符删除,删除len个字符,对应下图下方红色箭头部分。
下图是代码的放大版,做法1是自己挪动,逻辑为:让下标cur从pos处开始,将cur下标对应的值修改成cur+len下标对应的值,每次循环后cur的值都要+1,一直循环到(cur+len)下标对应的值为\0,最后一次循环cur下标对应的值要修改成\0。
做法2是靠库函数挪动。注意两个做法都对\0做了处理的,做法1中,_size下标对应的位置就是\0,while循环时条件是<=_size,所以最后把\0也是挪动了的,做法2的strcpy也是会拷贝\0到目标串的。
operator>>和operator<<以及clear()
接下来编写流插入operator>>()和流提取operator<<()函数。这里说说我对它的理解。
以前我看到cout<<某某或者cin>>某某时,因为cout和cin都是类对象(cout是ostream类的对象,cin是istream类的对象),而且cout<<某某这样的函数调用方式就和类对象调用成员函数时类似,导致我以为operator>>和operator<<都是类成员函数,事实上这是错误的理解方式。流插入和流提取函数都是全局的函数,它没有this指针,不是被某个对象调用的。
比如ostream& operator<<(ostream&,const string)函数,该函数的名字就叫做operator<<,它表示在全局完成对string类型的函数重载。至于为什么调用该函数时格式为 cout<<“hello” 而不是 operator<<(cout,“hello”),个人猜测是因为前者更简便,但如果你想要通过后者调用流提取函数也是OK的,如下图2演示。
事实上,你调用cout<<“hello”时本质还是调用的是operator<<(cout,“hello”),这是编译器帮你做的,它把cout<<“hello”转换成了operator<<(cout,“hello”)。
注意下图的写法1是不符合string要求的,因为operator<<打印string对象时,是根据_size大小判断打印多少字节的,写法1复用了operator<<对const char*的重载,虽然代码不会崩溃,但不符合string的特性了。
分割线---------
可以如上图把流提取设置为成员函数吗?
答案:流提取或者流插入最好不要写在类内,因为成员函数的第一个参数是隐含的this指针,如果如上图把流提取设置为成员函数,则在类外调用时格式如下图演示。可以发现明显和正常调用的规则不同,我们不要打破规则,所以放弃这种写在类内的方案。
operator<<定义在类外时,写法如下图。
注意下图的写法1是不符合string要求的,因为operator<<打印string对象时,是根据_size大小判断打印多少字节的,写法1复用了operator<<对const char*的重载,虽然代码不会崩溃,但不符合string的特性了。
流提取或者流插入函数对某种类型完成重载时,函数不一定非要是某类的友元,因为不一定需要访问private成员。
这里operator<<的返回值是干嘛的呢?用于连续输出的,比如cout<<x<<y,输出顺序就是先输出x,即cout<<x,输出结束后(cout<<x)作为一个整体,表示返回的cout对象,然后cout<<y,这里x和y的类型可以完全不同,cout是支持连续输出不同类型的数据的。
最后咱们可以总结出一点,既然operator<<既可以在类内实现成成员函数,也可以在类外实现成全局函数,那其他带operator的运算符函数,如operator+ 或者operator<=等等运算符函数也应该支持既可以实现在类内,也可以实现在类外。
然后进而咱们能体会出,好像operator系列的函数和正常的函数没有什么区别,正常函数的名字是字符构成,operator系列函数的名字也是字符构成,只不过带了运算字符如+;正常函数可以实现成成员函数,也可以实现成全局函数,operator系列函数也如此。
所以operator系列的函数和正常的函数相比,唯一的区别在于调用的格式,正常函数是通过函数名加参数列表调用,如func(x,y),而operator系列函数,比如operator+是通过如x+y调用,参数位于函数名中的运算符两侧,虽然最后也会被编译器转换成operator+(x,y)调用,并且我们如果不嫌麻烦,也可以直接写成operator+(x,y)去调用该函数,免得编译器去转换。
接下来完成流插入函数operator>>的编写。
下图示例是一种错误的写法,虽然能对string s进行输入,但无法结束输入,为什么呢?
答案:从键盘输入数据到内核缓冲区后,cin或者scanf从C语言缓冲区读取数据读到指定对象中时,是会无视空格‘ ’ 或者 换行符‘\n’ 的,也就是说cin或者scanf认为这俩不是有效字符,从而不会把空格‘ ’ 或者 换行符‘\n’ 读进对象中,而是把它们当作对象与对象的间隔符,如上图in>>c(本质是cin>>c)时,因为c是char类型,cin或者scanf则把 ‘ ’ 或者 ‘\n’ 当作char对象的间隔符。同理,当cin的对象是string类型,则把 ‘ ’ 或者 ‘\n’ 当作不同string对象的间隔符。
所以正确写法如下图所示,cin是istream类型的对象,该对象可以调用成员函数get(),该接口会把所有C语言缓冲区的字符都当成有效字符读取到指定的对象中。
介绍一下get接口,如果有char ch,则cin.get(ch)等价于ch=cin.get(),表示从C语言缓冲区读取一个字符到ch变量中。如果程序正在使用 get 函数简单地暂停屏幕直到按回车键,并且不需要存储字符,则该函数也可以这样调用:cin.get()。
cin也支持连续输入,比如cin>>x>>y,输入顺序就是先输入x的值,即cin>>x,输入结束后(cin>>x)作为一个整体,表示返回的cin对象,然后再次cin>>y给y输入值,这里x和y的类型可以完全不同,cin是支持连续输入不同类型的数据的。
上图代码的效率其实是非常低效的,因为operator+=()对char类型的重载函数内部复用了push_back接口,而push_back内部的eserve开空间时扩2倍,此时因为+=c的次数是不确定的,导致s可能会频繁扩容,效率低下,那能不能如下图,一上来就给string对象s开128空间呢?这样不就不用频繁扩容了吗?答案是也不合适,如果输入的字符串较短,则浪费空间,如果太长,也避免不了频繁扩容。
为此我们实现出了如下图的更优版本。
下图利用一个数组当作缓冲,然后让string对象+=该数组,+=接口复用了insert(const char*)接口,insert内部是计算了const char*的长度的,reserve会扩容到指定字节。这里为什么这样设计呢?因为有数组做缓冲,string每次+=的对象就变成了数组,相比于string每次+=一个char,string去+=的次数更少了,所以扩容的次数更少,所以效率更优。
并且这边被当作一层缓冲的数组在离开operator>>的作用域后是会被释放的,因为数组是函数内定义的局部数组,所以不会发生内存泄漏的问题 。
注意上图operator>>的代码,无论是低效版本还是高效版本,它们都还存在一个小BUG没有解决,那就是如果string s对象本来就有数据,则在cin>>s时,因为咱们实现的operator>>接口的逻辑就是在原有的string对象s上+=上图的c数组,所以string对象s中,在cin之前就已经存在的数据不会被覆盖,如下图演示。
但通过库中的operator>>调用cin>>s时,如果s内已经存在数据,则会将它们清空,然后输入,如下图。
所以为了和库保持一致,咱们也要实现该功能,如下图代码则是最终版本。
修改起来很简单,只需要增加一个clear()接口,把_str【0】设置成\0,然后把_size设置成0即可。至于这样做的原因,则是和>>中调用的+=接口有关,实际上+=也是复用其他的接口,所以准确来说clear()之所以这样实现是因为和insert接口内部挪动数据的逻辑有关。这里感兴趣建议调试,三言两语说不清楚。
find
接下来实现字串查询的find接口,如下图。
substr
接下来编写substr接口
函数用于返回【调用该函数的string】中的子串,注意字串只是一份拷贝。
上图中给temp对象增加字符数据时不可以直接strcpy,因为光这样修改不了temp对象的_size和_capacity成员。
更加严重的危害是,因为temp对象中的_str只在堆上new了1字节空间用于存储\0,一般来说\0后面存储的数据信息是方便后序delete释放空间时知道该删除多少字节,如果在开辟更大的空间前使用strcpy,那么就会破坏\0后的数据信息,导致delete不知道该删除多少字节的数据,而temp对象是一个局部对象,出了substr的作用域后会先调用析构函数然后释放temp的空间,析构函数内部会delete【】_str,但此时delete不知道该删除多少字节,所以程序就会崩溃。
relational operators,即>、=、<等等
接下来编写relational operators
我们可以复用C库函数strcmp实现这些比较运算符函数。
下图写法中,实现了>和=,其他接口就都可以复用,比如所有的写法2都是复用。 C语言字符串不支持比较大小,因为表示C语言字符串只能通过数组名或者指针,而不管是数组名,还是指针,它们比较大小都是直接按地址去比。
string整体代码展示
#define _CRT_SECURE_NO_WARNINGS
#pragma once
#include<iostream>
#include<cstring>
#include<cassert>
using namespace std;
namespace mine
{
class string
{
public:
string()
:_str(new char[1])
, _size(0)
, _capacity(0)
{
_str[0] = '\0';
}
string(const char* str)
:_str(new char[strlen(str) + 1])
, _size(strlen(str))
, _capacity(_size)
{
strcpy(_str, str);
}
//现代写法
string(const string& str)
:_str(nullptr)
{
string temp(str._str);
//做法1
swap(_str, temp._str);
swap(_size, temp._size);
swap(_capacity, temp._capacity);
//做法2会导致程序崩溃
//swap(*this, temp);
}
//传统写法
/*string(const string& str)
:_str(new char[str._capacity + 1])
, _size(str._size)
, _capacity(str._capacity)
{
strcpy(_str, str._str);
}*/
//现代写法
string& operator=(const string& str)
{
if (this != &str)
{
string temp(str._str);
swap(_str, temp._str);
swap(_size, temp._size);
swap(_capacity, temp._capacity);
}
return *this;
}
//传统写法
/*const string& operator=(const string& str)
{
{
char* c = new char[str._capacity + 1];
memcpy(c, str._str, (strlen(str._str) + 1));
delete[]_str;
_capacity = str._capacity;
_size = str._size;
_str = c;
}
return *this;
}*/
~string()
{
delete[]_str;
_size = 0;
_capacity = 0;
}
const char* c_str()const
{
return _str;
}
size_t size()const
{
return _size;
}
const char& operator[](int pos)const
{
assert(pos >= 0&&pos<_size);
return _str[pos];
}
char& operator[](int pos)
{
assert(pos >= 0 && pos < _size);
return _str[pos];
}
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;
}
void reserve(int n)
{
assert(n >=0);
if (n > _capacity)
{
char* c = new char[n + 1];
strcpy(c, _str);
delete[]_str;
_str = c;
_capacity = n;
}
}
void resize(size_t n,const char&c='\0')
{
if (n > _capacity)
{
reserve(n);
for (size_t i = _size; i < _capacity; i++)
{
_str[i] = c;
}
_str[_capacity] = '\0';
_size = _capacity;
}
else
{
if (n < _size)
{
_size = n;
_str[_size] = '\0';
}
else
{
for (size_t i = _size; i < n; i++)
{
_str[i] = c;
}
_size = n;
_str[_size] = '\0';
}
}
}
void push_back(char c)
{
//做法1
/*if (_capacity == _size)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size] = c;
_size++;
_str[_size] = '\0';*/
//做法2
insert(_size, c);
}
string& operator+=(char c)
{
push_back(c);
return *this;
}
string& operator+=(const char* c)
{
append(c);
return *this;
}
void append(const char* c)
{
//做法1
/*int len = strlen(c);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str+_size, c);
_size += len;*/
//做法2
insert(_size, c);
}
void append(const string& str)
{
append(str._str);
}
void append(int n, char c)
{
assert(n >= 0);
reserve(_size + n);
for (int i = 0; i < n; i++)
{
//做法1
//_str[_size] = c;
//_size++;
//_str[_size] = '\0';
//做法2
push_back(c);
}
}
string& insert(size_t pos, char c)
{
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
//做法1
//size_t end = _size; //这里end直接指向\0,那么第一次循环会把\0往后挪一次,不需要考虑\0问题
//while((int)end >= (int)pos)
//{
// _str[end + 1] = _str[end];
// end--;
//}
//_str[pos] = c;
//_size++;
//return *this;
//做法2
size_t end = _size + 1;//end指向\0的后一个位置
while (end > pos)
{
_str[end] = _str[end - 1];
end--;
}
_str[pos] = c;
_size++;
return *this;
}
string& insert(size_t pos,const char*c)
{
assert(pos <= _size);
assert(c != nullptr);
size_t len = strlen(c);
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size;//end最开始指向\0,这里在下面插入逻辑执行前,已经把\0挪到了对应位置,因此后序插入时无需考虑\0.
while ((int)end >= (int)pos)
{
_str[end + len] = _str[end];
end--;
}
//strcpy(_str + pos, c);
//这里不可以使用strcpy因为它会拷贝\0到目标字符串,
//假如原串为"world",那么strcpy拷贝"hello"头插到原串后,则原串为"hello\0orld\0",
//此时cout以C语言字符串的方式打印原串,则只能打印出hello
//拷贝字符串的做法1
/*for (size_t i = 0; i < len; i++)
{
_str[pos + i] = c[i];
}*/
//拷贝字符串的做法2
//strncpy(_str + pos, c, len);
//拷贝字符串的做法3
memcpy(_str + pos, c, len);
_size += len;
return *this;
}
void erase(size_t pos=0, size_t len=npos)
{
assert(pos < _size);
if (pos + len > _size)
{
_str[pos] = '\0';
_size -= pos;
}
else
{
//做法1
/*size_t cur = pos;
while (cur + len <= _size)
{
_str[cur] = _str[cur + len];
cur++;
}
_size -= len;*/
//做法2
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
void clear()
{
_size = 0;
_str[0] = '\0';
}
size_t find(const char& c, size_t pos = 0)const
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == c)
return i;
}
return npos;
}
size_t find(const char*c,size_t pos=0)const
{
assert(c != nullptr);
assert(pos<_size);
const char*p=strstr(_str+pos,c);
if (p == nullptr)
{
return npos;
}
else
{
return (p - _str);
}
}
string substr(size_t pos=0, size_t len=npos)const
{
assert(pos < _size);
string temp;
if (pos + len > _size)
{
for (size_t i = pos; i < _size; i++)
{
temp += _str[i];
}
}
else
{
for (size_t i = pos; i < pos + len; i++)
{
temp += _str[i];
}
}
return temp;
}
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
{
//写法1
return strcmp(_str, s._str) != 0;
//写法2
//return !((*this) = s);
}
bool operator<(const string& s)const
{
//写法1
return strcmp(_str, s._str) < 0;
//写法2
//return !((*this) > s);
}
bool operator<=(const string& s)const
{
//写法1
return strcmp(_str, s._str) < 0 || strcmp(_str, s._str)==0;
//写法2
//return (*this < s) || (*this = s);
}
bool operator>=(const string& s)const
{
//写法1
return strcmp(_str, s._str) > 0 || strcmp(_str, s._str) == 0;
//写法2
//return !(*this <= s);
}
private:
char* _str;
size_t _size;
size_t _capacity;
const static size_t npos=-1;
};
ostream& operator<<(ostream &out, const string &s)
{
//错误的写法1
//out << s.c_str() << '\n';
//正确的写法2
for (int i=0;i<s.size();i++)
{
out << s[i];
}
return out;
}
istream& operator>>(istream& in, string& s)
{
//低效版本
/*s.clear();
char c;
in.get(c);
while (c != ' ' && c != '\n')
{
s += c;
in.get(c);
}
return in;*/
//高效版本
s.clear();
char ch;
size_t i = 0;
const size_t N = 31;
char c[N];
ch = in.get();
while (ch != ' ' && ch != '\n')
{
if (i == N-1)
{
c[N-1] = '\0';
s += c;
i = 0;
}
c[i] = ch;
i++;
ch = in.get();
}
c[i] = '\0';
s += c;
return in;
}
void test_string()
{
mine::string s("hello world");
s.resize(30, 'c');
cout << s<<endl;
s.resize(12,'a');
cout << s << endl;
s.resize(17, 'a');
cout << s << endl;
cout << endl << endl;
/*for (auto& x : s1)
{
cout << x << ' ';
}*/
}
}