本期我们来学习C++的string,本期内容相当的多,且有一定难度,需要大家静下心来看
目录
1.标准库中的string
1.1string类的介绍
1.2 string类的常用接口
构造函数、析构函数、赋值、拷贝构造
npos
push_back
append
operator[ ]
size
迭代器
reverse
sort
反向迭代器
const迭代器
max_size
capacity
reserve
resize
shrink_to_fit
at
insert
erase
replace
c_str
find
substr
rfind
find_first_of和 find_last_of
运算符重载
getline
to_string
模拟实现string
基本框架
增删查改
引用计数和写时拷贝
全部代码
我们日常生活中有很多信息只能用字符串表示,比如名字,住址,个人信息等等,我们之前在C语言学过str的各种函数,比如strlen,strcpy等等,但是这些函数不能满足我们的需求,所以C++以类的角度写了string,不仅可以完成C的功能,还能完成增删查改,以及各种算法,下面我们来对string进行学习
1.标准库中的string
1.1string类的介绍
1. 字符串是表示字符序列的类2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。3. string 类是使用 char( 即作为它的字符类型,使用它的默认 char_traits 和分配器类型 ( 关于模板的更多信息,请参阅basic_string) 。4. string 类是 basic_string 模板类的一个实例,它使用 char 来实例化 basic_string 模板类,并用 char_traits和allocator 作为 basic_string 的默认参数 ( 根于更多的模板信息请参考 basic_string) 。5. 注意,这个类独立于所使用的编码来处理字节 : 如果用来处理多字节或变长字符 ( 如 UTF-8) 的序列,这个类的所有成员( 如长度或大小 ) 以及它的迭代器,将仍然按照字节 ( 而不是实际编码的字符 ) 来操作。总结:1. string 是表示字符串的字符串类2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作 string 的常规操作。3. string 在底层实际是: basic_string 模板类的别名, typedef basic_string<char, char_traits, allocator>string;4. 不能操作多字节或者变长字符的序列。在 使用 string 类时,必须包含 #include 头文件以及 using namespace std ;
我们来用一下string
1.2 string类的常用接口
string的成员函数非常多,有一百多个
我们来随便看一看,首先是构造函数
构造函数、析构函数、赋值、拷贝构造
构造函数实现了7个
上面是析构函数
还有赋值
这么多的函数我们只需掌握常用的,大概二十多个,其余的我们在需要是查一下即可
我们先来学习构造函数
这里就是无参构造和有参的构造
还可以以一个数字,一个字符,来完成多个字符的初始化,这样我们可以省点力气
还有拷贝构造(s4)
我们再简单了解一下这三个
3的功能是从pos位置开始拷贝len长度
比如这里我们就是从下标为6的位置开始拷贝5个字符
我们在C语言时比较字符串要用strcmp,不过现在就不需要了
string类实现了重载,这里比较s1和s2我们要加上括号,原因是运算符的优先级问题
我们继续看3,3里面有个缺省参数,len = npos
npos
npos是一个静态成员变量,值为-1,因为是size_t类型,所以是整形的最大值
这意味着我们如果不给第三个参数它会拷贝到结尾的位置
如果len太长,超过字符串长度,或者len是npos,直接取到结尾
我们可以靠这个操作分割字符串,这里是人工数的,后续我们会加上别的操作,就不用我们去数了
因为npos是一个非常大的数,所以我们在取结尾时非常方便,如果没有npos,我们写起来就很麻烦了
.size()可以计算长度,再减去28就是剩余多少
我们再看5的功能 ,是拷贝s字符的前n个进行初始化,第7个涉及迭代器,我们暂时不介绍
我们再看看赋值
可以支持多种写法,我们看到第三种方法使用的是单引号,这个还被吐槽过,没有必要
push_back
push_back可以在string后添加一个字符
比如我们在hello后添加一个空格,那如果还要添加一个world的话,我们也要一个一个写吗?答案是不用
append
我们可以使用append
非常的方便,而且不像C语言那会,string这里会自动扩容
这还不是最牛的地方,我们看下面
我们可以直接使用+=,非常的舒服,体验直接上升一个档次
这里有一个x,如果我们要把x转成string对象,该怎么办呢?
size_t x = 0;
cin >> x;
string xstr;
while (x) {
size_t val = x % 10;
xstr += ('0'+val);
x /= 10;
}
cout << xstr << endl;
我们来看结果
其实还是有点问题的,我们逆置一下即可(一会儿会讲一些方便的函数)
string是一个管理字符数组的类(其实就是字符数组的顺序表)
下面我们来看一个比较重要的重载
operator[ ]
重载的是方括号,方括号本质是一种解引用,让数组可以访问它的数据
现在我们就可以访问它的每一个字符了,用下标加方括号
我们可以看到,两种方式访问的结果是一样的,只是第一种是直接打印,第二种是一个一个的打印,第二种相对更加自由
上面我们用到了size,我们来看看size
size
大家再想一想,我们知道字符串的结尾是\0,那我们上面可以访问到\0吗?
我们打印一下size,发生并没有统计\0,原因是\0不算有效字符,是特殊字符,是标识字符串结束的特殊字符,这里和C语言的strlen一样,都没有算\0
有人可能回想在size后+1是不是可以访问到\0? 我们看结果发现好像什么也没有变化
有些编译器下,\0是不会被显示的打印出来,其实这里已经访问到\0了,只是没有显示出来
我们打开监视也是看不到的
但是在这里是可以看到的 ,我们要点开各种底层
这里就可以看到了,这块内容我们未来学习底层会进行讲解
我们再回过头来看operator[ ],它实现了两个,一个是普通对象的,一个是const对象的
我们可以给每个字符都修改一下
我们可以像数组一样使用,增删查改,非常方便
这里看起来很像,其实底层是天差地别的
a是数组名,代表首元素地址,s3[1]会变为*(s3+1)
而s1是自定义类型,会调用operator[ ](1);
我们可以看到call是调用了operator[ ] 的,前面有一个很长的东西,这是string的类型,我们后续会讲这是为什么
了解了上面的内容,我们下面开始学习迭代器
迭代器
我们现在可以把迭代器理解为像指针一样的东西
比如说我们有一个string对象,它大概就是这样的结构,在堆上开了空间,然后指针指向这个空间,然后把常量字符串拷贝过来
迭代器就是增加一种访问方式,我们先来看迭代器怎么使用
我们先记住迭代器怎么写,至于为什么这样写,我们后面会讲
s1.begin会返回hello world的开头位置的指针(不一定是指针,我们先想象为指针)
end会返回\0的位置(左闭右开的区间),*it开始是h的位置,所以++it会让它往后走,下一次就是e,然后就是llo
迭代器是像指针一样的类型,有可能是指针,也有可能不是,我们后续会慢慢接触
任何容器,begin都是第一个位置的迭代器
那迭代器能否修改呢?答案是可以的
我们先解引用,再--,就修改了数据
我们的访问方式多种多样,但是我们之前学习过范围for,其实范围for才是我们的最爱
我们再回忆一下,我们这里进行了++,但是发现输出结果没有改变,这是因为这里是依次取字符拷贝给ch,然后++,本体并没有变化,所以我们应该加引用
加上引用就变化了
其实,范围for的底层会替换为迭代器,也就是说根本就没有什么范围for,都是迭代器罢了
我们看底层,其实也是begin和end
如果一个类不支持迭代器,就不支持范围for,比如栈就不支持
迭代器的另一个好处是通用,如何容器都支持迭代器,并且用法是类似的
比如我们后面要学的vector,我们发现,代码基本是一样的
list也同理,也就是说,我们学一个迭代器,其他就基本都会了
这里还有一个问题,很多人可能会以为下标加方括号是主流的访问方式,但是string可以用,vector可以用,那list呢?
答案是不能,因为只有连续的空间(数组结构)才能重载方括号,比如树也是不行的,但是他们都是可以用迭代器的
总结一下:iterator是像指针一样的类型,有可能是指针,有可能不是指针,iterator提供了一种统一的方式访问和修改容器的数据
下面我们来简单看看算法,这里我们讲算法的主要目的是为了演示迭代器
首先,算法是怎么作用的数据上的呢?数据在容器里,算法是不能直接访问到容器的,因为容器里的数据是私有的
我们先简单看看
reverse
reverse是逆置的意思,是通用的,无论列表,数组都可以使用,传的参数是左闭右开的区间
这里的两句代码调用的也不是一个函数,因为我们看上面的reverse,是函数模板
有了迭代器,我们直接使用范围for,非常的舒服
sort
还有sort,传的也是迭代器
另外,这里的列表是不支持sort的,具体原因我们之后再谈
上面我们讲了迭代器的第二个作用,跟算法进行配合
所以算法就可以通过迭代器出处理容器中的数据
迭代器除了普通的迭代器,还有反向的迭代器,比如有时候我们需要倒着遍历
反向迭代器
反向迭代器是reverse_iterator,我们来使用看看
反向迭代器调用的自然也是rbegin和rend,我们发现确实反向了
rbegin是最后一个数据的位置,rend是第一个数据的前一个位置
我们这里调用反向迭代器,非常的麻烦,前面写那么长,还要写string,reverse,所以我们可以用auto
使用起来非常方便,我们想修改数据也是可以的
这里就进行了修改
仔细想想我们就可以发现,范围for是有局限性的,它是不可以倒着遍历的,只有反向迭代器才能倒着遍历
我们再看一些使用场景
我们这里传s1,其实是不愿意这样传的,因为这里会调用拷贝构造,这里是深拷贝,代价极大
所以我们要加const引用
我们在里面写点代码
我们看,这里编译是不通过的
原因是const对象不能用普通迭代器,要使用const迭代器
const迭代器
普通迭代器可以读可以写,const只能读不能写
这样就可以通过了 ,如果要倒着遍历,也是类似的
原因也是一样,需要使用const的迭代器
这样就可以了,但是,这样写太长了,直接用auto就可以了,也就是我屏蔽掉的那一行auto
总结一下,我们现在学习了四种迭代器
前两种是正向迭代器,后两种是反向迭代器,1和3可以读可写,2和4是只读(给const对象使用)
下面我们再看看容量相关的
我们看到有size和length,发现他们的解释是一样的
运行结果也是相同,这其实是和C++的发展史有关
containers是容器的意思,但我们发现里面其实并没有string
严格来说,string不属于stl,是属于标准库的,stl是属于标准库的一部分,string比stl产生的早一点
上面的接口,string上面定义的是length,在stl出来后加了size,因为列表这些都可以叫length,但是树叫length是不合适的,所以就有了size
我们一般推荐使用size
max_size
我们来看max_size,意思是最大长度
最大长度是21亿,但是我们换个编译器就可能有变化
比如vs的13下就是42亿,这个函数有点不靠谱
stl是一个规范,有很多版本,底层实现大同小异,所以不同版本的底层都是有差异的
比如这里对于max_size就是不同的,max_size在实际中毫无意义,但因为向前兼容的问题,是不能删除的
capacity
我们再看capacity
是15,我们再换一个编译器,换到Linux下
结果是不一样的,我们再看扩容机制
除了第一次是二倍,后续大概都是1.5倍,我们再看看Linux下的
Linux是二倍扩容
vs下最开始的长度其实是16(还有一个\0),第一次扩容后其实是32,上面的没有算 \0
clear是清理数据的意思
大家先想一想clear可能会让size改变,那会不会让capacity改变呢?
答案是不会,同样的,我们来看下Linux下
也是一样的 ,空间是很多的,一般情况下是不会轻易释放空间的,因为最后是有析构函数做保底的
下面我们做些练习题
这道题是大数相加,有些数太大,我们的int,甚至long long都保存不下,比如99个9,这时就有人想到把数据保存到字符串里,所以就有了这种题,我们下面来看代码
class Solution {
public:
string addStrings(string num1, string num2) {
int end1=num1.size()-1,end2=num2.size()-1;
string strRet;
int carry=0;
while(end1>=0 || end2 >= 0){
int val1 = end1 >= 0 ? num1[end1]-'0' : 0;
int val2 = end2 >= 0 ? num2[end2]-'0' : 0;
int ret = val1 + val2 + carry;
carry = ret/10;
ret = ret%10;
strRet += ('0'+ret);
--end1;
--end2;
}
if(carry==1){
strRet += '1';
}
reverse(strRet.begin(),strRet.end());
return strRet;
}
};
我们设置end1和end2取两个字符串的尾部,val1和val2负责保存并转换为数字,strRet是我们最后返回的字符串,ret是我们临时保存val1+val2+carry的值,carry是进位的意思,比如9+9=18,这里carry就是1,然后让strRet保存8即可,因为是字符和整形转换,所以我们要注意加减字符0,最后还要判断一下,如果carry为1时,strRet最后还是要加1的, 因为我们是从最后一位开始保存的,所以最后需要逆置一下
我们回过头继续看capacity
我们知道,扩容是要付出代价的,那我们有什么办法可以减少代价呢?
reserve
所以就有了reserve,他的英语单词和reverse非常像,但reserve是保留的意思
我们看他的参数,有一个n,假设我们知道我们的string要插入100个字符,我们就可以
就不需要我们后续开空间了,已经提取开好了,至于这里为什么是111,这是和vs的对齐机制有关的,另外,如果在Linux下的话,这里给的就是100,所以不同的平台走的是不同的,不管在哪一个平台,这里一定会大于等于100
我们仔细看reserve的文档,他说在一些情况是会缩容的,我们来验证一下
在某些情况下,他确实是会缩容的,比如我们先开100个空间,然后clear,再reverse,就缩容了,和clear也有点关系,这里是否用for循环填满空间不影响结果
如果有数据,就不会缩容,clear清空一下,就会缩容了
resize
我们再看resize,resize是扩容加填数据
reserve只对capacity进行改变,是单纯的开空间,resize是开空间加填值初始化
默认填的是0
当然我们想自己设置也是可以的
resize开空间时,如果n比我们的空间大,他会扩容,如果比空间小的话,他是会删数据的
我们看到size从100变成了20,相当于把后面的数据全删了
另外,这里是不会缩容的,resize(0)也不会缩容,缩容是要付出代价的,所以一般情况都不会缩容
如果我们就想要缩容,我们可以
shrink_to_fit
他会将capacity减少到size,可能比size大,因为有对齐等等的原因
operator[ ]可以让我们把string像数组一样使用,返回pos位置的字符,我们可以随意修改,但是const不能修改,这两个接口构成重载
at
at的更能和[ ]是一样的,但我们平时不喜欢用,at是早年没有运算符重载时提供的,我们了解一下即可
at和[ ]的区别是越界的检查,at是抛异常,[ ]是断言
这里[ ]就是断言错误
at是异常,异常需要我们捕获
append是字符串,push_back是一个字符,不过我们平时基本都使用+=
我们再看assign,是赋值的意思,我们来对比一下
实际中我们也基本不用assign,大家了解即可
insert
我们再看insert,他是在pos位置插入一个字符串,或者字符串的前n个
方式多种多样
还可以是迭代器 ,如果我们也想用迭代器在中间位置插入,我们可以
insert我们要谨慎对待,因为有数据挪动,不宜多用
erase
我们再看erase,是从pos位置删除n个字符
比如这里我们就把world删除了
如果我们只给其实位置,他就是一直删,直到结尾,如果我们连这个5也不写,就是全部删除
给迭代器也是可以的,这里就变成了头删
同样因为数据挪动的原因,不宜多用
replace
再看replace ,是替换的意思,上面的(1)是把pos位置开始的len个字符替换为string
我们简单看看
大致就是这样一个样子,同样的,还是因为数据挪动问题,不宜多用
c_str
c_str是返回底层的存储的字符串
第一个s1是调用string的流插入,下面的是char*
c_str是为了和c语言的一些接口更好的配合
比如我们打开一个文件,我们是不能直接传filename的,但是有了c_str就可以这样写
find
find就是查找,字符串和字符都可以查找,这里的pos从pos位置开始进行搜索,会返回找到的第一个位置,没有找到返回-1
我们来用find做个练习,随便给定一个网址,将协议,域名,资源名分开
substr
这里我们结合substr, 这是截取子串,pos位置开始截取len个
int main() {
string url = "https://legacy.cplusplus.com/reference/string/string/";
//协议,域名,资源名
size_t pos1 = url.find("://");
string protocol;
if (pos1 != string::npos) {
protocol = url.substr(0, pos1);//当前位置的下标是前面的数据个数
}
cout << protocol << endl;
string domain;
size_t pos2 =url.find('/',pos1+3);//pos1+3是查找的其实位置,防止返回://的/,我们要的是com后面的/
if (pos2 != string::npos) {
domain = url.substr(pos1+3,pos2-(pos1+3));
}
cout << domain << endl;
string uri;
uri = url.substr(pos2 + 1);//只给起始位置,会一直截取到结尾
cout << uri << endl;
}
此时我们随便换网站都是可以的,我们之前写的是写死的
rfind
除了find,有时候我们还需要rfind,比如查找最后一个单词的长度或者查找文件的后缀
rfind和find完全是类似的,只不过rfind是从后往前找
find_first_of和 find_last_of
再看find_first_of还有find_last_of,下面为了方便,简称firstof和lastof
firstof是找任意一个,我们来试一试
这段代码就是把所有的aeiou替换为*
lastof和firstof的功能是一样的,只不过是从后往前找
还有find_first_not_of和 find_last_not_of,他们的是找不是这里面的,比如我们上面的aeiou,他查找的为不是aeiou的,和上面两个是反正来的,同样的,first是从前向后,last是从后向前
还有运算符重载,大家了解即可
运算符重载
+和+=,+是不改变自己 ,+=是改变自己
getline
然后就是getline
我们借助这道题来讲解
这里的代码非常简单,我们直接来看这个问题,这是什么原因?我们在vs下测试一下大家就明白了
原因是空格和空格之后的字符都没有被录入,scanf和cin都有一个特点,多个值之间遇到空格或者换行,认为这次读取结束,剩下都在缓冲区
我们读取两次就可以验证
所以此时就需要getline ,我们看getline的参数,第一个是istream,第二个是字符串,第三个是结束符,我们可以自己控制在哪里结束,默认是换行符
大家还记得C语言中字符串和整形间的转换吗?还有字符串和浮点数之间的转换
整形是itoa和atoi函数,浮点数没有,要自己写,我们来看C++是怎么玩的
to_string
我们直接来试一试(C++11支持)
非常强大,我们再看字符串转整形怎么实现
默认是按10进制转,这个我们是可以控制的
还有转浮点数的
所以我们使用这些函数会非常方便
最后我们再看string,string是模板,是typedef的,他的本源是basic_string
为什么要写成模板呢?
因为还有wstring,u16和u32,要兼容这些版本,这些东西和编码有关,目前我们先了解一下即可
我们接触过的编码就有ASCII码,ASCII是美国的,所以不能表示汉字,于是我们就自己设计了gbk编码,还有一个就是国际上的unicode,叫做万国码,他要将全世界的文字收录,所以上面的这几个都是为了更好的表示其他国家的文字而设计的
在实际中,我们使用较多的是utf-8,他是一个变长的,可以兼容ASCII等等,大家感兴趣可以了解一下
模拟实现string
下面我们来模拟实现string
基本框架
我们先把基本框架写下
我们先看构造函数,我们是不能这样初始化的,因为上面是const char*,下面是char*,涉及权限放大问题,另外,万一传入的是一个常量字符串,这里都不能修改了,所以string的构造函数不能用字符串去初始化,要去开空间,我们尽可能使用初始化列表初始化
最后我们写成这样,有人可能会把初始化列表里的顺序换一下
这样看起来没有毛病,但是别忘了,初始化列表的初始的顺序不是按照我们写的顺序,而是声明的顺序,即按找private里属性的顺序来初始化,所以这里如果我们运行的话是会出现错误的
我们测试一下,为什么预期结果和我们想的不一样,没有字符?因为我们上面只开了空间,而没有把数据拷贝过去
此时我们再测试就没问题了,所以大家一定要注意细节
我们的代码还有一点问题,strlen是一个o(N)的接口,是实时去算的,我们初始化用了三遍是非常费时间的 ,还有为了解决乱移动代码顺序问题,我们修改一下
下面我们再实现一下析构函数和c_str,实现c_str后我们就不用再一直看监视窗口了,方便一点
这两个实现非常简单
我们简单测试一下
我们再按照实际需求,假设我们需要一个无参的构造函数,所以我们要实现一下默认构造
先看这个,这样写是错误的,_str这里不能给nullptr,否则我们的c_str就悬空了,会崩掉
如果我们使用标准库的sting,定义一个无参的string,他的里面只有一个\0
所以我们应该这样写,我们合并一下两个构造,无参的带参的使用全全省的即可
我们先看这两种错误的,这两种错误是新手们经常犯的,第一个错误的原因是\0是char,而str是char*,类型都不匹配 ,第二种如果给空的话,下面的strlen就直接崩了
其实我们什么都不用给就可以了
我们看到这里的空串其实是有\0的,这个\0是哪来的呢?其实是常量字符串末尾默认是有\0的
然后strcpy就把\0拷贝过去了
如果我们要遍历字符串,是需要知道size的
我们直接返回即可,这个函数和c_str我们最好在后边加上const ,原因是const可以被const对象调用,是权限的平移,他修饰的是this指针指向的对象,普通对象也可以调用,是权限的缩小
下面我们再实现一下operator[ ],这样我们就可以像数组一样去使用string
我们使用assert,返回pos位置的字符,并且这里可以返回引用,因为出了作用域_str还在,他是对象的成员,用引用返回我们可以读,也可以写
而且我们要提供两个版本,这里我们要多看文档
比如只有const版本,因为只用读,不用写
这个就有两个版本,第一个是可以读可以写,第二个个只读,当是const对象时,期望不能修改,所以只读,构成函数重载
我们简单测试一下,这里就可读可写
而当是const时就不能修改 ,编译器会自动寻找最匹配的
我们再来实现一下迭代器
我们知道迭代器有begin和end,begin是开始的位置,end是最后一个位置的下一个位置(\0不是有效字符)
就是这个样子
我们简单实现一下
我们还可以用范围for
写了迭代器就支持范围for,在底层范围for就被替换成了迭代器
我们可以证明一下,我们把迭代器的end屏蔽掉
然后就会出现这样的错误 ,编译器的底层就是傻瓜式的替换而已,名字都需要是一样的,我们改一下名字都不行
那const对象调用迭代器呢?const对象只能读不能写
const char*即可
我们简单测试一下,*cit+=1是不允许的 ,const迭代器是只读的
这一串写起来太长了,我们是可以 使用auto的
增删查改
下面我们来实现push_back和append
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
//2倍扩容
reserve(_capacity == 0 ? 4 : _capacity * 2);//这里的判断是防止空串初始化容量为0
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
//至少扩容到_size + len
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
我们添加时都是需要先判断是否需要扩容,所以我们写一个reserve,逻辑也很简单,下面我们测试一下
是没有问题的,我们在实际中其实最喜欢的是+=这些,所以我们来实现一下
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
我们基于前面的代码实现即可,然后我们测试一下
也没有问题
我们再实现一下insert
void insert(size_t pos, size_t n, char ch)
{
assert(pos <= _size);
if (_size + n > _capacity)
{
//至少扩容到_size + n
reserve(_size + n);
}
//挪动数据
int end = _size;
while (end >= (int)pos)
{
_str[end + n] = _str[end];
--end;
}
for (size_t i = 0; i < n; i++)
{
_str[pos + i] = ch;
}
_size += n;
}
我们先看pos位置插入n个字符的版本,这里挪动数据end设置为int,下面pos强转int,原因是pos如果为0,size_t类型是不会为负数,也就是说while循环是死循环了,我们只把end写成int也不行,因为pos不强转为int的话,在比较时就会发生整形提升,把end转换为无符号的,这是一个解决方案
第二种方法是我们挪动的时候直接把end设置为结尾位置,然后挪动,这样就不会有0的问题,不过这样写出来不太好看,我们来看第三种
我们给定一个npos
然后我们在挪动数据这里加一个条件即可
我们测试一下,没有问题
void insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
//至少扩容到_size + len
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;
}
}
我们再实现erase,也是非常简单
测试一下也没有问题
增删查改我们还差一个查,我们来实现find
find查找字符串我们使用暴力匹配,为什么不用kmp呢?因为实际中kmp其实并不好,甚至strstr这个函数都是暴力匹配写的,一般不要求效率的话都使用暴力匹配,如果大家想换一种写法的话可以去了解一下BM算法
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;
}
return npos;
}
我们借助strstr就可以轻松实现find,我们再测试一下
没有问题
我们再写一个substr,截取子串,而且我们还要写拷贝构造
string(const string& s)//拷贝构造
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
我们要开一个一样大的空间来完成深拷贝
string substr(size_t pos = 0, size_t len = npos)
{
assert(pos < _size);
size_t n = len;
if (len == npos || pos + len > _size)//长度超过size
{
n = _size - pos;
}
string tmp;
tmp.reserve(n);
for (size_t i = pos; i < pos + n; i++)
{
tmp += _str[i];
}
return tmp;
}
代码也非常简单,我们再测试一下
没有问题
我们再实现一下resize
void resize(size_t n, char ch = '\0')
{
if (n < _size)
{
_size = n;
_str[_size] = '\0';
}
else
{
reserve(n);//直接调用reserve检查看是否扩容
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
代码逻辑也非常简单,我们再测试一下
我们和标准库的对比一下,没有问题
我们再来插入一下流插入和流提取,流插入和流提取涉及抢占第一个位置的原因,不能写成成员函数,所以我们写成全局函数
我们先这样写,发现报错了,原因是这里涉及到防拷贝的知识,大家现在先了解一下,后面我们会讲,我们修改一下
ostream& operator<<(ostream& out, const bai::string& s)
{
/*for (size_t i = 0; i < s.size(); i++)//两种方法都可以
{
out << s[i];
}*/
for (auto ch : s)
{
out << ch;
}
return out;
}
代代码一样很简单,我们再测试一下
此时我们就可以不用c_str来打印了
这里再说一下,函数参数里的bai::string,我们也可以把他放到命名空间里,这样就不用加前缀了
那这样打印和c_str有什么区别呢?
c_str是返回c的字符串,打印的是const char*,是内置类型,遇到\0就终止
而流插入是不管\0的,有多少字符就打印多少字符,目前是有bug的,但我们目前不用管,我们简单实现就行,我们来测试看看区别就明白了
这里大家就可以看到他们的区别了
这里的bug很麻烦,我们中间有\0的话,我们上面的实现就得改很多内容,我们使用了非常多的str函数,比如strcpy等等,遇到\0就终止,我们都得修改,如果不用strcpy的话,我们得用memcpy
我们来看一个bug,就用拷贝构造举例
strlen是不用替换的,因为c语言的字符串结束位置是\0,而string的结束位置是看size的
所以我们把strcpy替换为memcpy
修改完后就没有问题了 ,大家要记得把strcpy都替换掉,比如reserve,append那里,也会有bug
在结尾我会把代码全都附上,大家也可以到时候去对比
我们再看流提取,直接说结论,这样写是错误的,我们输入换行和空格是不能结束的,我们来看一段程序
我们要输入多个字符,中间是以换行或者空格为分割的,流是认为换行和空格是分隔符
这个问题在整形里尤为明显
如果没有换行和空格,是不知道输入的到底是什么,比如第二张图的123,这是1,2,3还是123?
此时我们就需要get,无论是什么字符都可以拿过来
另外,我们使用cin对于同一个string每次输入是独立的,但我们现在不是,我们测试一下就明白区别了
我们的代码不会覆盖掉之前的内容,所以我们需要一个clear函数
clear函数非常简单,这里就不多说了
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch = in.get();
//in >> ch;
while (ch != ' ' && ch != '\n')
{
s += ch;
//in >> ch;
ch = in.get();
}
return in;
}
我们看流提取,再测试一下
此时就没有问题了 ,不过我们的代码还可以再优化一下,while里面有个+=,怎么看怎么难受,我们一次输入很长的字符串时,会多次扩容,所以我们修改一下
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch = in.get();
//in >> ch;
char buff[128];
int i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 127) //要给\0留一个位置,所以不是128
{
buff[i] = '\0';
s += buff;
i = 0;
}
//s += ch;
//in >> ch;
ch = in.get();
}
if (i != 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
这里就相当我们用了一个桶,水满了或者接完了我们再放入, 而之前的代码相当于一滴一滴的接
此时我们的问题都解决了码?没有,如果我们一上来就输入空格,然后输入数据呢?官方的库是可以拿到数据的,而我们的是拿不到数据的,我们下面来解决这个问题
我们加上框里的代码就可以了
我们平时运行代码时看着一行就完事了,其实当我们自己上手的话,会发现有很多很多的问题,所以,任重而道远啊
我们再来写比较大小,string的比较是按ASCII码比较的
字符串的比较都是按ASCII比较的
我们简单写写,不过这样写还是有bug的,遇到中间\0的还是有问题
用memcmp也是有问题的,坑非常多,首先memcmp要给一个长度,我们给定两个字符串中长度小的那一个,但是还有问题,比如这两个字符串,hello和helloxxx,这样结果就变成相等了,长度+1也不行,比如前面的中间\0问题,一个是hello\0xxxx,一个是hello\0yyyy,问题非常多
//bool operator<(const string& s)
//{
// 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 false;
// }*/
// //return i1 == _size && i2 != s._size;//这2种更简洁
// return _size < s._size;
//}
bool operator<(const string& s)//复用版本
{
bool ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);
return ret == 0 ? _size < s._size : ret < 0;//ret<0是第一个小于第二个
}
//3个测试用例及答案
//"hello" "hello" false
//"helloxx" "hello" false
//"hello" "helloxx" true
我在这里提供了多种版本,代码的逻辑有点绕,各位最好自己画一下,再对比后面的三个测试例子就明白了
有了小于,大家就可以轻松写出其他的比较,记得使用复用可以写起来更加简便,比如我们写大于等于就可以用小于取反
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);
}
全部复用即可,最后再全部加上const
下面我们再实现赋值,也就是=,我们要实现深拷贝
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;
}
这样就搞定了
不过上面是传统写法,都什么年代了还在写传统代码,接下来看现代的写法
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;
}
这里是先用拷贝构造,把tmp开辟一个和s一样大的空间,然后tmp出了作用域就会销毁,所以把内容全部换一下就ok了
另外这里是不能和*this换的,会出现栈溢出的问题,我们这里是赋值,然后调用swap,swap的是两个string对象,又是赋值,死循环递归了
要交换成员才可以
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s);
//this->swap(tmp);两种都可以
swap(tmp);
}
return *this;
}
最后我们把swap分出来,就变成了这样,当然,还可以继续简化
string& operator=(string tmp)
{
swap(tmp);
return *this;
}
是不是很神奇?这个写法是全自动的,比如我们写s1=s2,这里tmp一上来就是s2的拷贝,还是深拷贝,也就是会先调用拷贝构造,出了作用域tmp还会调用析构函数
拷贝构造也可以使用现代写法,我们来实现一下
string(const string& s)//拷贝构造
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tmp(s._str);
swap(tmp);
}
我们需要对数据进行初始化一下,因为内置类型编译器不做处理,这种写法是调用了构造函数
但是这种写法是有问题的,在中间是\0的时候就会出问题
s4这里后面是没有xxxx的,所以我们建议还是用传统的
引用计数和写时拷贝
写时拷贝这个概念我们了解一下即可,深拷贝的代价是很大的,有时候我们只拷贝,不做别的事情,是很浪费的,我们对比浅拷贝,浅拷贝的问题是会析构两次,一个对象修改会影响另一个对象,所以就出现延迟拷贝(写时拷贝)
他在这里加一个一个引用计数, 只有s1指向时是1,s2也指向时就变成了2,当s3也指向时就变成了3
这样做的话当s2释放时不析构,他会减一下引用计数,s1再释放时再次减少,引用计数变为0,此时才会析构,也就是最后走的一个人关灯
再看第二个问题,如果要修改,发现引用计数是2时,就不能修改,此时就会发生写时拷贝,即写的时候,引用计数如果不是1,就进行深拷贝,再修改
比如s1和s2指向一个空间,然后要修改s2,就会深拷贝,引用计数也会拷贝过来,然后就可以修改了 ,Linux下就使用的是这个
最后,我在这里附上我们模拟实现的全部代码
全部代码
#include<iostream>
#include<string>
#include<algorithm>
#include<assert.h>
using namespace std;
namespace bai
{
class string
{
public:
typedef char* iterator;//迭代器
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
typedef const char* const_iterator;//const迭代器
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)//构造函数
string(const char* str = "")//构造函数
:_size(strlen(str))
{
_capacity = _size;
_str = new char[_capacity + 1];
//strcpy(_str, str);
memcpy(_str, str,_size+1);
}
string(const string& s)//拷贝构造
{
_str = new char[s._capacity + 1];
//strcpy(_str, s._str);
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& 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=(const string& s)
//{
// if (this != &s)
// {
// string tmp(s);
// //this->swap(tmp);两种都可以
// swap(tmp);
// }
// return *this;
//}
string& operator=(string tmp)
{
swap(tmp);
return *this;
}
~string()
{
if (_str)
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
}
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];
//strcpy(tmp, _str);
memcpy(tmp, _str, _size + 1);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void resize(size_t n, char ch = '\0')
{
if (n < _size)
{
_size = n;
_str[_size] = '\0';
}
else
{
reserve(n);//直接调用reserve检查看是否扩容
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
//2倍扩容
reserve(_capacity == 0 ? 4 : _capacity * 2);//这里的判断是防止空串初始化容量为0
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
//至少扩容到_size + len
reserve(_size + len);
}
//strcpy(_str + _size, str);
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)
{
//至少扩容到_size + n
reserve(_size + n);
}
//挪动数据
/*int end = _size;
while (end >= (int)pos)
{
_str[end + n] = _str[end];
--end;
}*/
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);
if (_size + len > _capacity)
{
//至少扩容到_size + len
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;
}
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)//长度超过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;
}
//"hello" "hello" false
//"helloxx" "hello" false
//"hello" "helloxx" true
//bool operator<(const string& s)
//{
// 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 false;
// }*/
// //return i1 == _size && i2 != s._size;//这2种更简洁
// return _size < s._size;
//}
bool operator<(const string& s) const//复用版本
{
bool ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);
return ret == 0 ? _size < s._size : ret < 0;//ret<0是第一个小于第二个
}
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);
}
private:
char* _str;
size_t _size;
size_t _capacity;
static size_t npos;
};
size_t string::npos = -1;
ostream& operator<<(ostream& out, const string& s)
{
/*for (size_t i = 0; i < s.size(); i++)//两种方法都可以
{
out << s[i];
}*/
for (auto ch : s)
{
out << ch;
}
return out;
}
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch = in.get();
while (ch == ' ' || ch == '\n')//处理缓冲区前的空格或者换行
{
ch = in.get();
}
//in >> ch;
char buff[128];
int i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 127) //要给\0留一个位置,所以不是128
{
buff[i] = '\0';
s += buff;
i = 0;
}
//s += ch;
//in >> ch;
ch = in.get();
}
if (i != 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
};
以上即为本期全部内容,希望大家可以有所收获
如有错误,还请指正