【语法】C++的string
目录
4个默认成员函数
迭代器
string的扩容:
capacity():
reserve():
resize():
插入与删除:
c_str:
find()和substr:
getline():
在C语言中,要想存储一串字符,往往用的都是char arr[],也就是字符数组,而在C++中,引入了一个比字符数组更方便,可执行的操作也更丰富的类:string,可以把string归类到STL里面,但实际上string出现的要比STL早。本篇将介绍string类中常用的20几个接口
4个默认成员函数
string构造函数可以说是相当的丰富,每个都有不同的用法
//默认构造函数
string s1;
//其他构造函数
string s2("hello");//将s4初始化为hello
string s3(5,'a');//将s5初始化为5个'a',即"aaaaa" (不重要)
string s4("hello",1,3);//将s6初始化为“hello”中从第1个字符开始的后3个字符 (不重要)
string s5(s2,1,3);//将s6初始化为s2中从第1个字符开始的后3个字符 (不重要)
string s6("hello",3);//将s7初始化为“hello”的前3个字符 (不重要)
//拷贝构造函数
string s7(s4);
string s8 = s5;
//默认赋值运算符重载
s1 = "world";
cout << s1 << endl << s2 << endl << s3 << endl << s4 << endl << s5 << endl << s6 << endl << s7 << endl;
输出结果是
要特别注意的是,在string s5("hello",1,3)这样用时,如果最后一个参数给的值超过了原字符串的总数,就会使用“npos” 这个值来代替它
npos是string中的一个静态const变量,指的就是直到结束的位置,它的值是-1,但是仔细看下图就可以发现,该变量是size_t类型,size_t也就是unsigned int,是没有负数的,所以-1也就是正整数的最大值的意思
当我们想往string类的后面尾插字符或字符串时,可以用接口函数
s1.push_back('A');//尾插一个字符A
s1.append("edd");//尾插一个字符串edd
push_back()是尾插字符,append()是尾插字符串
可以看到append有很多种调用方法,和一开始讲到的构造函数的调用很相似,这里就不多讲述了(不重要)
但还有更简便的方法
s1+='A';//尾插一个字符A
s1+="edd";//尾插一个字符串edd
这是string类提供的operator+=运算符重载函数
可以看到有3个重载函数,分别用于字符,字符串(c-string)和string类 (c-string是指的以'\0'结尾的字符串)
迭代器
定义了一个string类后,如果想要遍历它,有三种方法
1. 用C语言阶段就学过的[]+下标的方式
string s1("Hello World");
for(size_t i=0;i<s1.size();i++)
{
cout << s1[i] << " ";
}
size()也是string的一个接口函数,会返回字符串的字符个数
其实还有一个接口函数叫length,这是早期string用来求字符个数的函数,和size一样,但后面STL出了之后也出了size,就用size比较多了
顺带一提,max_size()是用来求string类最多可以容纳的字符的,它的值是固定的
string s1;
cout << s1.max_size() << endl;
x86环境:2147483647 x64环境:9223372036854775807
2. 迭代器
string s1("Hello World");
string::iterator it = s1.begin();
while(it != s1.end())
{
cout << *it << " ";
it++;
}
string::iterator就是string类的迭代器类型,用这个类型定义的变量就是迭代器变量
begin()和end()也是string类中的接口函数,分别返回第一个字符的迭代器和最后一个字符的迭代器
可以看到begin和end都有两个函数,带const的和不带const的
其实迭代器也有分const和非const,上面演示的就是非const,如果在iterator的前面加上const_,即const_iterator,这就是const类型的迭代器
可以看到,如果it是const类型的迭代器,就不允许修改了,const迭代器存在的意义就是只让读不让写
而且,你如果是const类型的迭代器,再去调用begin和end函数,就会调用const类型的函数了
那如果想要从后往前遍历呢?还有个专门的反向迭代器,具体实现如下
string s1("Hello World");
string::reverse_iterator it = s1.rbegin();
while(it != s1.rend())
{
cout << *it << " ";
it++;
}
reverse_iterator就是反向迭代器,而rbegin就是reverse_begin的缩写,下面的rend也如此,既然迭代器变量是反向的,那就只能用rbegin和rend,如果用begin和end会报错
反向迭代器和const迭代器也可以组合,const_reverse_iterator就是const类型的反向迭代器
3.范围for
在C++11标准中,引入了一个新的遍历方法
string s1("Hello World");
string::iterator it = s1.begin();
for(auto a : s1)//范围for
{
cout << a << " ";
}
即定义一个auto类型的a,来自于s1,编译器会将a推演成s1的迭代器类型的“解引用形式”(即*iterator),a会自动++,直到遍历完为止
string的扩容:
在C语言中,如果想要像string一样存储字符串,又可以插入删除操作,就要malloc空间并不断扩容(参考C语言实现容器(例如stack,heap,seqlist等)),而在C++中也一样
capacity():
string s1("Hello World");
cout << s1.capacity() << endl;
输出结果为15,但是s1里面只有11个字符啊?
因为string的容量并不是和字符数一致的,string扩容的流程是申请一块更大的空间->将原数据复制到新申请的空间上->释放原来的空间。 如果每次插入字符都需要再去扩容,那效率消耗就太大了。所以capacity的初始值为15,只要你的字符数还在15以下,就不会扩容。
那如果到了16,capacity会是多少呢?
string s1("Hello World11111");
cout << s1.capacity() << endl;
会输出31
15到31,差不多2倍,真的是这样吗?
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';
}
}
这是一个用于检测capacity变化的程序
可以看到31后面是47而不是60,47后面不是90,但他们也并不是无规律可言,31*1.5=46.5, 47*1.5=70.5, 70*1.5是105,而且一般来说都会在1.5倍的基础上多开一个字符,用于存'\0'
(注:这是在VS环境下的倍增策略,如果是别的编译器,初始化时可能capacity和size相等)
reserve():
一般来说,capacity是随着size的大小来1.5倍扩容的,但你也可以在size==capacity之前就给capacity扩容好指定的值
string s;
s.reserve(100);
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';
}
}
reserve可以给capacity分配n个字符的空间,现在再执行,就会发现capacity不会再有变化了
需要注意的是,该函数也是有缺省值的,如果()里不给数,那默认就是0
resize():
既然capacity可以提前指定,那么size也是可以提前指定的
string s;
s.resize(100);
cout << "size:" << s.size() << endl << "capacity:" << s.capacity() << endl;
cout << s << endl;
可以看到size直接是100了,capacity也跟着增容了,但为什么最后输出s确是空的呢?
可以看到resize有两个重载,我们刚才使用的就是第一种方式,第二种方式后面需要跟一个字符
string s;
s.resize(100,'A');
cout << "size:" << s.size() << endl << "capacity:" << s.capacity() << endl;
cout << s << endl;
这次我给了resize第2个参数'A',可以看到成功的输出了100个A,如果不给默认就是'\0'。也就是说,如果直接resize(100)的话,就是存了100个'\0'在s里面
下面这段代码会输出什么?
string s("Apex Legends");
s.resize(5);
cout << "size:" << s.size() << endl;
cout << s << endl;
s.resize(15,'S');
cout << "size:" << s.size() << endl;
cout << s << endl;
可以看到,如果resize的值比原来size要小,就会把多余的删掉
如果本来字符串中有值,resize了一个更大的空间,原来的值也会保留。
插入与删除:
string里也可以和别的数据结构一样插入和删除
这是string的插入接口函数,可以看到一共有6个重载一个模板,但实际只需要记住两种用法就可以了
string s("Apex Legends");
s.insert(4, "Rainbow");//在下标为4的地方插入"Rainbow"(只能插入字符串)
cout << s << endl;
s.insert(s.begin(), 'L');//在最前面插入一个字符'L'(只能插入单个字符)
cout << s << endl;
s.insert(5, "Zelda", 3);//在下标为5的地方插入"Zelda"的前3个字符(只能插入字符串)(了解即可)
cout << s << endl;
string s("Apex Legends");
s.erase(2, 3);//在下标为2的地方删除3个字符
cout << s << endl;
s.erase(2);//如果不给第2个参数,缺省值是npos,也就是把下标为2以后的字符都删除
cout << s << endl;
第一个erase上两个参数都有缺省值,如果第2个参数不给,就代表删除从pos开始的所有字符,如果第1个参数也不给,就代表删除整个字符串
c_str:
string其实和C语言中的字符串还是不一样的,我们来看下面代码
string s1("Hello ");
s1+='\0';
s1+="World";
cout << s1 << endl;
cout << s1.c_str() << endl;
可以看到在给s1初始化为Hello了之后,又给后面加了\0World,那这个时候输出的结果是
Hello World
Hello
第一行输出的是完整的这就是c_str的作用了,可以将string转换成c语言类型的字符串进行输出,即char*,而C语言的字符串是以\0作为结束标志,所以只会输出到Hello,而cout会输出string类中的全部字符,所以\0后面的也会输出
find()和substr:
顾名思义,是用来找字符的,返回的是该字符所在的下标 ,如果没有找到,就返回npos(string能表示的最大字符长度)
string s1("text.cpp");
string s2("string.c");
string s3("string.txt");
如果现在让你分别输出这三个字符串中的文件格式后缀,就可以先用find()找到.所在的位置,然后再输出.以及以后的字符
那怎么样输出.以及以后的字符呢
substr()就可以输出子串, 第一个参数是定义从下标多少开始输出,第二个参数则是定义输出多少个
string s1("text.cpp");
string s2("string.c");
string s3("string.txt");
size_t pos1 = s1.find('.');
if(pos1 != string::npos)//先确保找到了,再输出
cout << s1.substr(pos1) << endl;
size_t pos2 = s2.find('.');
if(pos2 != string::npos)
cout << s2.substr(pos2) << endl;
size_t pos3 = s3.find('.');
if(pos3 != string::npos)
cout << s3.substr(pos3) << endl;
那如果是text.cpp.rar这种呢?要怎么找到rar而不是cpp.rar?
这时候就要拿出另外一个函数了
rfind也就是reverse_find的意思,就是从后往前找
string s1("text.cpp.rar");
string s2("string.c.txt");
string s3("string.txt.c");
size_t pos1 = s1.rfind('.');
if(pos1 != string::npos)//先确保找到了,再输出
cout << s1.substr(pos1) << endl;
size_t pos2 = s2.rfind('.');
if(pos2 != string::npos)
cout << s2.substr(pos2) << endl;
size_t pos3 = s3.rfind('.');
if(pos3 != string::npos)
cout << s3.substr(pos3) << endl;
把之前的find换成rfind就可以了
string url("https://cplusplus.com/reference/string/string/rfind/");
现在需要把这个url的协议,域名和资源名称分别分离出来换行输出,就可以用到上面学到的find和substr来完成
string url("https://cplusplus.com/reference/string/string/rfind/");
size_t pos1 = url.find(':');
if(pos1 != string::npos)
cout << url.substr(0,pos1) << endl;//输出协议
size_t pos2 = url.find('/',pos1+3);
if(pos2 != string::npos)
cout << url.substr(pos1+3,pos2-(pos1+3)) << endl;//输出域名
cout << url.substr(pos2+1);//输出资源名称
getline():
在讲getline之前,我们先来看一下这道题
int main()
{
string s1;
cin >> s1;
int pos = s1.rfind(' ');//总后往前找空格,就能找到最后一个单词的开头
//如果只输入了一个单词,rfind找不到空格,就会返回-1(npos的值)
cout << s1.size()-(pos+1);//pos+1就是截止到最后一个空格时的长度(pos=-1时也适用)
return 0;
}
乍一看没什么问题,但当程序运行起来,如果只输入了一个单词,那结果还是可以正常输出的,但只要是输入了超过一个单词的字符串,不管怎么样,它输出的长度都是第一个单词的长度,这是怎么回事?
在C语言里,我们都知道scanf在输入时是以空格,tab或enter作为结束符,cin其实也一样,所以在输入了第一个单词并且空格后,后面的单词就没有意义了。
在C语言中要想解决这个问题,一般都会用gets(已经被废除)或fgets,在C++中则是用getline,它在输入时只会以换行符作为结束标志
int main()
{
string s1;
getline(cin,s1);
int pos = s1.rfind(' ');//总后往前找空格,就能找到最后一个单词的开头
//如果只输入了一个单词,rfind找不到空格,就会返回-1(npos的值)
cout << s1.size()-(pos+1);//pos+1就是截止到最后一个空格时的长度(pos=-1时也适用)
return 0;
}
只需要把第二行的cin换成getline这种格式,就可以读取空格了