目录
目的:
我们知道C++中给我们提供了一个string类进行使用,那么我们为什么还要自己再实现一个string类呢?是为了写一个比编译器给我们提供的更好的吗?这不是重点,虽然我们知道string类在设计上有许多缺陷,比如功能实现太过冗余,有些功能没必要那么设计,C++的其它容器跟string相比就会好很多,这里其实包含着一些历史原因,我就不过多解释,有兴趣的朋友可以网上查询或者私信我都可以。
那么我们自主实习一个string类的目的是为了更好地去理解底层,我们知道C++是一门重视底层的编程语言,我们如果要学好C++这门计算机语言就必须去接触底层,去感受大佬当时写这块功能是有着何种考量,接触了底层之后,在我们日后的使用时,我们就会想到这个功能的底层是如何实现的,从而能够更加合理、高效地去使用它们。
因此,本文我会给大家讲解string大部分的内部成员函数以及一些函数重载,甚至是一些现代方法。
1string类的定义
一般我们的成员函数是设置成public属性,使其能在外部调用到,而内置成员变量我们一般是设置成私有,不允许外部随便就能修改到它。
我们知道string是字符串类,所以要有char*类型的指针,还要有它的有效元素个数_size,还有计录它空间的_capacity,因此初步来看,string类基本上的一个框架就是这样,到后面我们会慢慢完善。
通过这张图我们大致能感受到string的成员函数非常多,在此我推荐一个学习C++的网站cplusplus.com,大家直接网上搜就可以搜到,现在一开始的页面都是新版本的,不过我建议切换回老版本更好用。
框选的地方就可以切换回老版本
这里就可以搜你各种想要了解的东西,容器也好,函数也好都可以搜到,这个网站里面有非常全面的概念知识,方便大家更好地理解。
书接上文,我们知道了它有如此多的功能,那为啥说它冗余呢?
你看,单是一个插入函数,它就重载了一大堆,我觉得当初设计的时候想得太全面了,哪中写法都考虑,都往上面去写,其实没必要,程序员使用的时候也就会用那些常用的写法,所以在后面讲解的时候我就写常见的重载方式。
2.成员函数
2.1构造函数
我们使用构造函数列表的方式更加规范一些,我们已知的框架有三个内置成员变量,我们先计算str的有效个数赋值给_size,再让我们的空间等于我们的_size,再给我们的_str开空间,注意这里要多开一个空间用来存放我们的'\0',再利用strcpy将str的内容拷贝给我们的_str。接下来我来说说我们的形参为什么要这么写,我们在char*的前面加上const修饰,在这里的意思是匹配我们传入的字符串,因为像“hello world”这串字符串是具有常属性的,不用const修饰编译上就不会通过。我们给它赋个空字符串的缺省值是因为你可能什么都不传,不传的话我要想创建一个对象就要来一个空字符串,不然我的_size,_capacity,_str就没法赋值了。
2.2析构函数
析构函数就简单得多了,只需要将_str的空间释放掉,然后给它赋个空指针,再将它的size和capacity赋0就可以了。
2.3c_str
c_str是用来得到字符串的一个函数,我们知道在C++中想要得到字符串可以直接
这是因为<<被重载了,所以可以打印s1的字符串,但是我们要知道C++是兼容C语言的,而我们熟知的C语言没有类的概念,它只有结构体strcut,而且也不支持运算符重载,所以我们的C++标准里string类里面就弄了一个这么个函数来读取字符串,所以它的名字还带着一个c字母呢。
c_str的实现很简单(如图所示)
直接返回_str就可以了,至于为什么要给this指针加一个const修饰,这是为了适配对应的const修饰过后的类的类型,既然权限可以被缩小无法被放大,那么我们直接就将权限设置成最小就可以使两种情况都通过编译。
2.4重载[]
这里的我们就要两种分开来写了,因为c_str是读取一个字符串,不需要修改,我们为了方便,可以写成一个,但是[]不一样,没有const修饰的对象,我们一般还可能对其进行修改的操作,但是如果只写一个第二种,我们是没有办法对其进行修改的,所以这种情况就要分两次讨论。我们直接断言一下pos,pos总不能越界吧,断言完后直接返回这个字符就可以了,返回类型要加引用也是因为只有这样才能修改到对应堆上这个字符的空间。
2.5string迭代器的实现
实现迭代器之前我们先来实现两个小功能,一个是取_size的数据,一个是取_capacity的数据。
然后我们来实现.begin和.end。
我们知道迭代器的实现方式有很多种,这里我们采用指针的方式来进行,我们先重命名一下类型。
我们这里也需要设置成两种,原因跟之前的说法大差不差。
我们begin和end都是需要两种,begin返回的就是字符串的首元素地址,而我们的end返回的是最后一个元素('\0')的地址。
2.6push_back(传统写法)
以上的push_back是传统的写法,现代写法我会在实现insert之后再讲解。push_back的功能是给字符串尾插一个字符,所以我们再尾插前得先检查一下空间是否足够,不够的话我们就需要扩容,至于空间扩大为原来的几倍我们可以自己定,我这里就扩二倍,如果我们的空间本身就是0,也就是空字符串的话,我们直接给一个4大小的空间。扩容的具体操作我们就实现一个reserve来搞定。string类中reserve的作用就是扩容。
扩容的操作我们很好理解,开n+1个空间是为了'\0',strcpy是C语言中的一个字符串拷贝函数,不要忘记我们在将tmp的空间地址给到_str之前要先将它的堆上的空间释放掉,不然会造成空间泄漏。
回到push_back,我们完成扩容的操作之后接下来的操作就很简单了,将字符ch尾插,然后不要忘记将'\0'加回去就行了。
2.7append(传统写法)
append的功能是尾部添加一个字符串。到这里大家就能感觉到string类当时设计的一些不妥,同样是尾部插入它还设置成两个。
它的实现跟push_back相比也是大差不差,先检查一下空间是否足够,不够就扩容,不过这个的扩容有一点讲究,你试想一下,我们这次添加的是一个字符串,字符串的话我们固定的将空间扩大好像都差点意思,所以这里的一个优解就是根据其实际的长度进行扩容。扩完容之后,我们还是可以使用strcpy这个函数来将新的字符串拷贝到原字符串的末尾位置,这里我们没有再手动添加一个'\0'是因为strcpy这个函数会将'\0'也一同进行拷贝。
2.8重载+=运算符
在实际应用当中,我们尾插字符、字符串的操作最常用的就是+=,前面的那两个函数不太会去使用。+=我们也分两种,字符与字符串,欸?这不就跟我们之前那个一样吗!既然如此,我们可以直接将那两个函数搬过来。
2.9insert
insert是任意位置插入,insert函数我们也来实现两种,分别针对字符和字符串,两个代码的整体逻辑都是一样的,我们一个一个来,先看字符的。
这个函数的两个形参代表的意思分别是索引值、字符,字符版的逻辑很简单,首先我们先断言一下pos,它最大也不能比有效元素个数还大吧,断言之后我们来进行扩容,扩容的逻辑还是一样的,因为要移动数据,所以接下来我们使用一个end来充当尾部的下标,然后将元素一个一个往后移就可以了。将pos位置的元素也移走之后我们就可以将ch放到pos位置了。
接下来我们来看看字符串版的。首先是断言,然后扩容,这些操作与之前几乎都相差无几,重点是插入部分,这回end就不再是尾元素的位置了,而是尾元素后len个的位置,这样在将pos位置的元素移走时,刚好逻辑上就空出了len个长度的空间,在将元素移走后我们就可以使用strncpy函数来进行拷贝,同为拷贝函数,strnpy可以限制拷贝长度,这样我们就不会将要拷贝的字符串的'\0'也拷贝进去了。
————————————————————————————————————————
我们已经将insert讲完了,接下来我们来看看push_back和append的现代写法——
实现了insert之后我们就可以直接将insert函数写入对应的功能当中了,这样我们的整个添加操作本质上只用了insert的底层,大大降低了代码的冗余。
2.10erase—删除功能
删除功能也很简单,我们的形参有两个,pos是索引值,len是你想删除的字符串长度,npos是int型的-1,当它进行隐式类型转换的时候,就会变为正整数最大值。
这是我们改进后的私有成员,我们给每个成员变量都设置了一个缺省值,将npos设置为共有是因为我们希望外部也可以访问到它,给它增添常属性使其不能被修改,为了防止每开一个变量就占一块空间,我们再给它设置成静态变量。
讲完npos之后,我们来说说函数内部,先对pos断言,然后判断要删除的字符串长度加上索引值有没有超过字符串的有效元素个数。超过了就直接在索引值对应位置加个'\0',没有的话就将不删除的部分填补空位就行了。
2.11 resize
resize的功能是增容,它的功能与reserve的区别是它既可以往小了缩,也可以往大了缩,而我们的reserve只能往大扩容,往小了没有用。
这个函数我们一开始先判断往那个方向缩,往小的话就直接在n位置放字符ch,使用者没写的话默认为'\0',如果往大的话就先扩容,然后再把字符ch一个一个赋值进去。
2.12 拷贝构造函数
拷贝构造函数我们也有传统版和现代版,现代版我们后面再介绍。
拷贝构造函数也很简单,我们将被拷贝的那个对象传进来,然后就把它的数据一个一个赋值过去就行了。
2.13赋值运算符重载
赋值运算符重载我们还是有传统版和现代版,现代版我们后面再介绍。
赋值运算符重载的思路也很简单,概括起来就是:创建一个临时字符串指针并开空间,然后将被拷贝字符串的值给到临时指针,然后释放掉自己的空间再去=tmp的值。
这样做的好处就是空间消耗小,因为你的tmp是在局部作用域内,出了这个函数就自动销毁了。
2.14swap
std标准库里就有swap,这里我们可以直接拿过来用。
看完swap之后我们来看看拷贝构造函数的现代写法:
本质上就是调用了构造函数:tmp进行构造函数,再用swap去交换双方的值。
赋值运算符重载的现代写法:
这里也是s调用构造和拷贝构造来完成swap交互。
所以,现代写法跟传统写法的区别在于,传统写法是靠自己,而现代写法是别人做好了你选择去使用。
2.15 find查找
find我们也要满足找字符和字符串的,我们先来看看找字符的,找字符的很简单,断言一下就可以直接遍历查找了,找到了就返回下标,找不到就返回npos。
我们再来看看字符串版本的,同样是先断言,然后使用C语言提供的strstr函数,这个函数的功能是匹配字符串,如果找到就返回第一个匹配字符的地址,我们用p来接收后就可以直接利用地址相减返回下标。
2.16 substr
substr是提取字符串的内容,代码的逻辑很简单,要取的内容超过本身,就返回全部,否则返回部分。
3 重载流输入、流提取
重载这两个我们必须写在类的外面,不然的话,由于this指针的存在,我们的重载后的流提取将无法连续执行。
流提取,顾名思义就是获取内容,C++里标准输出流的类型是ostream,所以我们的cout就是ostream类型。提取的操作很简单,用范围for将S里的内容全部提取到cout中,最后将它返回就可以了。
流输入我们也是要定义在类的外面,原因跟流提取是一样的。
这里,我们的标准流输入是从键盘中输入信息到cin中的,cin的类型为istream。因为要输入内容,所以我们的第一步操作是先把字符串里的数据进行清空。清空完后,我们要获取一个个字符,get函数可以做到读取一个个字符,因为流输入我们是不清楚一共有多少个数据,如果一次性扩太大不好,频繁扩容也不好,这个时候就有一个大佬想了一个办法,用buff数组当中间商,临时存储数据,满了就给s,然后将下标变为0,再继续重复操作,如果没满且读完的话,就将值交给s,然后返回就可以了。
这里要注意,我们的流输入是读到空格和回车就会停止的,所以我们这里循环终止的条件就是' '和'\n'。
4.getline
既然我们的标准输入流无法读取我们显示器上的一行内容,那我们怎么办呢?有一个getline函数就可以解决这个问题,getline就无视空格,直到'\n'才会停止,因此,我们的代码都不需要改动,直接将循环条件的空格去除就可以了。