string类
本节内容
为什么要学习string类 标准库中的string类 string类的模拟实现 总结
为什么学习string类
C语言里面没有字符串类型,那么我们是如何表示字符串的呢?我们通过使用字符数组,或者说是使用字符指针来去访问字符串类型的东西。 C语言中的字符串----->C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP(面向对象)的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。 C中有些处理字符串的函数其实并没有那么安全,一个不下心就会造成错误,比如说是越界访问啊什么的,就很容易引起错误,然后导致崩溃。
标准库中的String类
string类(了解)
String类文档的介绍 字符串是表示字符序列的类 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。 string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型 string类是basic_string模板类的一个实例它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
总结:
string是表示字符串的字符串类 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。 string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>string; 不能操作多字节或者变长字符的序列。 在使用string类时,必须包含#include头文件以及using namespace std;
实际操作一下String类中的各个接口
String类对象的常见构造
首先,如果想要使用string类这个类的话,那么首先是需要包含头文件的,如果不包含头文件的话,那么编译器也不知道这个string到底是什么东西,string的头文件在#include<string 里面,是没有.h的,这一点需要记清楚
# include <iostream>
# include <string>
using namespace std;
int main ( )
{
return 0 ;
}
引入了头文件之后,现在就需要知道string类的对象该如何去构造了,构造的方法有下面的几个方法 第一个,是构建了一个空的string类的方法,也就是说是是一个空的字符串 第二个是用c-string来进行构造 第三个是string类对象中包含n个字符c 第四个是利用了单参的拷贝构造函数,参数是const类类型的引用
# include <iostream>
# include <string>
using namespace std;
void TestString1 ( )
{
string s1;
string s2 ( "hello world" ) ;
string s3 ( 10 , '$' ) ;
string s4 ( s2) ;
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
}
int main ( )
{
TestString1 ( ) ;
return 0 ;
}
现在,我们通过监视窗口,输入s1,s2,s3,s4,进行查看,点开s1之后,会发现s1有两个数据成员,一个是size,一个是capacity,其实这有一点像顺序表的结构,size代表在string类中有多少个有效的字符,capacity就代表这底层空间最大有多大,在C语言里面看一个字符串是不是有效的,就看字符串后面到底有没有\0就可以去判断哪个字符串到底是不是有效的。C++的字符串里面其实也是有\0的,而size,是只包含了有效字符,并没有包含\0的个数的(size的大小只是有效元素的个数,是并不包含有\0的长度的) 原始视图是底层的一段空间,不用过多的去关注,和监视窗口是有关系的。 展开s2,对s2进行查看,发现size其实是没有包括\0的大小的(size不会计算\0的大小) string类类型的对象,可以直接使用cout进行输出,也可以直接使用cin来进行输入
cin >> s1;
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
在进行输入之后,s1的内部就会有内容了,在C语言中还需要关心,你所输入的字符或者说字符串的长度是多少,因为为了防止会有越界的情况产生,但是在C++中就完全不需要担心这样的情况,尽管往里面进行输入就可以了,不用担心空间够不够用的问题 s2和s5的构造方式时一摸一样的
# include <iostream>
# include <string>
using namespace std;
void TestString1 ( )
{
int a1;
int a2 = 10 ;
int a3 ( a2) ;
string s1;
string s2 ( "hello world" ) ;
string s3 ( 10 , '$' ) ;
string s4 ( s2) ;
string s5 = "hello world" ;
cin >> s1;
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
}
string类类型的对象可以直接进行操作(算数运算类的操作,是可以直接进行的)
string类对象的容量操作
第一个是size(重点),返回字符串有效字符的长度,不会算入\0的大小 第二个是length,返回字符串有效字符长度 第一个和第二个实际上是一样的,说了实际长度,其实就是不算\0的长度而已,用size和length求出来的结果其实是一摸一样的,没有任何的区别。
# include <iostream>
# include <string>
using namespace std;
void TestString2 ( )
{
string s1 ( "little bit,huge dream!!!" ) ;
cout << s1. size ( ) << endl;
cout << s1. length ( ) << endl;
cout << s1. capacity ( ) << endl;
if ( s1. empty ( ) )
cout << "空字符串" << endl;
else
cout << "不是空字符串" << endl;
s1. clear ( ) ;
if ( s1. empty ( ) )
cout << "空字符串" << endl;
else
cout << "不是空字符串" << endl;
}
int main ( )
{
TestString2 ( ) ;
return 0 ;
}
关于clear(); 上面的代码中,有一点是需要注意的,就是说,clear功能只是将字符串中有效字符清空,并不会去改变底层容量的大小,容量是并没有发生任何的变化的 需要注意的一点就是,在调用clear的时候,他只是会把空间中的内容清空掉,空间还是原来的那个空间,并且空间的大小并不会发生任何的变化
需要重点演示一下resize和reserve,去看看他们到底是有什么样子的区别
reserve
# include <iostream>
using namespace std;
void TestString3 ( )
{
string s ( "hello" ) ;
cout << s << endl;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
s. reserve ( 20 ) ;
cout << s << endl;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
s. reserve ( 10 ) ;
cout << s << endl;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
}
int main ( )
{
TestString3 ( ) ;
return 0 ;
}
通过运行结果,我们看出来,在使用了reserve之后,发生变化的只有capacity这个变量,其他的成员都没有发生变化 但是,现在来说的话,其实也看不出来到底reserve的功能是什么,那么现在,我们再来对代码进行一些修改
# include <iostream>
using namespace std;
void TestString3 ( )
{
string s ( "hello" ) ;
cout << s << endl;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
s. reserve ( 20 ) ;
cout << s << endl;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
s. reserve ( 40 ) ;
cout << s << endl;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
s. reserve ( 50 ) ;
cout << s << endl;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
s. reserve ( 40 ) ;
cout << s << endl;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
s. reserve ( 20 ) ;
cout << s << endl;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
s. reserve ( 10 ) ;
cout << s << endl;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
}
int main ( )
{
TestString3 ( ) ;
return 0 ;
}
修改之后的运行结果如下图所示: 那么,既然每一次都只有capacity在发生变化的话,那么就证明reserve这个方法是用来进行扩容的 ,那么,总结下来的话,其实就是: reserve这个东西其实就是用来进行扩容操作的,但是,只是进行扩容操作,并不会去改变有效元素的个数 reserve(n)—n只有比当前string类对象底层空间容量大的情况下才会真正的去扩容,而且一般情况下不会直接把容量给成n,都会比n稍微的大一些 reserve(n)—n小于当前stirng类的对象底层空间时(15),reserve会忽略本次的扩容操作,除非n小于15的时候 reserve方法一般只会把容量进行扩大,而不会将空间进行缩小的,因为本身的扩容操作的话,其实就需要申请空间,拷贝元素,释放旧空间,这样的一组操作它本身情况下就已经是挺复杂的操作了,那么,好不容易申请的空间就还是不要轻易的去把扩容之后的空间进行缩小的操作了吧,这种操作一来一去的成本就太高了,所以reserve一般只是将空间进行扩大,而不将空间进行缩小的操作。除非是小于15的情况,才会将空间去进行缩小的操作 ,那么问题又来了,为什么是15呢? string类为了提高性能,在其类中管理了一个长度为16个字符的数组,也就是说,其实string类内部包含了一个静态的数组,也就是说创建了一个string类的对象,除了有size,capacity,还有一个指针指向那个长度为16的数组,因为一般情况下都不会超过16个字符(当然也只是一般情况下),容量之所以显示的是15,因为还要空出来一个位置去存放\0的空间,所以显示出来的是15,如果说你要存放的东西它的长度是小于16的,那么就可以直接把你需要存放的东西,放在那个数组里面就可以了,就不需要额外的再去申请空间了,那么,其实就是可以提高效率的。
resize
下面的两个resize方法形成了重载 改变字符串的内容到n个字符 先给出下面的这一段代码,那么下面的这一段代码运行的结果是多少 由运行结果我们可以知道,下面的这段代码运行的结果是5和15
# include <iostream>
using namespace std;
void TestString4 ( )
{
string s ( "hello" ) ;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
}
int main ( )
{
TestString4 ( ) ;
return 0 ;
}
那么,我们现在对上面的代码做出调整 下面的这一段代码打印的结果是什么 通过运行下面的这一段代码,我们发现代码的运行结果是10和15
# include <iostream>
using namespace std;
void TestString4 ( )
{
string s ( "hello" ) ;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
s. resize ( 10 , '!' ) ;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
}
int main ( )
{
TestString4 ( ) ;
return 0 ;
}
那么,我们下载继续对上面的代码进行填充的操作 那么下面的代码,运行之后的结果又会是什么结果呢 通过运行可知,下面的多出来代码运行的结果是20和31,40和47 上面的代码之所以打印的结果是20和31,其中的结果只因为,在没有这一部分之前,string底层空间的容量是15个,那么我现在使用resize方法,要求stirng类里面的字符个数需要达到20个,但是现在底层的空间只有15个,那么就是需要去进行扩容的操作的,所以打印出来的结果是20和31,40和47是同样的道理
# include <iostream>
using namespace std;
void TestString4 ( )
{
string s ( "hello" ) ;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
s. resize ( 10 , '!' ) ;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
s. resize ( 20 , '$' ) ;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
s. resize ( 40 , '&' ) ;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
}
int main ( )
{
TestString4 ( ) ;
return 0 ;
}
那么,扩大的操作我们看完了,现在我们来看一看对resize后面的参数进行缩小的操作,代码又会有什么样的结果呢 下面代码中多出来的部分,运行的结果是30和47,20和47,10和47
# include <iostream>
using namespace std;
void TestString4 ( )
{
string s ( "hello" ) ;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
s. resize ( 10 , '!' ) ;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
s. resize ( 20 , '$' ) ;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
s. resize ( 40 , '&' ) ;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
s. resize ( 30 ) ;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
s. resize ( 20 ) ;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
s. resize ( 10 ) ;
cout << s. size ( ) << endl;
cout << s. capacity ( ) << endl;
}
int main ( )
{
TestString4 ( ) ;
return 0 ;
}
resize(newsize,ch):假设string类有效元素个数为size个,容量为capacity newsize<size:只是将有效元素的个数改变到newsize个就可以了,不会去缩小容量 ,容量已经扩了,就不会再去缩小了size<newsize<capacity:直接将有效元素的个数增加到newsize,多出的newsize-size,用ch进行填充,如果没有传递ch的话,就用0去填充 newsize>capacity:就要进行扩容了,申请空间,拷贝元素,释放旧空间然后需要将新元素的个数增加到newsize个,多出来的和上面的操作是一样的
注意
size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size() 。 clear()只是将string中有效字符清空,不改变底层空间大小。 resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。 reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数 ,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小,是去进行扩容的操作
容量改变
# include <iostream>
using namespace std;
void TestPushBack ( )
{
string s;
size_t sz = s. capacity ( ) ;
cout << "making s grow:\n" ;
for ( int i = 0 ; i < 100 ; ++ i)
{
s. push_back ( 'c' ) ;
if ( sz != s. capacity ( ) )
{
sz = s. capacity ( ) ;
cout << "capacity changed: " << sz << '\n' ;
}
}
}
int main ( )
{
TestPushBack ( ) ;
return 0 ;
}
上面代码的运行结果如下所示: 因为一边插入一边扩容效率太低了,所以我们选择在一开始的时候就进行扩容的操作,这样可以使得代码的效率提高一些
string类对象的访问及遍历操作
先来看运算符的重载,可以看到string类中运算符的重载也给出了两个函数,这两个函数形成了重载 普通的方法调用下面的那个函数,const的方法调用上面的那个带有const关键字的方法 我们先给出下标重载操作中的第二个函数的实际应用 先看一个打印的操作,可以使用iterator进行打印,也可以使用基于范围的for循环来进行打印操作
# include <iostream>
# include <string>
using namespace std;
void TestString5 ( )
{
string s1 ( "hello" ) ;
cout << s1[ 0 ] << endl;
s1[ 0 ] = 'H' ;
cout << s1 << endl;
for ( size_t i = 0 ; i < s1. size ( ) ; i++ )
{
if ( s1[ i] >= 'a' && s1[ i] <= 'z' )
{
s1[ i] -= 32 ;
}
}
string:: iterator it = s1. begin ( ) ;
while ( it != s1. end ( ) )
{
cout << * it << endl;
++ it;
}
cout << endl;
for ( auto c : s1)
{
cout << c << endl;
}
}
int main ( )
{
TestString5 ( ) ;
return 0 ;
}
打印的结果如下图所示: 从上面的代码,可以看出来的结论是:string类他是支持下标运算符的,从打印s1的[0]下标中的元素,就可以看出来string类是支持下标运算符号的 那么,我们现在先来简单看一下迭代器的原理到底是什么 但是像上面那样定义迭代器的方法其实是有些复杂的,我们可以换一种方法,我们可以使用auto来进行遍历,道理其实是一样的,方法如下图所示: 接下来,我们给出下标重载函数中第一个函数的相关操作,因为第一个函数实际上是重载了const相关的操作了的,那么,其实我们都知道,const类型的数据其实是不可以被修改的,如果下标重载函数没有对const相关的类型进行重载的话,那么我们或许其实还是可以去改变他的,但是他已经对const类型的东西进行了重载,那么我们就不可以去改变了,所以像下面这样的代码,在运行起来之后,实际上是会引起代码崩溃的问题出现的 所以说,像下面这种代码他是没有办法通过编译的
# include <iostream>
# include <string>
using namespace std;
void TestString6 ( )
{
string s1 ( "hello" ) ;
cout << s1[ 0 ] << endl;
s1[ 0 ] = 'H' ;
cout << s1 << endl;
for ( size_t i = 0 ; i < s1. size ( ) ; i++ )
{
if ( s1[ i] >= 'a' && s1[ i] <= 'z' )
{
s1[ i] -= 32 ;
}
}
string:: iterator it = s1. begin ( ) ;
while ( it != s1. end ( ) )
{
cout << * it << endl;
++ it;
}
cout << endl;
for ( auto c : s1)
{
cout << c << endl;
}
const string s2 ( "world" ) ;
s2[ 0 ] = 'W' ;
}
int main ( )
{
TestString6 ( ) ;
return 0 ;
}
operator[]和at rbeing和rend—其实就是逆向打印的操作 红色为正向的迭代器,蓝色为反向的迭代器
string类对象的修改操作
append有多种追加的方式,如下所示: operator+=也有多种方式,如下所示,下面的第三种方式其实和push_back是一个原理,就只是追加一个字符而已 push_back只能追加单个字符,但是,append和+=的操作就有很多种类型了
# include <iostream>
using namespace std;
void TestString7 ( )
{
string s;
s. push_back ( 'I' ) ;
s. append ( " " ) ;
string ss ( "Love" ) ;
s. append ( ss) ;
s += " " ;
s += "China" ;
cout << s << endl;
}
int main ( )
{
TestString7 ( ) ;
return 0 ;
}
# include <iostream>
using namespace std;
void TestString7 ( )
{
string s;
s. push_back ( 'I' ) ;
s. append ( " " ) ;
string ss ( "Love" ) ;
s. append ( ss) ;
s += " " ;
s += "China" ;
s += " " ;
s. push_back ( '!' ) ;
cout << s << endl;
s. insert ( s. find ( '!' ) - 1 , ":)" ) ;
cout << s << endl;
}
int main ( )
{
TestString7 ( ) ;
return 0 ;
}
那么,既然有在任意位置进行插入的操作,那么,肯定也有在任意位置进行删除的操作erase 下面这个erase不穿参数的话,就表明,你首先要去找到,然后从!开始删除,一直删除到末尾 npos如果没有给出的话,就会一直删除到末尾了,npos给的是size_t的-1的值,size_t的-1表示的就是无穷大的值了,所以pos如果没有给出的话,就是一直删除到末尾。 截取文件的后缀—用substr