string的模拟实现

文章详细介绍了如何实现一个自定义的C++string类,包括基础框架、拷贝构造和赋值运算符函数、size()和operator[]接口、迭代器、内存管理(reserve和resize)、push_back、operator+=、append、insert、erase、流插入和提取(operator>>和operator<<)以及查找和比较操作。强调了在实现过程中需要注意的内存安全、效率优化和兼容性问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

基础框架

拷贝构造和赋值运算符函数 

size()以及operator【】

string的迭代器类

reserve()

resize

push_back 

operator+= 

append 

insert

erase 

operator>>和operator<<以及clear()

find 

substr 

relational operators,即>、=、<等等

string整体代码展示


基础框架

首先设计出string的框架,如下图。

9b6d13d31fe24500a7217b2b6df116cc.png

一般在源码里,只要一种数据不可能为负数,那么整形的类型就是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呢?

64141f70aa554a8cb62ef919c8df3909.png

答案是不能,这里如果把_str设置成nullptr则会导致自己实现的c_str()接口无法使用,什么意思呢?(下图c_str右边的const表示该成员函数是一个const函数,这样const对象才可以调用这个接口,否则无法调用。注意非const对象也是可以调用const函数的。)

52c8eed6821c44a5bc5c08922cb245d4.png

c_str接口是直接返回string内部的字符指针_str的,_str一般指向我们new出来的位于堆上的空间,空间上就是数据。这里如果_str为nullptr,则会在cout<<打印的时候出错,如下图右边红框处,其实这里已经崩溃了,为什么呢?

9ac421c3ab8e44cea24ba2feae0e933f.png

虽然_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。

6f7cf18c4ee64f36856e89c211139907.png

这里补充一点:其实在实际开发中,上图的构造函数在初始化列表中初始化成员是不太好的,因为它们之间存在依赖关系,什么意思呢?比如上图在初始化列表中我就用了_size的值初始化_capacity,这样就叫做存在依赖关系,注意这样写需要格外注意在声明各成员时的顺序,即使你没有写错,但将来别人可能随便修改一下你声明成员时的结构,代码就会出现问题,所以不推荐这种写法。但我又不想太笨的一直在初始化列表里频繁用strlen计算,该怎么办呢?

正确做法是返璞归真,直接写在函数体内,如下图示例。

1ab0916fa06f426f9c13247162004833.png

拷贝构造和赋值运算符函数 

接下来说一说拷贝构造和赋值运算符函数

我们说如果不自己编写,会生成默认的拷贝构造,此时它会完成对内置类型的值拷贝(浅拷贝),对自定义类型去调用自定义类型的拷贝构造。

对于简单的类比如日期类来说,默认生成的就够用了,但对于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)

64262dffd3bb4b07b88b6a52f9fd7929.png

接下来开始编写拷贝构造和赋值运算符函数

b0b9bcd3565b47478c92d669b430aa91.png

编写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,所以才添加了返回值 

上图中的拷贝构造和赋值运算符函数都是传统写法,它们还有现代写法。

拷贝构造的现代写法如下图。

1e673816e3f448a8970c313e8287f089.png

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整个对象,程序会崩溃,为什么呢?

f66c81e41a5942148780bc6270addcca.png

首先我们看看库中swap的逻辑,如下图红框处。

ee799e6447834dff8b5c99a04c4e2c5e.png

按照上图红框处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计算该开多大的空间。

15261f27efa7454697a3c29283cb14ca.png

而且就算你把strlen的问题解决了,还存在一个无法解决的问题,因为拷贝构造会调用swap,swap内部又会调用拷贝构造,这样会无限的反复调用对方函数导致栈溢出,如下流程图所示。

而如果把s1和s2的成员单独拉出来swap,如下图做法1,则不需要考虑各种可能会导致程序崩溃的小bug。

95fb54a5ee6d44ca9aeaf2deeb101cd0.png

赋值运算符函数的现代写法如下图。

76014bdd781b4feea533d3fffad5e9ab.png

和拷贝构造同理,这里也不可以直接swap整个string对象,比如swap(*this,temp)就会导致栈溢出,因为swap内部会调用operator=,就是说,我调operator=就会调用swap,而调用swap又会调用operator=,这也会无限的反复调用对方导致栈溢出,如下流程图所示。

所以综上所述,拷贝构造或者赋值运算符函数会深拷贝的类,使用swap交换它们的类对象时一定要小心。这里建议库中的swap最好只用于交换内置类型的对象,不然容易出现各种莫名其妙的问题。

size()以及operator【】

 接下来编写下图的三个接口。

79b5256fa41d45a1bfc2d98b30b01316.png

将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实现的迭代器类实际上就是原生指针,如下图。

fb56d92af2ad498ba3e75c782cf07b75.png

和上文中的operator【】()同理,这里的begin或者end也都构成函数重载,因为this指针的类型不同。进行重载的理由也相同,为了让const string对象调用const版本的begin或者end,这样在类外别人就无法利用解引用迭代器对_str的数据进行修改;让非const对象调用非const版的begin或者end,这样在类外别人就可以通过解引用迭代器对_str的数据进行修改。

额外一提,这里我们实现了迭代器类,因为范围for底层就是迭代器,所以是可以支持范围for的。

b80ed8b0ca174a97b61d26dd9f870893.png

接下来开始编写增删查改的接口

reserve()

涉及到增加数据,则一定会有扩容的问题,所以首先编写扩容的接口。下图红框处+1是为了给\0预留一块空间,因为我们_capacity表示可存储有效字符的总量。

b4f6cfab7de245f7928902e27b556db3.png

resize

push_back 

有了reserve可以扩容后,我们选择先编写push_back接口,push_back一次只尾插一个元素。

d623c1368d7a40ffb03b36a36880a350.png

在插入字符后,一定要记得在插入字符的后面添加\0,如上图红框处,否则在cout以C语言字符串打印string对象时,就会因为读取不到\0导致一直读取,导致打印乱码,直到恰好内存中有一个随机值刚好为\0才会停止打印,如下图打印出乱码就是因为push_back中少写了上图红框处代码。

aba9d2903ab4433d9ede919a0f368e86.png

operator+= 

有了push_back,我们可以复用它实现operator+=()函数,如下图。

注意是可以连续+=的,比如有string s1,s2,s3,是可以s1+=s2+=s3的。

768c62839317417f9c2ced0fbe22bd38.png

append 

接下来我们实现append接口,append和push_back不同,它一次可以插入多个元素,注意append只支持尾插,不支持指定位置插入。

de7df147c15544f9a9156697a525f412.png

append插入时如果需要扩容,那么这里扩容的时候不能简单的认为扩容2倍或者扩容一个固定的大小就可以了,因为追加的C语言字符串的长度是未知的,扩容2倍或者扩容一个固定值都是可能不够的,所以这里我们采用直接计算C语言字符串的长度,然后把该长度和string中_str的长度相加,然后扩容到相加的这个值。

注意strcpy在拷贝字符串时,是会把\0也拷贝过去的,所以这里最后无需_str【_size】= '\0',所以上图代码中,_size+=len后,_size已经指向\0了。

这里拷贝C语言字符串时不推荐使用strcat,表示在原字符串后追加字符串,该函数是一个失败的设计,因为每次追加都得在原字符串中寻找\0,找到后再从\0开始追加,该函数最后也会把\0拷贝过去。如果非要使用strcat,可以算出原字符串\0的位置,避免内部逻辑去从头寻找,提高效率。

23bb5f832bd24c5c89b8673c7069c45d.png

append对string类的重载如上图,只需要复用append对C语言字符串重载函数即可。

d062ce76ceb24238b33965d295cf1d93.png

对于上图接口,值得一提的是,在for循环内部连续push_back时,有可能因为n过大,导致频繁扩容,这样效率就太低了,所以在for循环外面,在插入字符前,可以首先扩容_size+n个空间,这样在插入时则无需考虑扩容的问题了。

insert

接下来编写insert接口。该功能支持在指定的下标pos插入字符,所以得把pos以及pos后面的数据依次往后挪动一次。

a541dee58c9d43d68f425b0bea1c827c.png

_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下标对应的位置。 

9d9fa9b2c1da4059a6854f59f7b410c1.png

 接下来编写insert插入字符串的版本。

f490c2df8b464260aa1ef79f99b52d7e.png

和insert插入字符的版本同理,这里不想在while判断时强转也有其他的解决方案。

如上图注释所说,这里拷贝字符串时不可以用strcpy,但可以使用strncpy或者memcpy,因为后两个接口都可以指定拷贝的字节数,这样可以避免拷贝\0到原串。

在面试中,如果时间紧急,可以先实现insert接口,其他所有和插入有关的接口都可以复用insert接口,如下图所示。

a39587889c4d4ff095d95c7ee6a3be76.png

 dd8163c5e6f34ffa8d848371ebb2ccc6.png

erase 

接下来编写erase接口。 

注意string或者其他容器的任何删除接口,比如erase()或者clear()等,它们都不会把物理空间释放,而只是处理数据,不让你访问到它们,并修改_size,这里_capacity一般是不会修改的。只有容器对象调用析构函数时,它们占用的物理空间才会被释放。

首先定义一个静态变量npos,方法有如下图两种。下图中有点问题,npos应该是公有数据,因为库中的npos在类外也能调用。

cb5c18b828a74a58a668043a4526dffe.png

然后在删除的时候需要考虑两种情况,第一种是从pos位置开始,把往后所有字符全部删除,对应下图中上方红色箭头部分,第二种是从pos位置开始,把往后部分字符删除,删除len个字符,对应下图下方红色箭头部分。

ddf3c49648bf46fabe050dabacfc3e8c.png

下图是代码的放大版,做法1是自己挪动,逻辑为:让下标cur从pos处开始,将cur下标对应的值修改成cur+len下标对应的值,每次循环后cur的值都要+1,一直循环到(cur+len)下标对应的值为\0,最后一次循环cur下标对应的值要修改成\0。

做法2是靠库函数挪动。注意两个做法都对\0做了处理的,做法1中,_size下标对应的位置就是\0,while循环时条件是<=_size,所以最后把\0也是挪动了的,做法2的strcpy也是会拷贝\0到目标串的。

c2fa849979534aecb154fb9b4e9ea63d.png

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的特性了。

341b9ba0e5dd4efa99c1a2f97b07ffd6.png

b89a5063da8d46738d300d26ff963616.png

分割线---------

cd3a0f80de6b443e899eafd4f3a7f7b2.png

可以如上图把流提取设置为成员函数吗?

答案:流提取或者流插入最好不要写在类内,因为成员函数的第一个参数是隐含的this指针,如果如上图把流提取设置为成员函数,则在类外调用时格式如下图演示。可以发现明显和正常调用的规则不同,我们不要打破规则,所以放弃这种写在类内的方案。

b6c07966145849efaa9d84e3a48e33e0.png

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是支持连续输出不同类型的数据的。

61885b61418b47779d0a7007b83af623.png

最后咱们可以总结出一点,既然operator<<既可以在类内实现成成员函数,也可以在类外实现成全局函数,那其他带operator的运算符函数,如operator+ 或者operator<=等等运算符函数也应该支持既可以实现在类内,也可以实现在类外。

然后进而咱们能体会出,好像operator系列的函数和正常的函数没有什么区别,正常函数的名字是字符构成,operator系列函数的名字也是字符构成,只不过带了运算字符如+;正常函数可以实现成成员函数,也可以实现成全局函数,operator系列函数也如此。

所以operator系列的函数和正常的函数相比,唯一的区别在于调用的格式,正常函数是通过函数名加参数列表调用,如func(x,y),而operator系列函数,比如operator+是通过如x+y调用,参数位于函数名中的运算符两侧,虽然最后也会被编译器转换成operator+(x,y)调用,并且我们如果不嫌麻烦,也可以直接写成operator+(x,y)去调用该函数,免得编译器去转换。

接下来完成流插入函数operator>>的编写。

下图示例是一种错误的写法,虽然能对string s进行输入,但无法结束输入,为什么呢?
0b0a81003cf744c491f338511ac49294.png

答案:从键盘输入数据到内核缓冲区后,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是支持连续输入不同类型的数据的。

31cadab1492249e7b0aa1b42d097b287.png

上图代码的效率其实是非常低效的,因为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 << ' ';
		}*/
	}


}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值