Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!
我的博客:<但凡.
我的专栏:《编程之路》、《数据结构与算法之美》、《题海拾贝》、《C++修炼之路》
欢迎点赞,关注!
1、string类初识
<string>
是 C++ 标准库中用于处理字符串的头文件。我们可以用string提供的丰富的接口来实现各种各样的字符串操作。string类的底层其实就是顺序表,只不过在存储字符的同时还多存储了一个‘\0’。再后续的使用讲解中大家会感受到string的强大。
使用string类需要包含头文件:
#include<iostream>
#include<string>
好的现在我尽量用最直白的语言告诉大家为什么这个string类非常方便。
#include<iostream>
#include<string>
using namespace std;
int main()
{string s="Hello World!";cout << s;
}
我们创建了一个helloworld字符串,并且把他打印了出来。现在的字符串就像int, char一样是一个类型,我们不需要再去像C语言一样再去创建字符数组啊什么什么的。但是不仅如此,string类提供了非常多的接口,所以我们可以随便的在这个字符串中增删查改。具体怎么增删查改,我们继续往下看。
2、迭代器
我们可以把迭代器理解成和指针差不多的东西。因为我们想访问它都得解引用。
对于迭代器类型的接收我们可以使用auto,auto可以自动识别变量的类型。但需要注意的是auto这个东西不推荐多用,因为很可能会识别错误。
auto不能作为函数的参数,可以做返回值,但是建议谨慎使用 。auto不能直接用来声明数组
这些都是string类中返回迭代器的接口,我们一个一个来介绍:
2.1、begin和end
begin是返回该字符串中第一个字符位置的迭代器,end是返回该字符串最后一个字符位置的下一位的迭代器(也就是‘\0’的位置)。我们可以使用begin和end对字符串进行遍历:
#include<iostream>
#include<string>
using namespace std;
int main()
{string s="Hello World!";auto it1 = s.begin();auto it2 = s.end()-1;while (it1 != it2){cout << *it1 << " ";it1++;}
}
输出结果:
注意我们这里没让他输出感叹号!
有几个细节需要提一下:第一,我们的迭代器是支持+-操作的,+就是让迭代器挪到下一个位置,-与+相反。
第二,我们迭代器的比较其实是非常严格的,所以尽量使用 != 和== 来比较。
2.2、rbegin和rend
这个rbegin和rend就有意思了啊,它实际上就是和begin和end进行了一个对调,rbegin返回的是整个字符串最后一个字符的位置,而rend返回的是第一个字符的位置的前一个。但需要注意,++begin其实是往前走,向前遍历。也就是说我们现在从字符串最后向前是正方向。
#include<iostream>
#include<string>
using namespace std;
int main()
{string s="Hello World!";auto it1 = s.rbegin();auto it2 = s.rend();while (it1 != it2){cout << *it1 << " ";it1++;}
}
需要注意一点,我们不能执行rbegin-1的操作,因为rbegin-1这个位置虽然物理内存上是存在的,但是对于反向迭代器来说是不合法的,属于越界访问。
2.3、cbegin、cend、crbegin、crend
这四个迭代器其实和上面我们介绍过的四个指向的位置没有区别,但是他们迭代器的类型变成了const_iterator。也就是说,我们迭代器指向的这一块内存存放的内容是不可以被修改的。
#include<iostream>
#include<string>
using namespace std;
int main()
{string s = "Hello World!";auto it1 = s.begin();(*it1)++;auto it2 = s.cbegin();(*it2)++;//报错:表达式必须是可修改的左值
}
实际上后面四个我们不常用。
如果我们在不使用auto的情况接收以下两种情况的迭代器,必须使用const_iterator:
void test(const string& s)
{string::const_iterator it = s.begin();string p = "asd";string::const_iterator it2 = p.cbegin();
}
注意以下两串代码表达的意思并不相同,第一个是指迭代器的指向不能改变,也就是不能执行it2++这样的操作,而第二个是迭代器的指向的内容不能被修改。
const string::iterator it2 = p.begin();string::const_iterator it = p.begin();
3、capacity
以下是我们这一部分要介绍的接口:
3.1、size、length和max_size
其实这两个接口都是返回字符串的长度。size()是为了和后面STL里面其他容器的接口一致而后设计出来的。我们一般使用size,不使用length。
max_length就是返回我们这个字符串能够容纳的最大长度。
#include<iostream>
#include<string>
using namespace std;int main()
{//两种定义string的方式string s = "Hello World!";//隐式类型转换string s1("abc");cout << s.size() << endl;//包含空格cout << s1.size()<< endl;cout << s1.length() << endl;cout << s.max_size() << endl;//输出2147483647
}
输出结果:
3.2、resize
resize可以将字符串大小调整为 n 个字符的长度。
如果n小于当前字符串的长度,就会对该字符串进行截取,如果n大于当前字符串的长度,多余的位置用我们指定的字符串进行补充。如果没有指定就用\0补充。
#include<iostream>
#include<string>
using namespace std;int main()
{//两种定义string的方式string s = "Hello World!";//隐式类型转换s.resize(5);cout << s << endl;s.resize(50);cout << s << endl;s.resize(100, 'c');cout << s << endl;
}
输出结果:
3.3、capacity
capacity可以返回当前为字符串分配的存储空间的大小,以字节表示。
#include<iostream>
#include<string>
using namespace std;int main()
{string s = "Hello World!";cout << s.capacity() <<endl;
}
输出结果:
现在我们可以把capacity用来观察编译器对string容量的自动扩容。
#include<iostream>
#include<string>
using namespace std;int main()
{string s = "Hello World!";for (int i = 1;i <= 10;i++){s += "ssssssssss";//每次尾插10个字符cout << s.capacity() << endl;}
}
输出结果:
3.4、reserve
reserve根据计划的大小更改调整字符串容量,长度最多为 n 个字符。如果我们给的容量小于当前容量,他并不会缩小容量。这意味着他不会改变当前字符串的内容。但我说的不会缩小是针对我是用的编译器vs2022来说的,不同环境下可能有不同的结果,可能缩也可能不缩。
但需要注意的是,如果给的大小大于当前的容量,他也不会严格按照你给的容量进行扩容,由于底层内存对齐规则和编译器的优化(这不在讨论的范围内),他只会大于或等于你要求他扩容到的大小。
#include<iostream>
#include<string>
using namespace std;int main()
{string s = "Hello World!";cout << s.capacity() << endl;//15s.reserve(5);cout << s.capacity() << endl;//15s.reserve(20);cout << s.capacity() << endl;//31s.reserve(40);cout << s.capacity() << endl;//47
}
输出结果:
所以说这个东西大家尽量别用,很可怕,用户对于容量的可控性很低。
3.5、clear和empty
clear可以情况当前串,使它变成空串,而empty可以判断当前串是不是空串。
#include<iostream>
#include<string>
using namespace std;int main()
{string s = "Hello World!";cout << s.empty() << endl;//0s.clear();cout << s.empty() << endl;//1
}
输出结果:
4、Element access
这里我们介绍几种接口用来访问元素。
4.1、方括号
首先是最常用的方括号访问元素。这里的访问我们就把字符串看成一个下标从0开始的字符数组就好。
#include<iostream>
#include<string>
using namespace std;int main()
{string s = "Hello World!";cout << s[0] << endl;cout << s[12] << endl;//访问'\0
}
输出结果:
4.2、at
at也可以访问特定下标的元素:
#include<iostream>
#include<string>
using namespace std;int main()
{string s = "Hello World!";cout << s[0] << endl;cout << s[12] << endl;//访问'\0cout << s.at(0) << endl;cout << s.at(4) << endl;
}
输出结果:
需要注意,方括号访问越界是直接断言报错,而at访问越界是抛异常(我们之前介绍过异常)。但是只有debug版本才会断言报错,realease版本断言就被忽略了。
4.3、back和front
这两个C++11新增的接口就是返回字符串首尾元素,没啥好说的、
#include<iostream>
#include<string>
using namespace std;int main()
{string s = "Hello World!";cout << s.front() << endl;cout<<s.back()<<endl;
}
输出结果:
5、Modifiers
这里我们介绍几个修改我们字符串的接口。
5.1、+=,insert和erase
之所以先介绍这三个,是因为这三个使我们最常用的。
首先来说+=,这个+=就是运算符重载了,可以支持我们的字符串相加:
#include<iostream>
#include<string>
using namespace std;int main()
{string s = "Hello";s += " World!";cout<<s<<endl;
}
输出结果:
接下来再来看insert,他可以支持我们在字符串中任意位置插入数据:
我们可以看一下,他支持了这么多函数重载。 现在我们挑几个来实验一下:
#include<iostream>
#include<string>
using namespace std;int main()
{string s = "Hello";s.insert(0, 1, '*');//在下标为0,传入1个字符,内容为*cout<<s<<endl;s.insert(s.begin(), '&');//在begin迭代器指向的位置插入&cout << s << endl;s.insert(5, "pppppp");//从下标为5的位置插入字符串//注意下标为5的位置被改变了cout << s << endl;
}
输出结果:
erase支持我们在指定的位置进行删除。
使用案例:
#include<iostream>
#include<string>
using namespace std;int main()
{string s = "Hello";s.erase(s.begin());//删掉迭代器指向位置的cout << s << endl;s.erase(0, 2);//从下标0开始,删掉两个字符cout << s << endl;s.erase(0, string::npos);//从下标0开始,删掉npos个字符,实际上就删完了,我们后面会介绍nposcout << s << endl;s += "yyy";s.erase(s.begin(), s.end());//删除所有字符cout << s << endl;
}
输出结果:
5.2、append
append可以在字符串末尾追加字符串。他既可以追加string字符串,也可以追加c风格字符串还可以追加多个字符,甚至还支持追加迭代器范围内的字符。
#include<iostream>
#include<string>
using namespace std;int main()
{string s = "Hello";s.append("ssss");//追加c风格string s1 = "***";s.append(s1);s.append(10, '@');//追加多个字符s.append(s.begin(), s.end());//相当于把这个字符串复制一遍加上去cout << s << endl;
}
输出结果:
5.3、push_back
这个也是尾插,没啥好说的。 但是这个只能插入单个字符。
#include<iostream>
#include<string>
using namespace std;int main()
{string s = "Hello";s.push_back('H');cout << s << endl;
}
5.4、assign
assign可以以多种方式替换字符串的内容。
使用案例:
#include<iostream>
#include<string>
using namespace std;int main()
{string s = "Hello";string s1("ppp");s.assign(s1);//另一串赋值cout << s << endl;s.assign("sssss");//c风格字符串cout << s << endl;s.assign(10,'o');//多个字符cout << s << endl;s.assign("*********", 2, 4);//从给定的string串下标2开始以后的4个字符进行替换cout << s << endl;string s2 = "666";s.assign(s1.begin(), s1.end());//迭代器cout << s << endl;
}
输出结果:
5.5、replace
这个replace也是替换,但是他更灵活,我们可以自己指定需要替换的范围。在这我就简单列举几个例子:
#include<iostream>
#include<string>
using namespace std;int main()
{string s = "Hello";string s1("ppp");s.replace(2,3,s1);//从2开始替换3个字符为s1cout << s << endl;s.replace(2, 3,"sssss");//c风格字符串cout << s << endl;s.replace(2,10,5,'o');//从2开始以后10个字符替换为5个'o’cout << s << endl;
}
输出结果:
replace要谨慎使用因为如果替换的长度不对等他需要自己挪动数据,消耗时间。
5.6、swap
这个就是单纯的替换,没啥好说的。但需要注意的是这个交换是string特有的只能交换字符串的浅拷贝,效率比算法库自带的swap更高。下一篇我们还会展开说这个地方。
#include<iostream>
#include<string>
using namespace std;int main()
{string s = "Hello";string s1("ppp");s.swap(s1);cout << s << endl;
}
5.7、pop_back
pop_back可以删除最后一个字符 (注意不是‘\0’)。
#include<iostream>
#include<string>
using namespace std;int main()
{string s = "Hello";s.pop_back();cout << s << endl;
}
输出结果:
好了这部分也算是讲完了。大家应该可以感受得到,实际上string类在这地方设计的还是比较冗余的,有很多功能都是重复的。
6、string operations
6.1、c_str、data和copy
这个主要是让我们的string类型字符串以c风格返回。
#include<iostream>
#include<string>
using namespace std;int main()
{string s = "Hello";cout << s.c_str() << endl;
}
输出结果:
data的作用和c_str类似,但是在不同的标准中有一些区别。这个不过多说了因为不常用。
copy可以将string串中的字符拷贝到字符数组中,但是必须手动添加终止符:
#include<iostream>
#include<string>
using namespace std;int main()
{string s = "Hello";char s1[100] = " ";s.copy(s1,s.size());s1[5] = '\0';cout << s1 << endl;
}
输出结果:
6.2、find和rfind
这个是比较常用的,他可以支持我们以多种方式查找string串中出现的字符或字符串。
#include<iostream>
#include<string>
using namespace std;int main()
{string s = "Hello***&&&@@";cout << s.find('*', 0) << endl;//5cout << s.find("*&", 0) << endl;//支持C风格字符串查找cout << s.find("9", 0) << endl;//未找到返回nposcout << s.find("*&#",0,2) << endl;//指定要查找我给定的C风格字符串的前两个字符
}
输出结果:
对于find来说,只要没找到,就返回npos,也就是字符串能容纳下的最大长度。
rfind就是倒着往前找,但需要注意的是如果我们想让他查找完全整个串的话我给给定的应该是从最后一个字符得下标开始查找:
#include<iostream>
#include<string>
using namespace std;int main()
{string s = "Hello***&&&@@";cout << s.rfind('*', 12) << endl;cout << s.rfind("*&", 12) << endl;//支持C风格字符串查找cout << s.rfind("9", 12) << endl;//未找到返回nposcout << s.rfind("*&#",12,2) << endl;//指定要查找我给定的C风格字符串的前两个字符
}
输出结果:
6.3、find_first_of和find_last_of
find_first_of可以查找整个串中从前往后第一次出现这个字符或者给定的字符串中任意一个字符的位置。他经常用于检验这个字符串中是否包含某个特定字符。他和find的区别就是,就算给定的串与string串不完全匹配也没关系,只要包含其中的一个字符就好。
#include<iostream>
#include<string>
using namespace std;int main()
{string s = "Hello***&&&@@";cout << s.find_first_of('*', 0) << endl;cout << s.find_first_of("*&", 0) << endl;//支持C风格字符串查找cout << s.find_first_of("9", 0) << endl;//未找到返回nposcout << s.find_first_of("*&#", 0,2) << endl;//指定要查找我给定的C风格字符串的前两个字符
}
输出结果:
find_last_of就是和find_first_of反过来到这往前找而已,没啥好说的。
6.4、find_first_not_of和find_last_not_of
这两个其实就是字面意思,查找第一个不是该字符或字符串的位置。一个是从前往后找,一个是从后往前找。
#include<iostream>
#include<string>
using namespace std;int main()
{string s = "Hello***&&&@@";cout << s.find_first_not_of('H', 0) << endl;//cout << s.find_first_not_of("*&", 0) << endl;//支持C风格字符串查找cout << s.find_first_not_of("9", 0) << endl;//未找到返回nposcout << s.find_first_not_of("*&#", 0,2) << endl;//指定要查找我给定的C风格字符串的前两个字符
}
输出结果:
6.5、substr
substr可以截取规定区域的字符串。但需要注意的是substr并不会改变原字符串。
#include<iostream>
#include<string>
using namespace std;int main()
{string s = "Hello***&&&@@";cout << s.substr(0,5) << endl;
}
输出结果:
6.6、compare
compare就是字符串比较,和C语言里面的strcmp差不多的道理,都是字典序进行排序。
#include<iostream>
#include<string>
using namespace std;int main()
{//大写字母ASCII码比小写字母小//按照字典序排序string s = "Hello";cout << s.compare("Assaa") << endl;string s1 = "Hello";cout << s.compare(s1) << endl;cout << s.compare(0, 3, s1) << endl;//s的0到3个字符和s1作比较cout << s.compare(0, 3, s1, 0, 3) << endl;//s的0到3个字符和s1的0到3个字符作比较}
输出结果:
返回1表示s大于我们传入的字符串,0表示相等,-1表示s小于我们传入的字符串。
7、getline
getline就是读取当前这一行。什么意思?我们的scanf和cin都不能读取包含有空格的字符串,所以我们得用getline读取这一行能够读取空格。
#include<iostream>
#include<string>
using namespace std;int main()
{//输入hello worldstring s;cin >> s;//hellocin.ignore(256, '\n');//刷新缓冲区,把剩下的world清掉string s2;getline(cin,s2);cout << s << endl;cout << s2 << endl;
}
输出结果:
好了,今天的内容就分享到这,我们下期再见!