String
- 前言
- 为什么学习string类?
- string类的常用接口说明
- string类对象的常见构造
- 析构函数
- 赋值运算符重载
- [ ] 重载
- size和length
- 迭代器
- 字符串追加
- 关于容量的函数
- insert和erase
- find
- replace
- c_str
- rfind
- find_first_of
- find_first_not_of
- find_last_of
- substr
- getline
- to_string
- 总结
前言
这篇博客讲的是STL中的string,作为一名C++程序猿,搞懂STL可是非常重要的。这篇博客就是介绍一些string类中比较常用的函数。但重要的不是这,因为记住所有的函数接口是不可能的,而且重要的函数接口就几个。重要的是让大家培养出查文档的习惯。
string类中成员变量大概长这样,其实就是一个专门放字符串的顺序表,不过是库里面给你提供了一个现成的,不需要你自己去实现:
这里的size和可不是sizeof()中的那个理解了。size就是有效字符的个数。这里是hello这五个。
上面的图留个印象,可以帮助了解string类。
为什么学习string类?
C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。不是很方便。
C++中有string类来提供各种各样的函数接口,大大提高了办事效率。
学了数据结构的同学,简单理解string类的话,其实就相当于是存放数据类型为char的顺序表。
函数接口有很多,我这里给出相应的网站供大家参考:STL中string参考文档,里面是英文的,读起来比较费劲的话我也没什么办法。
简单说几点,看不懂没关系,刚接触肯定不能完全理解的,我刚学的时候也不懂。
- 字符串是表示字符序列的类。
- 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器(容器就相当于是数据结构)的接口,但添加了专门用于操作单字节字符字符串的设计特性。
- string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信息,请参阅basic_string)。
- string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)。
typedef basic_string<char, char_traits, allocator> string;
- 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
关于编码的东西,我直接给出大家一篇文章:刨根问底之——什么是编码?
感兴趣的可以自己看看,不过重点不在这。
要记住一点就是不同的编码中,一个字符可能占用不同的字节数。
比如utf-8一个汉字占用3个字节,utf-16一个汉字占用2个字节,utf-32一个汉字占用4个字节。
- 不能操作多字节或者变长字符的序列。
string类的常用接口说明
只讲常用的接口,如果想要自己拓展,可以查看文档,
string类对象的常见构造
先看一下提供了几个构造:
总共七个构造,很多,但常用的就两三个。上面图中标上序号为1,2,4的记住就行。剩下的我待会给出用法,不需要记,等到写程序的时候需要某种功能的时候,去这里面查查就行。
上例子:
标号为1,2,4的。
1 string();
2 string (const string& str);
4 string (const char* s);
上面cout能直接打印string类对象,是因为库中也有重载的>>函数。
标号为3的。
string (const string& str, size_t pos, size_t len = npos);
这个函数功能是从string类对象str中的pos位置开始,往后len个长的字符作为某个string对象的初始值。
当len超过了能够初始化的有效字符长度或者没有给len值的时候会直接将pos后面全部作为初始化的内容。
len过大:
未给len(len为缺省参数npos的时候):
npos的值为-1:
上面第一句话就说了npos是string类中的一个静态成员常量。固定为-1。
当len以无符号整型size_t接收-1的时候,-1以补码的形式给了len,len会接收到一个非常大的数,有42亿多,就是二进制全1的情况(如果不懂去了解一下整数的存储规则),其实也就相当于是len过大的情况。因为基本不可能出现一个42亿字节大小的字符串,也就是4G左右的大小。
标号为5的
string (const char* s, size_t n);
这个函数是将常量字符串s中前n个字符作为对象初始化的值。
可以这样用:
也可这样用:
这里的c_str后面会讲,这里先不说。
标号为6的
string (size_t n, char c);
这个函数将类对象的初始化字符串设置为n个字符c。
最后一个先不说,用到迭代器了,等会讲迭代器的时候再说。
析构函数
这个没啥讲的,自动调用的东西。
赋值运算符重载
库里提供了三个,1,2重要,但挨个说一下。
第一个和第二个放一块:
string& operator= (const string& str);
string& operator= (const char* s);
上例子:
第三个:
string& operator= (char c);
再说个这个:
上面的几个初始化,都是调用的构造函数,只有最后一个赋值才是赋值运算符重载,初始化时不管用没用=,都不是调用赋值运算符重载。编译器做了优化。
这个在我前面类和对象中的博客也讲了,不懂的可以看看:类和对象下,看explicit关键字那里就行
最下面那个才是赋值运算符重载。中间蓝色框框住的是调用对应函数时,反汇编中函数的地址,右边绿色的是调用的函数。
[ ] 重载
这个[ ]的重载,对于字符串来说非常有用,可以向数组一样来访问字符串中的任意位置字符。
例子:
访问任意位置:
遍历字符串
可以看到,库里面给了两个,还有一个const修饰的。
意思就是假如用string创建出的const的对象,这个对象是不能被修改的,此时再使用[ ]重载就会调用const修饰的[ ]重载。
来例子:
此时就只能用来访问了,也就是只读的。
[ ]会自动检查越界的情况。
还有一个能随机访问的。
看下例子:
不过用起来没有[ ]那么方便,这个也是提供了有const修饰的函数的。
这个at和[ ]的区别就是,用at越界了之后是抛异常,[ ]是程序直接崩溃。
可以看到上面那张图中,for循环中我用到了s1.size()这个函数,讲讲这个。
size和length
库里提供了这两个。
二者表示的是一个意思,只不过当初STL还没出的时候string已经出了,没出STL的时候用的是length表示string中有效字符的个数,后来除了STL之后,为了跟STL中其他的容器看齐才加的size,因为别的容器中都用的是size来表示元素个数,所以后来也给string中加上了这个size,用来表示有效字符个数。
那么上面的那个例子就好理解了。s1.size()就相当于是字符串的长度。
这里再把lenth给出:
一模一样。
迭代器
这个就要重点说了。
先看长啥样:
其中stirng::iterator就是迭代器,s1.begin()就是迭代时的初始位置,s1.end()就是迭代的末尾位置,就相当于是首元素的地址和末尾有效元素的地址。
这个用法是死的,毕竟语法就是这样,没法改,只能记住。
迭代器用起来就像是指针,但是有的容器中iterator用的是指针,有的就不是。
string和vector(顺序表)中的迭代器其实就是指针实现的。
其他的就不是指针了。但是在string和vector中不习惯用iterator,因为可以直接用[ ]来访问元素,用iterator反而麻烦了。
list/map/set…只能用iterator来访问,用法也是类似的。
这里给出list(链表)中使用iterator的例子:
注意循环里面的 it++,我刚学的时候老是忘记。
可以看到,我们用C语言写的链表可没有这样的功能,只能是next来找下一个节点。所以说C++学STL是很有必要的,会方便很多。
上面的是正向的迭代器,还有反向的迭代器。
叫做reverse_iterator,对应的头和尾也要改成rbegin和rend。
不仅有正反向还有const修饰的对象。
像上面的一样,也是const对象就调用的是const修饰的begin和end。
把例子给出:
正向+const
反向+const
如果觉得写迭代器比较麻烦的话,可以用auto来替代它,auto可以自动识别类型,这个在我的第一篇C++博客当中也是说了的,感兴趣的可以看看直接看最后的auto就行 。
还是给例子:
这时候不管有多长的类型,都可以直接用auto来替换,写起来就比较方便了。
提到auto了就说一下范围for。
给例子:
看起来很🐂,其实范围for底层就是用迭代器来实现的。
迫于vs2019下反汇编看起来二者相似度不大,但我又我没法展示出来更为相似的地方,但反汇编还是有些地方是相似的。
用vs2013可能更好观察一点。
迭代器就说到这。
下面把前面的构造函数的最后一个演示一下:
template <class InputIterator>
string (InputIterator first, InputIterator last);
上面的InputIterator first和InputIterator last这两个,其实传的就是迭代器。
参数中 s1.begin() 和 s1.end() 可加可减。
字符串追加
有三种用法。
第一种是push_back接口。
第二种是append接口。
第三种是+=接口。
挨个演示
push_back
加单个字符
append
展示几个,其实和构造函数里的一模一样那几种情况一模一样。
string& append (const string& str);
string& append (const string& str, size_t subpos, size_t sublen);
这个和构造函数里面的那个很相似。
正常情况:
sublen太大:
不给sublen:
string& append (const char* s)
演示下迭代器,剩下的两个就不说了,跟构造函数里的用法一样。
然后就是+=了
这个用起来会比前两个方便很多。
演示一下:
所以前两个方法只是为了演示一下,不建议用,真要用的话用+=就行,很方便。
关于容量的函数
前言部分,给了这张图:
capacity函数可以返回string对象的容量
预开空间reserve:
这个函数会将_capacity开到n(或者更大)。不会初始化string对象中的内容。
最后开的_capacity的大小在不同的系统下是不一样的,vs下是这,g++下就不是这个了,会直接开为100。
里面啥也没放:
预开空间并初始化resize:
这个函数会将_capacity开到n,并且会将size的大小改为n,然后将前n个初始化为c,如果没给c,则初始化为0。
看以看到size变为了100,capacity变为了111。
string的扩容机制在vs2019下有点怪:
我用下面的代码进行string类对象的扩容观察:
string s1;
cout << "init size::" << s1.size() << endl;
int capacity = s1.capacity();
cout << "init capacity::" << s1.capacity() << endl;
int count = 1000;
while (count)
{
s1 += 'h';
if (capacity != s1.capacity())
{
cout << "capacity changed ::" << s1.capacity() << endl;
capacity = s1.capacity();
}
count--;
}
在vs2019下:
可以看到大概是1.5倍的扩。
而且如果用reserve的话,也不是给定的capacity值。
但是g++下就不一样了:
同样的代码放到g++下:
运行:
得到的结果就是100,不是vs2019下的111。
再看一下扩容的情况:
这里能看到,g++下是2倍的扩的。
就是给大家演示一下,不同编译器下的结果可能是不一样的。
再演示一下resize缩小size的情况:
上面resize(5),会将hello world!!!中的 world!!!全部劈掉,只剩下hello。
insert和erase
先看inseret:
其实参数和上面的构造等非常相似,都跑不开string对象,常量字符串,单个字符。
string& insert (size_t pos, const string& str);
//插string对象
string& insert (size_t pos, const string& str, size_t subpos, size_t sublen);
//插string对象的sobpos位置处的后sublen个字符
string& insert (size_t pos, const char* s);
//插常量字符串
string& insert (size_t pos, const char* s, size_t n);
//插常量字符串的前n个字符
string& insert (size_t pos, size_t n, char c);
//插n个字符c
上面的都是从pos位置插入,只不过是前插。
放一块演示一下:
剩下的就不说了。
然后是erase
就说第一个。
从pos位置开始,删除npos个字符。
默认情况下全删
给上参数:
insert和erase不建议多用,因为二者都可能存在挪动数据的情况,效率比较低。
find
上面都是找字符(串),找到了返回下标,找不到返回npos。
//从pos位置开始找str
size_t find (const string& str, size_t pos = 0) const
//从pos位置开始找s
size_t find (const char* s, size_t pos = 0) const;
//从pos位置开始找s的前n个字符,很鸡肋,不如第二个
size_t find (const char* s, size_t pos, size_t n) const;
//从pos位置开始找字符c
size_t find (char c, size_t pos = 0) const;
这里注意,找的永远是从pos位置往后的第一个c。
给道例题:将下面字符串中空格替换为字符串:::
hello world I am xxx
也就是换成:
hello:::world:::I:::am:::xxx
第一种方法:结合前面的insert和erase
具体的就不讲了,注意画图理解。
第二种方法:空间换时间
还有一个函数replace,听名字就是替换。
replace
string& replace (size_t pos, size_t len, const string& str);
将string对象的pos位置往后的len个字符替换为str。
给几个例子:
0位置往后的2个字符换为tmp
0位置往后的3个字符换为tmp
0位置往后的1个字符换为tmp
0位置往后的2个字符换为tmp
然后也能用这个来搞前面那道例题了:
然后replace的就不讲了,感兴趣的自己可以查文档。
但是如果想要找到最后一个单词呢?
可以用rfind。
c_str
这个比较有用,就是将string类对象中的字符串以常量字符串的形式返回。也就是将C++中的类对象转换为C中的常量字符串。
其实二者没什么区别。区别都是C++和C的语法使用上的。
但是有一个场景要注意:
C++中打印字符串是以字符串本身的size来决定打印多少的。
而C中的打印是以’\0’为结束标志的。
就记住这个就行。
rfind
这个就是倒过来找的意思。reverse find。
然后就可以做上面的那道题了:
还是不细讲,用的时候查文档就行。
find_first_of
这个函数名字起得不好,应该叫find_any_of,这个函数用法是找到字符串里任意一个字符第一次出现的位置。
例子:
这里的2就是hello中的第一个 l 的下标。
这里的用法是从第五个位置开始找,找到oI中的任意一个。
find_first_not_of
找到对象中第一个没有出现字符串中字符的位置。
不细讲。
find_last_of
找到最后出现的。
其实就是倒着找。
substr
这个函数是帮忙取出子串的。
直接给例子:
取出下标为3位置处的后4个字符
再来个例子:将网址的协议、域名、后面的分开打印出来。
网址都是死格式:
protocal://hostname:portname/pathname/?search#hash
就先找://,搞成字串
再找/,搞成字串
后面的再搞成字串就OK了
我就以我现在所编写的网址为例:
getline
还有一个比较重要的东西,就是getline,相当于C语言中的fgets。
就是获取一行字符串。
有的同学可能会说为啥不直接用cin呢?
看:
cin和scanf一样,遇到空格或者回车就会当成是字符串就结束了。
而getline和fgets(gets)就是C++和C中能够读取包含空格字符串的两个函数。
所以这里用getline就好了。
to_string
这个也比较好用。
将数字转成字符串。
这些所有参数的类型都能转换成string类型。
C语言中的atoi没有这个好用。
还有字符串转数字的。
总结
剩下的没什么好讲的了,其实最常用的就是[] += 迭代器 size这几个常用点,剩下的留个印象就行,没必要全记住,其他的想用的时候忘了查文档就行。
到此结束。。。