12.任意插入字符/字符串:
void insert(size_t pos,char c)//任意插单字符 { assert(pos <= _size); if (_size == _capacity) { reserve(_capacity == 0 ? 4 : 2 * _capacity); } int end = _size;//现在的_size对应的就是下标,别搞错了 while ((int)pos <= end)//注意,对于双目操作符会存在一个隐式类型转换的问题,即有符号整型会被强转为无符号整型,这样就导致-1会大于0,后续再访问就出错了,故为了让其不错,我们将size_t类型的数据强转为int整型去比较即可 { _str[end + 1] = _str[end]; end--; } _str[pos] = c; _size++; }
类似插入排序的一种思想,任意插入字符最需要关w注的地方是我们的pos是无符号整型,而我们的end是int,在使用操作符号时,有符号整型会被转换为无符号整型,这就导致我们的end到了0之后变成-1依旧符合循环的条件,但是数组是没有-1下标的,这就导致了越界,所以,在这里我采取了将无符号强转为有符号去比较,就解决了这样的问题。
后面的插入字符串如下:
void insert(size_t pos,const char*str)//任意插字符串 { assert(pos <= _size); int len = strlen(str); if (_size + len > _capacity) { reserve(_size + len); } int end = _size; while ((int)pos <= end)//由于pos有等于0的可能性,故我们依旧需要强转,这个别忘了 { _str[end + len] = _str[end]; end--; } strncpy(_str + pos, str, len);//限制长度的拷贝,在后面加上一个要拷贝的个数,注意能使用函数就用函数 _size += len; }
在这里,我们需要处理的问题和插入单个字符大差不差,但是我们在这里使用了strncpy部分拷贝字符串的一部分进入到我们的字符串中
13.任意删除字符串:
void erase(size_t pos, size_t len = npos)//任意删除一段 { assert(pos <= _size); if (len = npos || pos + len >=_size) { _str[pos] = '\0'; _size = pos;//别忘了处理_size,这个容易忽略 } else { int begin = pos + len; while (begin <= _size) { _str[begin -len] = _str[begin];//完全不用控制两个变量,用begin++减去len1,就相当于pos每次++ begin++; } _size -= len; } }
在这里,我们需要注意的问题就是,一旦我们传入的删除的字符串的位数过大,或者我们根本没给字符串的长度,默认使用缺省值npos即无限长,这就导致我们pos位置之后的都要删除,在C C++都是遇到\0判定字符串结束,故我们直接在pos位置给一个\0,然后调整size长度即可,而对于有限的长度,我们只需要利用一个begin去控制数组的前后两个位置赋值即可。
14.从指定位置去寻找字符:
size_t find(char ch,size_t pos=0)//从指定位置查找指定字符 { for (size_t i = pos; i < _size; i++) { if (_str[i] == ch) { return i; } } return npos;//找不到就返回极大值 }
没什么细节需要过多注意,遍历返回即可
15.从指定位置去寻找字符串:
size_t find(const char* str,size_t pos=0 )//从指定位置找指定字符串 { const char* p = strstr(_str+pos, str);//这样就满足从某个位置开始找了 if (p) { return p - _str;//在同一个连续的数据结构内,可以进行指针的相减,得到的是两个指针之间的元素个数,在这里要返回size_t,故我们选择指针相减法 } else { return npos; } }
在这里,我们利用的C语言的strstr函数去寻找对应目标字符串的第一个位置的指针,并且接收,在前面指针的知识中我们知道,在同一个顺序结构中,指针的相减是有意义的,它可以求出两个指针之间的元素的个数,故这里我们让返回的指针和字符串的头指针相减,即可得到从头到我们找到的字符串之间的元素个数,即对应的字符串的下标(正好差一个,即为对应下标)
16.取得一个子字符串:
程序如下:
string substr(size_t pos,size_t len=npos)//取子串 { string s; size_t end = pos + len; if (len == npos ||end >=_size) { len = _size - pos; end = _size; } s.reserve(len); for (size_t i = pos; i < end; i++) { s += _str[i]; } return s;//注意,在这里由于涉及到动态开辟内存的原因,故我们要显式给拷贝构造函数,要不然会出现反复调用析构而多次释放从而报错的问题 }
我们这里要注意的细节就是,和我们删除一段字符串一样,我们仍需要对len的长度一旦超过后续的长度,就需要对我们传入的字符串的长度进行限制,即拷贝允许拷贝的一部分取得,而不是全部的,故首先我们需要先判断,一旦超过我们就要将后续的全部都拷贝给我们构建的s的string类,在这里采取了一个一个字符给的方式,而没有使用strncpy,因为这里不涉及到拷贝,所以赋值即可。
最后说说返回值的问题,由于我们的返回值并不是引用,而是返回一个不存在的string类型,故我们需要拷贝一份临时变量才能返回,同时,由于我们在这里对s进行了动态开辟,故我们的拷贝构造就不能默认构造了,而是要显式去写拷贝构造函数,要不然就会出现多次调用析构多次释放而报错的问题。
17.拷贝构造:(利用现代写法去处理,建议反复琢磨)
我们使用一种采取中间值tmp交换的现代写法来写:
void swap(string& s)//交换数据 { std::swap(_str, s._str); std::swap(_size, s._size); std::swap(_capacity, s._capacity); } string(const string& s)//拷贝构造现代写法,利用tmp交换处理 :_str(nullptr), _size(0), _capacity(0)//由于有个时候内置类型默认的拷贝构造不会处理,故编译器一旦给随机值的话,tmp出了作用域就会被销毁,调用析构函数,此时交换数据tmp的_str是随机的,长度未知,很容易造成无限次的析构递归,直接炸了,故为了保险起见,我们在拷贝构造这里给*this赋初值,保证后续交换时不会出现随机的问题 { string tmp(s._str); swap(tmp); }
在这里,我们利用s._str构建了一个tmp的空间,然后将其与this交换,注意,由于我们的拷贝时我们的this里面的成员变量是随机的,这导致tmp一旦调用析构就会有可能调用无限次的析构从而导致崩溃,所以我们在拷贝构造的前面最好先给我们的this先初始化一下,导致发生这样的错误
18.赋值运算符重载(非常简便的写法,建议反复思考研究)
string& operator=(string tmp)//更加极致的赋值运算符重载现代写法:直接传值传参要拷贝构造,因此在这里tmp就是传入参数的拷贝构造,把tmp直接跟this交换即可,出了作用域tmp直接就销毁了,然后重复上面的写法,非常巧妙,完全利用了类的特点,这种方式要多积累 { swap(tmp); return *this; }
我在这里采取更为精妙的现代写法,在这里我不传引用,而是直接传值,这样,tmp就直接根据s对象调用了拷贝构造,甚至不需要单独写出来,然后让其swap与this交换,返回*this即可。
19.改变字符串的长度:
程序如下:
void resize(size_t n, char ch='\0')//重置字符串的长度并且多余的部分给指定的字符,分三种情况考虑,_size小于等于之前的_size或者比原来大,比原来大又分为扩容或者不扩容,但这个不影响,扩容函数会自己判断 { if (n <= _size) { _str[n] = '\0'; _size = n; } else { reserve(n);//先考虑扩容的问题 while (_size < n)//然后再把指定字符插入进去 { _str[_size] = ch; _size++; } _str[_size] = '\0';//最后别忘了补\0作为字符串的结尾 } }
在这里我们分为两种情况,第一种是我们往小了缩,只需要直接给尾部改成\0即可,这样字符串就会在\0处判定结尾从而达到了缩减长度的效果,倘若是往大了缩,我们首先就需要考虑到扩容的问题,然后从_size位置出发,依次放入我们想要放入的字符,最后别忘了补上\0。
!!!!!我在这里必须要强调的是:字符串的结尾必须以\0结尾,故我们千万别忘了补上\0!!!!!
20.赋值运算符重载>>流输入:(重点!!!有很重要的处理字符串输入的方法以及一种新的输入扩容的思路)
istream& operator>>(istream& in, string& s)//流输入全员函数,这样把第二个参数带到第二个位置上,倘若在类里面io流是抢不过隐含的this指针的 {//流提取默认遇到\0或者空格就停止,且流提取从缓冲区得到字符,故我们要创建一个变量用来提取我们输入的字符并且将其形成循环 s.clear();//直接输入字符串,防止出现尾插的情况 char ch = 0; char extrabook[128+1];//辅助空间:extrabook 别忘了多开一个给\0,利用辅助空间的方法去减少扩容的过程,提高效率,这种方法很好,要反复琢磨理解 size_t i = 0; ch = in.get();//故这种方式是拿不到空格或者换行的,故我们引用一个接收一切in流的函数in.get()来接收包括空格换行在内的一切字符,即可解决这个问题,就像C语言的getchar()一样 while (ch != ' '&& ch != '\n')//注意换行是\n,不是\0,别搞错了,\0是字符串结尾的标志,但是我们本身的打不出来的 { extrabook[i++] = ch; if (i == 128)//当辅助空间蓄满了,就将数据传给s,然后重新给辅助空间蓄数据,直到遇到空格或者\0停止 { extrabook[i] = '\0'; s += extrabook; i = 0; } ch = in.get(); } if (i != 0)//如果i等于0说明上一次就结束了,不用再补\0了 { extrabook[i] = '\0'; s += extrabook; } return in; } //注意,cin和scanf一样,他们都不识别空格和换行符,他们都不会将其存入到变量中,故我们必须找到一个方法让其可以接收空格或者换行符 //在这里考虑一下如何扩容的问题 //对于扩容问题,我们可以采取辅助空间的方法,创建一个辅助的数组一部分一部分向字符串中插入数据,这样扩容也是一部分一部分开辟的,不会频繁的去扩容,甚至扩容一次就可以,极大的节省了效率
如同scanf一样,我们的cin也是无法识别到空格和\n的,故我们利用in.get()将其调整为可以识别空格和\n在内的一切字符,然后按照我们常规的,倘若不是空格和\n就一直输入,保证字符串的完整性,同时,为了防止我们出现的尾插的情况,我们要先清空我们原先的字符串,本质上就是在第一个位置放一个\0,直接让字符串识别到第一个字符是\0就结束。,然后将我们得到的字符一个一个放入我们字符串即可。
但之后我们思考这样一个问题,倘若我们根据情况,输入一个很长的字符串,我们就要反复多次调用扩容函数,这样严重影响了效率,故在这里我们采取了一种更为高效的方法:缓存字符串输入法。
如上面的程序,我们创建了一个extrabook字符串,这一次我们先向这个字符串传入字符,直到这个字符串满了,我们再让s+=extrabook,然后刷新extrabook,重新向里面输入字符,直到我们的输入空格或者\n,此时,如果我们的extrabook依旧有字符直接+=即可,然后这样我们的扩容次数就被极大的减少了,这种方法即为有效,我们应当反复琢磨并且掌握缓存字符串输入法,你可以这样理解,在这里,我们的extrabook如同一个水坝,水一旦涨的过多就将其输送给农田灌溉,然后继续蓄水,反复这个过程,这样极大提高了效率
在这里还是要强调一下我们的in.get()这个很有用,它可以识别所有字符,有点类似getchar(),再处理一些题目时非常有效,最好记下来!!!
我们的string库自身也采取了这种方法,让我们看VS是如何处理的:
程序如下:
int main() { string d1; string d2("hello world"); cout<<sizeof(d1)<<endl; cout << sizeof(d2) << endl; return 0; }
结果如下:
我们发现,不管是空字符串还是传入字符串,初始的对象的大小都是40个字节,除了我们的三个固定的成员占12个字节外,还存在着一个类似我们的extrabook的数组存在,如下:
在这里,我们的allocator便是一个有着28个元素的字符数组,加上12个字节正好是40个字节,为了证明它的作用,哪怕是:
string d2("hello world11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111");
这样去构建一个字符串,对于allocator来说,他依旧是缓存足够的长度,不会改变自己的长度,如下:
故现在我们知道,string类就使用了这种缓存字符串的方式,来减少扩容的次数。(在LINUX甚至更加极端,仅仅4字节,只有一个指针作为成员变量,利用延时拷贝和引用计数来进行处理)
总结:
由此,我们的模拟string类基本就实现了,但还是有很多功能需要我们去探索,我们实现的这些都是我们常用的string库的函数,而且我们模拟实现string类的本质依旧是方便我们去使用string类以及学到一些方法,这是最关键的,要清楚自己的学习目的!!!!
补充知识点:
在实现我们的string类中,我想最让我们头疼的就是深拷贝和浅拷贝的问题,故在这里我们好好的分析一下这个问题:
我们常说的浅拷贝,正如我们前面提到的那样,它的危害在于它可能会析构多次,同时一个改变会导致另一个也发生改变,因为它的指针可能同时指向一个内存空间,但浅拷贝也不是一无是处,对于不涉及指针或者不涉及内存的成员类时,使用浅拷贝的效果更好,故我们这样总结了一下浅拷贝和深拷贝的用法:
!!!!对于数据本身就存在对象里面的,我们适合浅拷贝,当倘若数据不是存在对象中,而是存在由成员的指针变量指向的对象外的空间比如堆区动态开辟的空间时,我们就必须要使用深拷贝!!!!
但是,难道就没有使用浅拷贝同时可以解决多次释放的影响的问题的方法么?
在这里,我们可以采取这样的一种方法:
我们可以使用引用计数法,利用一个count来记录实时有几个对象的指针变量同时指向一块动态开辟的空间,拷贝构造时count++,调用析构时–,这样当计数减到0时,说明执行到这一析构的指针正是最后指向这块空间的指针,此时便可以由这个指针来释放这块空间,这样就不会发生多次释放的问题了,同时使用浅拷贝就可以做到,从始至终都是一块空间。如下:
这种方法确实解决了我们的问题,但是它依旧没法解决我们修改一个数据时另一个数据也同时被修改的问题,就比如在这里,我们对A2指向的字符串,实际上就是在修改A1 A3 A4,这不是我们想看见的,所以同样的思路,我们在这里可以使用延时拷贝的方式。延时拷贝,顾名思义,它并不是无脑的使用拷贝构造,依旧需要我们的size统计指向一块空间的对象的指针个数,倘若是1证明只有一个指针指向这块空间,故我们就可以直接对这块空间进行修改,倘若size>1,说明此时有多个指针指向这一块空间,故这种情况下我们就只能拷贝一份空间在新空间修改并赋给我们对象的指针变量,这样能在一定程度上解决问题,但是倘若我们要修改数据,则必定要拷贝数据,但这也是写的时候拷贝而不是无脑的直接拷贝。
这种方案的意义:如果拷贝了,没有修改数据就极大的提高的效率,但是倘若修改了数据,就只能另外开辟一块空间去去修改而不能在原空间上修改,除非只有一个指针指向一块空间!!!
总结:
以上便是我们string类的全部内容,通过模拟和讲解,我们要掌握的时如何熟练的使用string库,从而为我们做题和运用的时候提供方便,更加快捷的开发程序,同时在模拟的过程中学习一些思路和方法,扩展我们的程序思路,这便是主要的目的,希望大家认真去体会和领悟!!!