【C++】——string类的使用(详细讲解)
一、string类简单介绍
1、string类参考网址
string类的文档
2、string类简介
由于stringhttps://cplusplus.com/reference/string/string/?kw=string类比STL发明要早,所以string没有被归到STL中,但是从归类上看可以将其和STL中的容器归为一类。
string是一个类,虽然平时使用的是叫"string",但是实际上它是被typedef之后的。通过这个模板basic_string指定类型char定义的一个类string,可以认为是一个char类型的顺序表。
二、构造函数
string类,其也是一种类,那么其也有构造函数,在C++98版本中一共有七个构造函数,在C++11中又扩充了几个,我们主要讲解98版本中的七个。
我们主要掌握一、二、四、六。
下面我们就讲解一下这几个构造函数是如何使用的。
构造函数的使用:
首先就是我们要使用string类的话,那么我们就需要包含其头文件<string>才可以使用其里面的内容。还有就是展开命名空间std。要是不展开的话,那么就需要频繁的指定命名空间域std::。
首先第一个构造函数,string(),那么其参数是空的,那么就是构造的一个空字符串。
第二个构造函数的话会使用参数里面的字符串来初始化字符串。
第三个构造函数,我们看到其有三个参数,那么第一个参数很简单,就是要初始化的内容,然后第二个参数就是我们要从第一个参数的那个位置开始复制copy到构造的字符串,就是第三个参数,npos是啥?
通过查询可以看到npos是一共无符号整型,且其值是-1,但是其是无符号所以其实际上是无符号整型的最大值。
那么就是说我们不传这个参数的时候,那么就会从传入的这个字符串的pos的位置开始copy到末尾。
然后第四个构造函数的话,其参数是const char *的类型,这是为了C++可以向下兼容C语言。
第五个的话,就是复制n个s中的字符构造字符串。
第六个构造函数就是给这个字符串构造n个字符c。
第七个构造函数就涉及到迭代器的内容了,接收两个迭代器参数 first
和 last
,它们定义了一个元素范围,然后遍历这个范围内的所有元素(通常是字符类型),用这些元素构造并返回一个 string
对象。这个我们后面再进行详解。
下面我们使用这七个构造函数来看看其效果:
然后还要注意的是,我们对于类的初始化还有使用复制运算符的:那么使用赋值运算符进行初始化的话是隐式类型转换,那么要先用指定的字符串去调用构造函数构造出一个临时对象,然后再通过拷贝构造拷贝给对应的字符串吗,不过这个过程编译器一般都会优化,使得其为直接构造,省了一次构造临时对象的时间。
然后就是对于隐式类型转换构造的对象,其具有常性,那么我们只能使用const类型的引用其接收。
还有就是我们的string类中,其已经完成了流插入<<和流提取>>的重载,不过要注意的是其并不输入string类的成员函数。所以我们可以直接使用cout打印我们的string对象。
然后也可以使用cin对一个string对象进行初始化。
三、赋值重载
这个内容我们在讲类和对象的时候已经讲的很详细了,这里我们简单过一下即可。
我们的赋值运算符对于内置类型的数据是可以直接计算,但是对于自定义类型,编译器是不知道其运算逻辑的,所以有了重载。
其赋值就是把一个字符串直接赋值给另外一个字符串。然后其右侧支持三种类型,分别为const string&str、const char*、char c三种。
其使用和我们的内置类型数据一样的。
就是要注意对于运算符右边是const修饰的数据,我们也要使用const的来接收,不然就会报错了。
四、string类的遍历
string的遍历有三种方式。
1、operator[]下标法
可以看到这里有两个重载,C++中字符串是可以读和写的,第一个版本的话是可以对字符串中这个位置的字符进行修改的,然后加了const修饰的就不能写,只能读。
这个方式的遍历的话,其有个好处就是其越界访问的时候其就会触发断言。
那么为了防止这种情况出现,那么我们可以提前知道这个字符串的长度,然后避免使用这个方式进行访问字符串的时候越加访问。
2、size求字符串长度
这个是string中的成员函数,通过这个函数我们得到这个字符串的长度,注意的是不包含\0.
3、[]+下标遍历
上面我们知道可以使用下标来访问字符串某个位置的字符,那么我们可以和遍历数组一样,使用一个循环遍历,然后我们循环的次数就是字符串的长度,所以我们先求出字符串的长度,然后利用循环遍历。
4、迭代器
在string中有两个成员函数begin()、end()。然后begin()是返回字符串开始的位置,end()返回的是字符串最后一个字符的下一个位置。
可以看到两个成员函数都是重载了两个版本,分别对应的是普通的可读可写的对象,那么其调用的就是第一个begin()和end(),返回的也是可读可写的。然后如果传入的对象是被const修饰的只能读的类型的对象,那么其就调用第二个const的函数。
那么我们就可以使用begin()得到起始的位置,然后end就得到末的位置,然后就可以从头开始遍历字符串了。
那么这是正向遍历,要是我们想要实现反向遍历呢?那么我们还可以使用反向迭代器。
4、反向迭代器
我们除了正向的迭代器begin和end外,我们的string还提供了反向的迭代器rbegin和rend,这两个就是反向迭代器,反向迭代器的话,begin返回的是最后一个字符的反向迭代器,然后rend返回的是第一个字符的前一个位置的反向迭代器。
不过这里的rend并没有直接去访问第一个位置的前一个位置,所以不会产生越界的行为。
那么我们搭配这两个成员函数可以实现字符串的反向遍历。
反向遍历:
我们会发现在遍历的时候使用的是++而不是--,这是因为正向的迭代器其是向右边的方向为正的,所以++是往右边走,然后反向迭代器就是左边是正向的,那么其++就是往左边走啦。
5、范围for
这个是在C++11后加入的功能。
auto关键字:
C++11中增加了auto关键字,其可以自动识别一个变量的类型。
可以看到其会自动去识别数据的类型,就是对于引用和指针指针情况,语法还是一样的。
然后auto也可以做为函数的参数,不过要编译器的版本较新才行。
那么这样的作用是啥呢?
就是我们学习了类和对象之后,有的数据类型的名称会比较长,然后使用这个的话就可以自动识别了,就不用我们人工去干预了。
使用范围for遍历:
可以看到这样也可以实现字符串的遍历,而且是正向遍历,这个是范围for中,自动取容器会将字符串中每个位置的数据赋值给ch,然后其会自动去判断循环结束的条件。
ch只是个名字,我们可以根据其实际意义去写。
然后使用范围for也可以实现字符串中字符的修改。
我们会发现s1并没有被修改,这是因为范围for中的类似于赋值这样将字符串中的字符赋值给ch的,所以我们要使用引用才可以使得s1的内容发生改变。
五、容量
1、size和length
我们前面使用过string中的size()成员函数了,然后我们的string中还有一个成员函数length(),其和size()的效果是一样的,都是返回字符串的长度。但是STL中其他的容器中,求大小的时候是没有length()这个函数的,都是size()这个成员函数。因此我们为了统一,通常会去使用size()。
可以看到这两个成员函数计算字符串大小的结果都是一样的,而且都是不算\0的。
2、capacity
这个成员函数就是返回字符串所能存储的有效字符个数。
这里打印出来是15,但是实际上是16个,还有一个字节的空间是用来存储\0的。
3、clear
这个成员函数的作用就是清楚字符串中的内容,空间不会被销毁。
4、empty
这个成员函数就是判断字符串是否为空。
可以看到当我们的字符串的内容是空的时候,那么这个函数就返回1,如果有内容的话就返回0。
5、reserve
这个函数的参数是一个无符号整型,然后其是缺省的,缺省值0.
这个函数的作用是将字符串的容量扩充至n,又因为有一些内存对齐的原因,导致实际扩充的空间会比传入的n要大一点。不过当n比当前字符串容量小,那么该函数会看实际情况来要不要缩小到n。并且这个函数不会影响字符串本身的长度和内容。
然后对于扩容的情况也很多种。
扩容可以原地扩容也可以异地扩容,realloc扩容要想原地扩容,那么就要其后面有足够的空间,如果不够那么就要异地扩容了。然后new只能异地扩容。异地扩容要去新找一块新的足够大的空间然后将原来空间的内容先拷贝到新的空间。然后对于扩多大,不同的编译器会有不同,例如在VS下,第一次是二倍扩容,后续就是1.5倍的扩容。然后在linux的环境下,每次扩容都是两倍。
然后缩容的话,先找一块空间将原来的数据拷贝到新的空间。这是因为我们扩容和缩容都只能从开头开始,然后没办法销毁一小段内容,所以只能先拷贝到另外一个空间。然后将原来的空间释放掉。
除非万不得已,不然不建议去缩容。
6、resize
这个成员函数的作用是,根据传入的n将一个字符串变成一个长度为n的字符串,那么其有两个重载,第一个就是我们没有传入字符,那么就插入\0,将这个字符串变成长度为n的字符串。然后要是有传入字符c那么就会插入这个字符c将字符串变成长度n的字符串。
那么这个函数在实际使用上一般会出现三种情况:
1、size<n<capacity
可以看到我们的s1开始的时候是11个字节的,然后其能够容纳的空间是15个字节,然后我们先使用resize(size_t n),然后我们会发现s1.size()的值为12,然后我们打印的时候\0是不会显示的。然后我们传了参数char后,其就变长到14了。
2、n>capacity
这个就是长度超过了字符串可以容纳的长度,所以不同的就是这个要先扩容,但是扩容多少就要看系统了。
3、n<size
这种情况的话,那么就会根据传入的参数n,然后保留字符串的前n个数据。
六、string类的元素获取
1、下标获取
这两个方式都可以通过下标的方式来或者对应位置的元素,第一个的话是一个符合重载的形式,然后第二个就是一个成员函数。
然后第一个方式我们前面已经讲过了。
at()函数其也是两个重载,所以其也是分为可读可写,还有就是只读的两个。
那么这两种下标获取元素有啥区别呢?
就是对于越界访问的处理方式不一样。
[]的话是通过断言来反馈的,然后at的话就是通过抛异常的方式。
2、back和front
这两个函数是在C++11后加入的,所以使用的时候要注意编译器是否支持。
back()是返回字符串的最后一个有效字符,然后front是返回字符串的第一个字符。
七、string类的修改操作
1、push_back
这个成员函数的作用就是在字符串中尾插一个字符,然后字符串的长度size会+1。要注意的是这个函数的参数是一个字符不是字符串,也不是string类。
2、append
这个函数就是在string类的后面加个字符串这样。其一共有六个重载。
第一个就是在string后面追加一共字符串。
第二个重载就是在string类的尾部,然后在str的subpos的位置开始取长度为sublen的字符。
第三个重载就是追加一个常量字符串。
第四个重载就是追加一个常量字符串的前n个字符。
第五个重载就是追加n个字符c。
第六个字符就是通过迭代器来追加字符。
下面我们就使用这几个成员函数来看看其效果:
3、operator+=
这个也是运算符重载,其有三个重载,其支持对string对象、常量字符串和字符的+=操作。
前面三种插入数据都是在字符串尾进行插入的,也就是尾插,那么下面我们看看头插是咋样的。
4、insert
这个成员函数的话就有七种重载了。
第一个就是在pos的前一个位置开始插入字符串str。
第二个是在pos的前一个位置开始,然后在字符串str上subpos开始取长度sublen的字符串插入。
第三个就是在pos的前一个位置开始插入常量字符串。
第四个就是在pos的前一个位置,取常量字符串的前n个字符插入。
第五个就是在pos的前一个位置插入n个字符。
第六和第七种就是迭代器的方式插入了。
对于上面的这种插入,要注意的是,其时间复杂度为O(n),所以时间效率比较低,尽量减少使用,这是因为每次插入都要挪动后面的数据。
5、erase
这个成员函数是用来删除字符串的数据的。
这里有三个重载。
第一个是在pos位置开始删除len个长度。然后npos前面也有详细讲过了。
第二个就是删除指定位置的数据。
第三个就是删除指定区间的数据。
要注意的是,这个接口其时间复杂度为O(n),这是因为其删除后也要涉及到数据的挪动。
八、其他操作
1、c_str和data
这个接口就是返回C语言形式下的字符串,即把一个string类型转换成const char*类型,然后在最后一个位置加\0。
两个接口的作用都是一样的,比较常用的是c_str。
2、copy
这个接口从其名字就知道其是用来干嘛的了,就是从pos位置开始拷贝len长度的字符数组,然后返回拷贝的字符数组的长度。
这个拷贝的数组是不含\0的。
所以我们拷贝完后要注意给最后一个字符的下一个位置赋一个\0。
3、substr
这个接口的作用就是从pos的位置开始截取长度为len的字符的字串取构建一个新的字符串。要是不传pos的话,那么就从0位置开始,要是不传len那么就取到尾部。
4、find和rfind
这两个接口是用来寻找字符匹配的对象的位置的,其从pos位置开始找string对象,字符串和字符的第一个匹配的,然后返回其下标,要是这个对象中没有匹配的,那么就返回npos也就是无符号整型的最大值。
然后rfind是从尾部开始往前找,find就是从头开始找。
八、其他非成员函数
1、关系运算符重载
这个关系运算符其是非成员函数,其是重载在全局的。
2、getline
我们在对变量进行数据输入的时候,我们一般都是使用cin,不过我们会发现一个问题,就是其会以空格或者换行来对输入的变量进行分割,就比如我们要输入hello world的时候,我们中间是有个空格的,那么其会以为前面的hello是给第一个变量的,然后world是给第二个变量的,所以为了应对这种问题,就可以使用getline。
getline函数其默认是以换行符为分割,我们输入一串字符的时候,其会一直读,然后直到遇到换行符,然后当我们指定字符的时候,那么其就会以这个字符作为分割。