文章目录
- 一、String的基本使用
- 二、String构造相关的成员函数
- 1.String构造函数
- 2.String析构函数
- 3.operator=运算符重载
- 4.String增删查改之增
- 5. operator[]运算符重载
- 三、String迭代器
- 1.迭代器介绍
- 2.string中迭代器的作用
- 3.迭代器跟容器结合
- 4.反向迭代器
- 5.const修饰的迭代器
- 四、String容量相关的成员函数
- 1.size和length
- 2.max_size
- 3.capacity
- 4.clear
- 5.reserve
- 6.resize
- 7.shrink_to_fit
- 五、一些经典的力扣题
- 1.仅仅反转字母
- 2.字符串相加
- 六、String其他接口
- 1.operator[]操作符
- 2.at
- 3.assign
- 4.insert
- 5.erase
- 6.replace
- 7.c_str
- 8.find
- 9.substr
- 10.rfind
- 11.find_first_of
- 12.find_last_of
- 13.relational operators
- 14.operator+
- 15.getline
- 16.string转为其他类型
- 17.其他类型转化为string类型
- 七、string与模板
一、String的基本使用
当我们想要使用string库的时候,我们需要先包上头文件string。然后还需要展开命名空间,如果没有展开,就需要指定了,如下所示是string类的一些基本用法,我们可以注意到与普通类的用法还是比较一致的。
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s1;
std::string s2;
std::string name("张三");
name = "张飞";
return 0;
}
二、String构造相关的成员函数
在这里,我们就需要去查看文档了。我们使用这个网站来进行查阅cplusplus.com/我们,直接搜索String即可
我们可以注意到下面有这个类的成员函数的使用方法
在string类中,大概有100多个成员函数,我们不可能全部记住,所以我们只需要掌握最常用的即可,剩下的我们需要的时候查文档即可
1.String构造函数
我们进入文档的构造函数部分,我们发下有七个构造函数
我们可以注意到:
- 第一个是无参的默认构造函数
- 第二个是拷贝构造
- 第四个是用一个c语言的字符串去构造
- 第六个是使用使用一个字符,用n个字符去构造
如上四个是比较常用的构造函数。他们的使用如下所示:
int main()
{
//无参的默认构造
string s1;
//用字符串去构造
string s2("hello world");
//用n个字符去构造
string s3(10, '*');
//拷贝构造
string s4(s2);
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
return 0;
}
如下所示为运行结果,在上面这段代码中,我们可以注意到流插入运算符可以直接运用于string类,这是因为流插入运算符已经在库里面被重载过了。除此之外,我们也有< ,>,==等运算符的重载,这些运算符重载的比较规则与C语言的strcmp是一样的。根据ASCII码来进行比较。但是如果要使用流插入等进行打印的时候,注意优先级
我们再来看一些不常用的几个函数:
- 第三个构造函数的功能是拷贝构造的一个版本,pos是某个的下标,将对象的pos这个下标开始,将len长度的字符串拷贝构造给一个新对象。我们也可以注意到len有一个缺省参数,这个缺省参数是npos,它的值是-1。但由于它接收的是无符号的数,所以这个-1会变得非常之大。可以认为是如果胜利了len,就从pos位置拷贝构造完这个函数的一个应用就是切割字符串,如下所示,当然下面的方法比较笨拙需要我们手动算位置。我们未来可以来自动搜索位置
- 第五个函数的作用是拷贝字符数组的前n进行类对象的初始化
- 第七个函数是涉及到迭代器的使用,我们在后文在详细了解
2.String析构函数
如下所示是析构函数的的使用说明,事实上析构函数我们是不需要自己去管的。因为它生命周期结束的时候自动调用
3.operator=运算符重载
如上所示,这个运算符重载有三个函数,他们由于参数不同又构成了函数重载。
- 第一个的作用是用一个对象运算符重载赋值给一个对象。
- 第二个的作用是用一个字符串去赋值一个对象
- 第三个是用一个字符去赋值一个对象
但是我们其实可以注意到,第二个和第三个其实没有也是可以的。因为编译器又隐式类型转换,它会自动用这个字符串或者字符去构造一个类对象,然后将这个类对象去拷贝构造给一个对象。但是编译器优化后就直接将构造+拷贝构造优化为了构造
4.String增删查改之增
我们创建好一个对象后,我们需要对其进行增删查改等操作。
当我们想要在一个对象后面增加一个字符的时候,我们使用push_back函数
当我们想要增加一个字符串的时候,我们使用append这个函数
这里我们要注意的是,append虽然有很多成员函数,但我们最喜欢最常用追加字符串的那个函数
int main()
{
string s1("hello");
s1.push_back(' ');
s1.append("world");
cout << s1 << endl;
return 0;
}
还需要注意的是,虽然c语言中有strcat这个也是追加字符串的函数,但是使用string的效率更高。而且还会涉及空间不够的问题,而string不涉及空间不够的问题,空间不够直接扩容即可,因为string的本质其实就是一个顺序表,它在类里面的成员变量大致如下
class string
{
public:
private:
char* _str;
size_t _size;
size_t _capacity;
};
虽然push_back和append很好,但是还有一个函数是operator+=,我们更喜欢使用它
它有三个函数重载,它可以加一个类对象,加一个字符串,加一个字符
事实上operator+=的内部就是复用push_back和append实现的。
但是append并不能完全被替代掉,我们继续观察以下append的函数重载有哪些
我们发现它与构造函数是非常相似的。功能也是类似的
- 第一个是追加一个对象
- 第二个是将对象第subpos位置开始,后面sublen个字符追加到一个对象上去
- 第三个是追加一个字符串
- 第四个是追加字符串的前n个
- 第五个是追加n个c字符
5. operator[]运算符重载
这个运算符重载是比较强大的,在c语言中,它就是一个解引用。在string中也可以实现类似的功能,比如说当我们想要遍历一个string对象的时候,我们就可以利用这个下标和operator[]运算符重载实现,注意这个size成员函数用于计算对象有效字符的个数的。
int main()
{
string s1("hello world");
cout << s1 << endl;
for (size_t i = 0; i < s1.size(); i++)
{
cout << s1[i];
}
cout << endl;
return 0;
}
我们知道size成员函数是计算有效字符的个数的,而\0字符并不是有效字符,他是一个特殊的标识字符。所以第二次遍历的时候并不会遍历到\0
而如果我们遍历的时候在size函数后+1,是可以访问到\0的,但是编译器不会显示的打印出来,所以打印结果还是原来的样子
我们还注意到,operator[]返回的是引用,所以是可以直接进行修改的。由此我们有了下面的代码
这样一来string就可以像数组一样玩了。
但是需要注意的是,它与数组的解引用是具有天差地别的,底层是完全不一样的
三、String迭代器
1.迭代器介绍
迭代器是一个像指针一样的东西
我们知道,string类的底层是这样的一个顺序表。这样就可来进行增删查改
而迭代器就是增加了一种访问的方式,这里要注意,迭代器只是像指针的一样的东西,而不一定是指针,我们可以暂且理解为指针。
我们来分析下面的代码,注意迭代器的使用必须是string::iterator。
这段代码中,begin和end可以指向的的是起始的位置,和末位置的后一个位置,由于末位置是最后一个字符,所以end指向的是'\0'字符
int main()
{
string s1("hello world");
cout << s1 << endl;
string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << ' ';
++it;
}
cout << endl;
return 0;
}
注意的是iterator是一个像指针的类型,有可能是指针,有可能不是指针
如下代码所示,它可以实现类似指针的功能,修改所指向的空间,也就是说迭代器可以读写数据
int main()
{
string s1("hello world");
cout << s1 << endl;
string::iterator it = s1.begin();
while (it != s1.end())
{
(*it)--;
it++;
}
it = s1.begin();
while (it != s1.end())
{
cout << *it << ' ';
++it;
}
cout << endl;
return 0;
}
2.string中迭代器的作用
我们知道,使用operator[]运算符也可以实现读写,使用迭代器也可以实现读写。在string中我们起始更喜欢使用运算符重载来读写,因为比较简洁。那么使用迭代器有什么意义呢?
我们知道,我们有时候在读的时候,也喜欢使用范围for来解决。
for(auto& c : s1)
{
c++;
}
for (auto c : s1)
{
cout << c << ' ';
}
cout << endl;
范围for实际上底层就是使用迭代器来实现的,底层直接替换为迭代器
所以说实际上并没有什么范围for,它只是一层伪装罢了,没有迭代器就不支持范围for,一个类只要支持迭代器就支持范围for,我们已经知道数组和string都是支持范围for的,而栈是没有迭代器的,所以自然不支持范围for,数组支持迭代器是因为指针就是天然的迭代器。
迭代器的一大好处就是任何容器都支持迭代器,并且用法是类似的
我们可以看下面的程序,虽然我们可能暂时不会写出来,但是还是能读懂的
#include<vector>
#include<list>
int main()
{
vector<int> v;
v.push_back(10);
v.push_back(20);
v.push_back(30);
v.push_back(40);
vector<int>::iterator vit = v.begin();
while (vit != v.end())
{
cout << *vit << ' ';
vit++;
}
cout << endl;
list<int> lt;
lt.push_back(50);
lt.push_back(60);
lt.push_back(70);
lt.push_back(80);
list<int>::iterator lilt = lt.begin();
while (lilt != lt.end())
{
cout << *lilt << ' ';
lilt++;
}
cout << endl;
return 0;
}
对于链表和树形结构就无法使用operator[]了。但是迭代器仍然可以使用。
总结:iterator提供了一种统一的方式访问和修改容器的数据
3.迭代器跟容器结合
我们容器中的数据都是私有的。我们无法访问的,但是如果想要实现算法的话,就必须访问数据,为了访问数据就需要使用迭代器了。因为iterator提供了统一的访问方式和修改容器的数据。
我们知道数组的逆置和链表的逆置是不一样的,但是库里面提供了统一的算法接口。
算法的库名称为<algorithm>
我们可以来看一下库里面的reverse函数,可以看到他是需要传入两个迭代器的,这个迭代器的区间为左闭右开,刚好就是我们迭代器的begin和end
我们来应用一下
#include<vector>
#include<list>
#include<algorithm>
int main()
{
vector<int> v;
v.push_back(10);
v.push_back(20);
v.push_back(30);
v.push_back(40);
vector<int>::iterator vit = v.begin();
while (vit != v.end())
{
cout << *vit << ' ';
vit++;
}
cout << endl;
list<int> lt;
lt.push_back(50);
lt.push_back(60);
lt.push_back(70);
lt.push_back(80);
list<int>::iterator lilt = lt.begin();
while (lilt != lt.end())
{
cout << *lilt << ' ';
lilt++;
}
cout << endl;
reverse(lt.begin(), lt.end());
reverse(v.begin(), v.end());
cout << "reverse_vector:";
vit = v.begin();
while (vit != v.end())
{
cout << *vit << ' ';
vit++;
}
cout << endl;
cout << "reverse_list:";
lilt = lt.begin();
while (lilt != lt.end())
{
cout << *lilt << ' ';
lilt++;
}
cout << endl;
return 0;
}
运行结果为
需要注意的是,我们的顺序表和链表虽然看上去用的是同一个函数,但其实并不是同一个函数,它调用的是函数模板。用函数模板自动生成一个函数。
我们在上面提到过,范围for起始就是迭代器。所以我们可以使用范围for来进行遍历一下数据
如果我们需要排序的话,可以直接用sort,而不需要使用qsort了,可以看到sort有两个函数重载,我们现在只考虑第一个函数重载,顺序表可以直接使用第一个进行排序,链表不能用第一个。可以看到,只需要传左闭右开的迭代器就可以进行排序了。
总结:算法可以通过迭代器,去处理容器中的数据
4.反向迭代器
有时候我们还需要反向遍历容器中的数据,这时候就需要我们使用反向迭代器了,反向迭代器的类型是在迭代器前面加上reverse,同样的调用begin和end也变成了rbegin和rend,并且和正向迭代器一样,是左闭右开的
int main()
{
string s1("hello world");
string::reverse_iterator rit = s1.rbegin();
while (rit != s1.rend())
{
cout << *rit << ' ';
rit++;
}
cout << endl;
return 0;
}
我们有时候会觉得迭代器的类型太过于繁琐,我们会直接使用auto来进行代替
和正向迭代器类似,反向迭代器也可以直接修改容器的数据
5.const修饰的迭代器
当我们写出如下代码的时候,我们发现报错了
这里其实涉及到一个权限放大的问题
我们为了使得传参的代价小一点,不去调用拷贝构造,故而使用了引用,但又因为我们函数内并未涉及到修改s的值,所以我们加上了const修饰,但是这样我们就发现,报错了。这是因为s是const对象,它必须调用const修饰的迭代器
我们查看库里面的,确实又一个const修饰的迭代器
所以我们将迭代器的类型进行修改就可以解决问题了
普通的迭代器可以进行读写,而const修饰的迭代器只能读不能写
在这里为了避免犯错误,我们可以使用auto来代替类型
同样的,反向迭代器遇到const修饰的对象,也需要使用const修饰的迭代器类型
四、String容量相关的成员函数
1.size和length
size是数据个数,length是长度,这两个在string的结果是一致的
这两个的功能是一样的,但是却有两个函数这其实就与string的历史有一些关系,string是在STL产生之前的。当时使用的是length,因为与c语言中的strlen是一致的。但是后来为了在STL中提供统一的接口,就采用了size了。因为对于树形结构等,用lenth这个单词并不合适。而使用size是比较合适的
2.max_size
这个函数可以返回最大的数据个数。
但是要小心,在不同的编译器上,这个函数可能会产生不一样的结果
这是因为STL是一个规范,它有很多版本,比如PJ版本SGI版本。他们的底层实现大同小异
3.capacity
这个函数顾名思义,是计算当前的容量的,同样的,这个函数也要小心,不同的编译器运行结果有可能不同
这里是本应该是16,但是最后一个是\0,故少了一位。容量就是15了
我们可以在vs上分析一下扩容的过程,如下代码所示,可以看到大概是呈1.5倍的速度扩容的
int main()
{
string s("hello world");
cout << s.max_size() << endl;
cout << s.capacity() << endl;
size_t old = s.capacity();
for (size_t i = 0; i < 100; i++)
{
s += 'x';
if (old != s.capacity())
{
cout << "扩容" << s.capacity() << endl;
old = s.capacity();
}
}
return 0;
}
而在Linux环境下,是呈2倍的速度进行扩容的
4.clear
clear的作用是清理字符串的内容,使其成为空字符串
我们可以注意到,size会被改变,但是capacity不会被改变
5.reserve
这里我们首先需要注意的是:不要与reverse这个单词搞混了
reverse: 这个单词的意思是反转
reserve: 这个单词的意思是保留
reserved的功能是请求一个容量的改变
那么我们应该如何使用呢?在前面使用capacity的时候,我们可以观测到string一个对象的容量的改变,而如果我们对对象使用这个成员函数,那么就可以直接改变容量,这样的好处就在于不需要扩容了
如下代码所示
int main()
{
string s;
s.reserve(100);
cout << s.max_size() << endl;
cout << s.capacity() << endl;
size_t old = s.capacity();
for (size_t i = 0; i < 100; i++)
{
s += 'x';
if (old != s.capacity())
{
cout << "扩容" << s.capacity() << endl;
old = s.capacity();
}
}
cout << s.size() << endl;
cout << s.capacity() << endl;
s.clear();
cout << s.size() << endl;
cout << s.capacity() << endl;
return 0;
}
我们可以观测到实际上是要比100大一些的
但是在Linux下,开的容量正好是100,在此不做演示了
所以这个函数具体开多大的空间还取决于编译器。但总的来说,来的容量必须比100要大。不能比100少
那么当比原来string对象的容量要小的时候,会发生什么情况呢,根据文档的描述,可能会缩容。
但是在实际中,缩小容量与否取决于编译器
下面是vs2022的结果,并没有缩小容量。
但是需要注意的是,当我们将这个reserve放到clear后面的时候,缩小了。但是并没有完全缩
而在Linux环境下, 确确实实缩小了容量,即要缩到多少,就缩小到多少。但是有个前提是跟clear有关系。
6.resize
对于这个函数他的作用是填值加开空间,他的作用与resever类似,但是比resever要多了一个填值的过程
下面是他们两个的对比
还需要注意的是,他的填值是由自己来决定的,如果我们不带上这个字符,默认填0
还需要注意的是,如果n比我们当前的数据要多,这自然是我们最期望的状态。但倘若n比我们的数据个数要小,这样的话,我们就会删一些数据
也就是说,他是一定会使得size减少,但是容量不会缩容的
7.shrink_to_fit
这个函数的作用是,收缩至合适,即将capacity调整至size
但是这个函数也是不具有约束力的,即不一定会收缩至和size一样,可能会比size大一些
五、一些经典的力扣题
1.仅仅反转字母
题目链接:仅仅反转字母
这道题思路很简单,我们采用双指针,利用快速排序的思路即可。isalpha()函数是判断一个字符是否为英文字母,若是返回真,否则返回假
class Solution {
public:
string reverseOnlyLetters(string s) {
int end=s.size()-1;
int begin=0;
while(begin<end)
{
while((begin<end)&&(!isalpha(s[begin])))
{
begin++;
}
while((begin<end)&&(!isalpha(s[end])))
{
end--;
}
swap(s[begin],s[end]);
begin++;
end--;
}
return s;
}
};
2.字符串相加
题目链接:https://leetcode.cn/problems/add-strings/
对于这道题,我们的思路是逐位相加。从末尾开始一步一步相加。一开始让进位端为0,然后将相加端都给提取出来,让这三个相加,得到一个数,然后考虑进位,然后将字符给一位一位加上去,最后逆置字符串即可。
class Solution {
public:
string addStrings(string num1, string num2) {
int end1=num1.size()-1;
int end2=num2.size()-1;
string s;
int carry=0;
while(end1>=0||end2>=0)
{
int val1= (end1>=0)?num1[end1]-'0':0;
int val2= (end2>=0)?num2[end2]-'0':0;
end1--;
end2--;
int ret=val1+val2+carry;
carry=ret/10;
ret=ret%10;
s+=ret+'0';
}
if(carry>0)
{
s+='1';
}
reverse(s.begin(),s.end());
return s;
}
};
当然我们也可以采用头插的方式,由于库中并没有给出头插的方式,因为头插的效率太低,string的底层是一个顺序表,我们知道对于顺序表更喜欢尾插。虽然没有头插,但是提供了insert的接口
我们使用第六个成员函数即可。将一个字符插在某个迭代器之前
class Solution {
public:
string addStrings(string num1, string num2) {
int end1=num1.size()-1;
int end2=num2.size()-1;
string s;
int carry=0;
while(end1>=0||end2>=0)
{
int val1= (end1>=0)?num1[end1]-'0':0;
int val2= (end2>=0)?num2[end2]-'0':0;
end1--;
end2--;
int ret=val1+val2+carry;
carry=ret/10;
ret=ret%10;
//s+=ret+'0';
s.insert(s.begin(),ret+'0');
}
if(carry>0)
{
//s+='1';
s.insert(s.begin(),'1');
}
//reverse(s.begin(),s.end());
return s;
}
};
虽然两种方式都可以,但是综合来看,第一种的效率更佳,总时间复杂度为O(N),第二种的总时间复杂度为O(N2)。
六、String其他接口
1.operator[]操作符
这个操作符,在文章我前面已经介绍过,这里在此简单的介绍一下
这个函数可以去访问string某个下标的元素,他有两个函数重载,一个是加了const修饰的,一个是未修饰的,函数在使用时候会自动匹配最合适的一个函数来进行调用
2.at
at其实与operator[]几乎是一样的。
at和operator[]唯一的不同之处就在于对于越界的检查是不一样的
当发生越界的时候,at是抛异常的方式,operator[]则是断言的方式
即at比较温柔一点,operator[]比较暴力
3.assign
assign这个接口的功能是赋值,他有以下的函数重载
如下是他与append的对比
4.insert
insert顾名思义就是在某个位置插入,他有如下的函数重载:
这些函数的功能也是比较明确简单的
如下代码是部分函数的演示
int main()
{
string s1("hello world");
s1.append("xxxxxx");
cout << s1 << endl;
s1.assign("xxxxxx");
cout << s1 << endl;
s1.insert(0, "hello");
cout << s1 << endl;
s1.insert(5, "world");
cout << s1 << endl;
s1.insert(0, 10, 'x');
cout << s1 << endl;
s1.insert(s1.begin()+10, 10, 'y');
cout << s1 << endl;
return 0;
}
insert虽然看上去不错,但是效率比较低下,不宜多用。
5.erase
erase用于从pos位置删除n个字符
如下所示是这个函数的演示
6.replace
顾名思义,这个函数的意思是替代
如下所示是replace函数的一些使用
然而这个函数看似方便,实际上效率极低。一般不会轻易使用这个函数
比如说当我们想要将某个字符串的空格替换为20%的时候,我们可以使用这个函数,但是效率太低,不满意。我们可以使用其他的函数来实现这个功能
int main()
{
string s1("hello world hello world");
string s2;
for (auto ch : s1)
{
if (ch != ' ')
{
s2 += ch;
}
else
{
s2 += "20%";
}
}
s1 = s2;
cout << s1 << endl;
}
7.c_str
这个函数的功能是返回他底层的字符串
这个函数一般是用于跟c的一些接口函数进行配合
int main()
{
string filename = "test.cpp";
FILE* fout = fopen(filename.c_str(), "r");
return 0;
}
我们使用string类型而不是直接使用字符串的好处就在于,string的库非常丰富。我们可以直接去进行调用
8.find
顾名思义,他的功能就是查找,有如下几个函数重载
如果找到了对应的内容,就返回这个位置的下标
9.substr
find可以很方便的找到要找到内容的起始下标。而substr的功能则是取出字符类中字符串中的某一部分,并且返回这一部分
这样的话,我们就可以使用find和substr进行搭配,来进行网站的分割
我们知道网站分为三部分:协议,域名,资源
我们对一个网站进行分割的代码如下所示
int main()
{
string ur1 = "https://home.firefoxchina.cn/?fromwww";
size_t pos1 = ur1.find("://");
//协议
string protocol;
if (pos1 != string::npos)
{
protocol = ur1.substr(0, pos1);
}
//域名和资源
string domain;
string uri;
size_t pos2 = ur1.find('/', pos1 + 3);
if (pos2 != string::npos)
{
domain = ur1.substr(pos1 + 3, pos2 - (pos1 + 3));
uri = ur1.substr(pos2 + 1);
}
cout << protocol << endl;
cout << domain << endl;
cout << uri << endl;
return 0;
}
10.rfind
rfind和find类似,只不过他是从后往前找,这样的好处在于找一个文件的后缀,因为一个文件可能会有多个后缀,所以我们显然不可能从前往后找,我们必须从后往前找,故
11.find_first_of
从名字上来说,它的功能似乎与find类型?
其实不然,虽然它的参数也和find一样,但它的功能是从所给的字符串中,找到第一个和这里面中的任何一个字符相同的字符位置
如下代码所示,功能就是找出这句话中aeiou的位置,并将其替换为*字符
12.find_last_of
这个函数的功能和上面的是十分类似的,唯一不同的就是它是从后往前找的
13.relational operators
这里面其实就是一些运算符重载,其实里面的函数实现是存在一些冗余的,只需要实现对象和对象之间的比较即可,其他的都有隐式类型转换
14.operator+
这个运算符重载也很好理解,就是两个字符串进行相加,但需要注意的是,相加后是不改变原来的对象的
15.getline
这个函数的功能需要与cin对比来看,cin相当于scanf,getline相当于gets。cin当遇到空格时就不读了。而getline可以自己设置结束读取标志,或者默认换行时候才结束,这就在当我们读取一个句子的时候,由于有空格,我们就必须使用getline函数了
我们可以看下面这个例子
字符串的最后一个单词长度
下面才是正确的代码
#include <iostream>
using namespace std;
int main()
{
string s;
getline(cin,s);
int len=s.size();
int pos=s.rfind(' ');
if(pos!=string::npos)
{
cout<< len-(pos+1);
}
else
{
cout<< len;
}
}
16.string转为其他类型
在c语言中字符串转为其他类型时候有两个函数可以去调用:atoi,itoa
但是这两个函数其实并不是很好用
在c++中,我们有这样一系列的函数可以进行转换
17.其他类型转化为string类型
有以下函数重载可以实现这个功能
七、string与模板
我们也许会发现,string貌似跟类模板关系不是很大。
但其实string也是一个模板出来的
我们可以注意到,string其实typedef出来的
类似的,其实还有很多的string