[STL] string类
文章目录
0. 为什么要学习string类?
string类实际上就是对字符串进行了封装,在C语言中就有字符串数组,为什么要C++要对字符串进行封装?是因为C语言中的字符串数组在使用时有很多不方便的地方,比如对字符串的修改,甚至是字符串的长度不够需要进行扩容,使用时还容易造成越界,由于这些对字符串的使用需求,C++作为一个面向对象的语言,就对字符串进行了封装,这样再使用字符串时,用户不用再对内存空间进行操作,很多功能只需要使用string类提供的函数就可以实现。
注意:由于STL的实现有很多版本,此文中使用的是visual studio编译器实现的版本。不同版本的STL可能会在扩容规模等不同地方的实现有所不同。
1. 标准库的string类
1.1 关于string类
通过查阅文档发现string类是对basic_string类实例化后的重命名,为什么不直接定义string类?观察basic_string的说明:basic_string是任何字符类型的类字符串的泛化。实际上字符类型是不只有一种的,string类就是对char类型字符的实例化。
basic_string所实例化出的类有下面几种:
basic_string实例化出多种类型是因为有多种字符类型,字符类型为什么会有很多种?这就涉及到编码的问题了,我们知道计算机底层只能存储0和1,但是我们会有存储和显示字母、数字、符号的需求,为了实现这些需求,就需要编码的帮助,学习C语言时ASCII码是很常见的编码:
ASCII码表中每一个字母、数字、符号都对应一个数字,计算机虽然不能存储字母、数字、符号但是能够存储数字,在计算机上显示字母、数字、符号的原理就是对存储的数字按照编码规则翻译,然后用图像技术处理显示在屏幕上。
如果只是存储字母、数字、符号(使用ASCII码)用char类型字符占用一个字节空间来存储绰绰有余,但是计算机不会只显示字母、数字、符号、为了满足显示别的文字等等的需求,创造了除ASCII以外的编码:
在统一码中显示不同的内容,需要存储编码的空间不同,为了满足存储不同内容的需求,需要有不同类型的字符(实际上就是字符的存储空间不同),因此basic_string实例化出许多类型。
几种类型字符存储的类型不同:
汉字的编码在Windows下采用的是gbk编码:
gbk兼容ASCII码,第一个比特位存储0也就是显示为正数会去ASCII码寻找对应编码,第一个比特位存储1也就是显示负数就会去汉字中寻找对应编码,编码基本上就是这样的原理。
汉字的编码规则会把同音字编码到一块,这一点常常被应用于屏蔽词功能,比如在游戏中我们想和队友进行激情的交流时有些字会变成**,这些字我们打不出来就会去打同音字,利用这个规则很方便的就把同音字也一起屏蔽了。
1.2 了解构造函数
string类函数由于是早期设计的,设计出100多个接口函数,很多函数是不常被使用的,因此本文只会涉及比较常用的函数。
string类内重载了许多个构造函数:
首先我们来看第一个不传入参数的就是默认构造函数,第四个是使用常量字符串进行构造
注意图中s3的构造过程,单参数的构造函数传入参数可能会发生隐式类型转换,"hello world!"字符串会被构造成string类,然后拷贝构造给s3,但是visual studio下会优化成直接用字符串构造s3。
第二个是拷贝构造函数,第三个是将string类从pos位置往后取len长度的字符进行拷贝构造:
注意第三个函数,如果被拷贝的string类内没有len长度的字符可以拷贝就相当于把pos位置后所有字符拷贝,该函数第三个参数给了一个缺省值:
由于是无符号整数,-1的补码会被解析成4294967295,当然string类也不能存储这么多的字符,该缺省值的意义就是如果不给值,该函数会把pos位置后的所有字符拷贝。
第五个函数的功能是用字符串的前n个进行构造:
第六个函数是用n个字符构造string类:
1.3 了解容量相关函数
和容量相关的函数如下:
size和length都是返回string类内字符的长度的函数,用法是一致的,由于STL中其他容器的长度函数都是size建议使用size函数。
文档中说max_size函数返回string类能达到的最大值,实际上它返回的是int的最大值,string类达不到这么大,这个函数实际意义不大。
capacity函数返回的是string类申请的空间能够存储的字符大小(visual studio 下不包含\0的大小)。
通过监视窗口来查看string类申请的空间大小:
在讲解扩容函数前,可以看看visual studio下的一种扩容机制:
#include <iostream>
#include <string>
using namespace std;
int main()
{
// 观察扩容情况 -- 1.5倍扩容
string s;
size_t sz = s.capacity();
cout << "making s grow:\n";
cout << "capacity changed: " << sz << '\n';
for (int i = 0; i < 100; ++i)
{
s.push_back('c');
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}
在vs下对string类字符不断插入单个字符观察它的扩容情况(注意capacity所指空间不包含\0)第一扩容是二倍扩容,后续扩容大概是1.5倍扩容,实际上每次扩容都是1.5倍扩容,这vs下的实现有关:
通过监视窗口查看vs下的string类有两个成员变量一个是buf一个是ptr一个string类构造时默认会开16个字节的空间也就是buf所指的空间,一旦buf所指的空间不够用了就会先开一个32个字节的空间交给ptr,然后将数据给到ptr所指的空间,在ptr中每次空间不够用就会1.5倍扩容,在使用ptr所指的空间后,buf所指的空间就被抛弃了。
//模拟vs下的string类成员变量实现
class string
{
private:
char _buf[16];
char* _ptr;
int size;
int capacity;
};
由于不同的STL实现不同,Linux下的string类就没有类似于buf的成员变量,每次扩容是二倍扩容,上面的代码在Linux下的运行结果:
了解了一些vs下的底层实现,我们来看string类的扩容函数:
reserve函数的功能是保留原有数据进行扩容,vs下扩容后的空间一般都比用户所指定的大,这和它的底层实现有关,该函数可以帮我们提前开好空间,减少扩容,提高效率。
还有一个扩容函数是resize函数:
resize函数还有初始化的功能:
同样是扩容100,reserve函数不会对数据进行修改,resize函数会把扩容出的空间内除了原有数据以外的数据默认修改为0
resize重载了可以自定义修改内容的函数:
resize函数有缩容的功能,可以将不要的数据部分清除
resize的缩容实际上就是修改了可以读取的字符个数,并没有将清除的数据从空间上清除,因为C++不支持清除一部分空间的操作,如果支持了内容管理的机制将会很复杂。
shrink_to_fit函数的功能是将string类的空间容量(capacity)相应数据容量(size)进行缩容
如果capacity远大于size大概率是会进行缩容的,如果capacity和size接近大概率不会缩容,由于C++中内存管理机制,缩容函数的代价较大,使用缩容函数需要谨慎。
1.4 了解修改函数
push_back函数的功能是再string类的字符后面插入单个字符,append函数的功能是在string类的字符后插入字符串。
以上两个函数的使用不是十分的方便,string类内还提供了+=运算符重载:
+=不仅能实现插入单个字符还能实现插入字符串。
因为有了push_back和append函数,实际上+=运算符重载是对push_back和append函数的封装。
assign函数的功能就像变量赋值一样,不论变量原有数据是什么,让变量的数据变成赋值后的数据。
insert函数提供了在string对象的数据内插入数据的功能。
使用insert函数的第三种接口在string对象的数据的头部插入数据:
使用insert函数的第五种接口往数据中插入空格字符:
使用第六种接口往string对象迭代器指向的位置插入空格字符:
insert不建议频繁使用,效率很低。
有插入函数同样也有删除函数:
调用第一种接口删除数据第五个位置开始后面三个字符:
第一种接口给了缺省值npos,前面提到了npos是一个很大的数,string数据个数不可能有npos那么多,该缺省值的意义就是在不传入值时将指定位置后的全部数据删除。
第二种接口的功能是删除迭代器指向的数据:
erase函数和insert函数一样不建议频繁使用,由于经常挪动数据,效率很低。
replace函数的功能是将string对象数据进行替换。
使用第一种接口将第九个字符换成***字符串:
swap函数的功能是将两个string对象的数据进行交换。
在swap函数的介绍中提及了algorithm库中也提供了swap函数,功能类似string中的swap函数。既然库中都实现了swap函数为什么string中还要实现swap函数?因为两种swap函数的实现方式不同。
库中实现的swap函数的实现方式是创建中间变量然后进行赋值。
而string中的swap实现方式是交换两个string对象指向的数据。
显然只是交换指针的指向的效率远高于赋值,因此string中实现的swap函数还是有意义的。
实际上在vs的实现下如果数据存在_buf中实现方式与库中没有区别,如果数据存在 _ptr中才会采用交换指针的方式。
1.5 了解迭代器
STL中的数据访问都采用迭代器的方式,因此使用迭代器是很重要的方式。
迭代器的使用起来是像指针一样。
我们先看看如何用迭代器遍历输出string类对象的数据:
迭代器使用起来就像指针,首先迭代器指向数据的头,然后解引用,然后迭代器加1,循环此过程直到迭代器指向数据的尾部的后一个位置。
下面我们来看看迭代器提供的接口函数。
begin函数返回一个指向string对象第一个数据位置的迭代器。
end函数返回一个指向string对象最后一个数据后一个位置的迭代器。
我们在来看这段代码:
#include <iostream>
using namespace std;
int main()
{
string s("hello world");
string::iterator it = s.begin();//定义一个string类的迭代器指向string数据的第一个位置
while (it != s.end())//循环直到迭代器指向string对象最后一个数据的后面一个位置
{
cout << *it << " ";
it++;
}
return 0;
}
迭代器的指向如下图:
为了实现反向遍历,string类还提供了反向迭代器:
rbegin函数会返回一个指向string对象最后一个对象数据的反向迭代器。
rend函数返回一个指向字符串第一个字符之前的理论元素的反向迭代器。
我们来看看反向迭代器和正向迭代器遍历同一组数据的效果:
注意:反向迭代器加1是从数据末尾往数据头部挪动。
迭代器还提供了const版本:
前面提到了迭代器的使用就像指针一样,const版本的迭代器就像在指针前加了const,const修饰的指针指向可以改变,但是指向的内容不能改变,const版本的指针也是一样。
图中代码const版本的迭代器加1就是迭代器在改变指向,但是迭代器指向的数据是不能改变的,使用起来的感觉和const修饰的指针一样。
向函数传入const类型的string对象引用,只能返回得到const版本的迭代器。
由于迭代器的名字过长,可以使用auto来使用,auto会通过函数返回值识别类型。
在C++11中,还提供了cbegin,cend,crbegin,crend函数用来返回const版本的迭代器:
1.6 了解迭代器外其他访问数据方式
迭代器使用起来给人的感觉很麻烦,除了使用迭代器外string类还提供了其他访问数据的方式:
string类重载了[]运算符使得string对象能够像数组一样进行访问和修改数据。
用[]运算符访问越界时会报断言错误:
除了使用[],string类还提供了at函数访问数据:
at函数访问数据越界时,会采用抛异常的方式:
虽然使用迭代器访问string对象的数据很麻烦,并且string类提供了除使用迭代器以外的访问数据方式,但是迭代器是STL提供的所有容器通用的数据访问方式,因此迭代器的使用很重要。
1.7 了解string数据操作函数
find函数的功能是寻找string对象数据中第一次出现的字符或字符串,然后将其位置返回,如果寻找失败会返回npos(即一个无效的位置)。
find函数配合replace可以实现对string对象的数据中指定的数据进行替换:
用find函数配合replace函数将string对象的数据中空格字符全部替换:
如果不传入值,使用缺省值,每次都是从数据的头部开始查找,效率会降低,并且replace还会频繁扩容,对于上面find配合replace的代码可以进行优化:
当然如果不考虑空间消耗还有另一种优化方式:
+=的效率远高于replace,相当于空间换时间的做法。
有正向寻找,当然也有反向寻找,缺省值给npos表明默认从string对象数据末尾开始寻找。
c_str函数的功能是将string对象的数据以字符串指针的形式返回。
C/C++中打印时传入字符串指针会按照字符串打印,按照字符串打印的方式遇到’\0’会停止打印,但是string的打印是根据成员变量给size的大小来打印的,不会因为’\0’的存在而停止打印:
substr函数的功能是将string对象数据中取一部分然后返回一个保存取出数据的string对象。
substr函数配合rfind函数能够实现得到文件后缀的功能:
find_first_of函数的功能是在string对象中寻找含有指定字符串中任意字符的位置的函数。
find_first_not_of函数的功能是寻找string对象中不含指定字符串的位置。
find_last_of函数的功能是从string对象数据的末尾开始寻找含有指定字符串的位置。
find_last_not_of函数的功能是从string对象的末尾开始寻找不含指定字符串的位置。
1.8 了解非成员函数的重载
string对关系运算符的重载使得string对象可以和string对象、字符串比较,但是这样的实现实际上是多余的,因为单参数的构造函数会发生隐式类型转化。
getline函数的作用是接受一整行的数据输入。
同样是输入一行字符,cin会在有空格和换行时分割,getline则是将整行获取,不论有无空格和回车。