【C++初阶】STL之学习string的用法
目录
- 前言:
- 一、认识下string
- 1.1 什么是string
- 1.2 为什么要有string
- 二、string 类的接口使用
- 2.1 初始化与析构
- 2.1.1 初始化
- 2.1.2 析构
- 2.2 容量操作
- 2.2.1 长度大小——size和length
- 2.2.2 空间总大小——capacity
- 2.2.3 判空——empty
- 2.2.4 清空——clear
- 2.2.5 预留空间——reserve
- 2.2.6 改变有效字符个数、填充多余空间——resize
- 2.3 遍历访问
- 2.3.1 下标遍历——operator[]
- 2.3.2 范围for
- 2.3.3 迭代器遍历——iterator
- 2.4 修改操作
- 2.4.1 尾插字符——push_back
- 2.4.2 尾插字符串——append
- 2.4.3 字符串追加字符串——operator+=
- 2.4.4 赋值——assign
- 2.4.5 插入——insert
- 2.4.6 删除——erase
- 2.4.7 查找——find
- 2.4.8 替换——replace
- 2.4.9 返回C格式字符串——c_str
- 2.4.10 截取字符串——substr
- 2.5 输入操作
- 2.5.1 获取一行字符串——getline
前言:
STL是C++的标准模板库,里面包含了许多算法和数据结构,例如我们熟悉的顺序表、链表、栈和队列以及一些常见的算法等等,编程者想使用这些就可以直接从库中调用,不必再自己造轮子了。
下面为STL内容的一张图:
接下来,我们要学习STL中的string。
一、认识下string
1.1 什么是string
string 是C++的一个类模板,字符串的类模板。要定义一个对象为字符串,就可以用string类型,说明它是一个字符串,相当于字符数组。
int main()
{
string s1("hello yss");
return 0;
}
1.2 为什么要有string
以前我们用C语言写代码的时候,假如字符串如果要计算它的长度,必须要调用C标准库中的函数才行。但是这些函数与字符串本身是分开的,与C++面向对象的思想不契合,而且很容易出现越界等错误,所以C++提供了string类,类里就有我们想使用的函数来操作字符串,我们只需要调用类的接口(函数)即可,更加简洁。
二、string 类的接口使用
string类有许多接口,这里只介绍一些常见的。
2.1 初始化与析构
2.1.1 初始化
1️⃣无参与有参
int main()
{
string s1();//无参 空字符串
string s2("hello yss");//有参
cout << s2 << endl;
return 0;
}
注意:括号里字符串也可以是一个字符,但是必须要 " y " 这样写,不能 ’ y ’ 这种写法,否则会报错。
2️⃣字符填充
前面的参数表示字符串的长度,后面的参数是要填充的字符。
int main()
{
string s1(5, 'y');
cout << s1 << endl;
return 0;
}
3️⃣拷贝构造
int main()
{
string s1("hello yss");
string s2(s1);
cout << s2 << endl;
return 0;
}
另一种写法:
int main()
{
string s1("hello yss");
string s2 = s1;//用已经存在的对象去拷贝构造刚未存在的对象
cout << s2 << endl;
return 0;
}
4️⃣拷贝子字符串
pos是从这位置开始拷贝,len是拷贝的个数
int main()
{
string s1("hello yss");
string s2(s1, 3, 5);//如果最后一个参数的值大于后面的
//长度,那么就直接把从pos位置开始的字符串全部拷贝构造
cout << s2 << endl;
return 0;
}
一个空格也算一个字符
我们发现len=npos,说明这是一个半缺省,也就是说最后的参数可以不写。
string s2(s1, 3);
npos表示size_t的最大值,作用是返回一个字符串中不存在的位置,可以判断某个子字符串是否存在。
2.1.2 析构
了解下即可
作用就是清理。
2.2 容量操作
2.2.1 长度大小——size和length
有两个操作函数,都可以计算字符串的有效长度
int main()
{
string s1("hello yss");
cout << s1.size() << endl;
cout << s1.length() << endl;
return 0;
}
一般计算大小使用size更好,因为size表示大小即长度,length表示长度,链表、顺序表有长度的说法,但是后面要学习到的树就没有所谓的长度了,只有大小,所以,为了方便且统一,计算的大小都使用size更好些。
2.2.2 空间总大小——capacity
给对象预分配好一定大小的空间。如果字符串的有效长度小于预分配的空间,空间大小不变;否则再次分配一定大小的空间
int main()
{
string s1("hello yss");
cout << s1.size() << endl;
cout << s1.capacity() << endl;
string s2("hello yssxxxxxxxxxxxxxxxx");
cout << s2.size() << endl;
cout << s2.capacity() << endl;
return 0;
}
2.2.3 判空——empty
int main()
{
string s1("hello yss");
cout << s1.empty() << endl;
string s2;
cout << s2.empty() << endl;
return 0;
}
2.2.4 清空——clear
清空的是有效字符,总空间大小不变
int main()
{
string s1("hello yss");
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.clear();
cout << s1.size() << endl;
cout << s1.capacity() << endl;
return 0;
}
2.2.5 预留空间——reserve
1️⃣参数是缺省值,所以参数可写可不写,不写默认缺省值为0,小于空间大小(capacity()的预留空间大小),空间大小不变,不改变字符串中的内容。
int main()
{
string s1("hello yss");
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.reserve();
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
return 0;
}
2️⃣参数的值大于0,小于有效字符的个数
int main()
{
string s1("hello yss");
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.reserve(6);
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
return 0;
}
小于空间总大小,所以空间总大小不变,不影响字符串的内容。
3️⃣参数的值大于等于有效字符个数,小于空间总大小
int main()
{
string s1("hello yss");
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.reserve(12);
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
return 0;
}
还是因为预留的空间小于空间总大小,所以空间总大小不变,且不影响字符串的内容。
4️⃣参数的值大于空间总大小
int main()
{
string s1("hello yss");
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.reserve(20);
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
return 0;
}
当预留空间大于空间总大小时,就会再分配新的空间大小,字符串的内容还是不受影响。
5️⃣调用两次,第二次参数比第一次小
int main()
{
string s1("hello yss");
s1.reserve(111);
cout << s1.capacity() << endl;
s1.reserve(6);
cout << s1.capacity() << endl;
return 0;
}
第一次调用预留空间的大小扩容后,第二次比第一次要小,不会改变,说明reserve没有缩容的功能。
总结:
1.当reserve预留的空间小于等于空间总大小时,空间总大小不变;
2.reserve预留空间不影响字符串的内容
3.reserve不能缩容。
reserve预留的空间大于空间总大小就会重新分配新的空间总大小,那么这空间总大小的增长规律是怎样的呢,看一下代码:
int main()
{
string s1("hello yss");
size_t len = s1.capacity();
for (int i = 0; i < 100; i++)
{
s1.push_back('x');
while (len != s1.capacity())
{
len = s1.capacity();
cout << len << endl;
}
}
return 0;
}
大概是以1.5倍的大小增长,不同的编译器可能不同。
2.2.6 改变有效字符个数、填充多余空间——resize
可支持两种写法,一个参数的表示有效字符个数,两个参数的分别为有效字符个数和要填充的字符。
1.先来一个参数的写法:
1️⃣参数的值小于有效字符个数
int main()
{
string s1("hello yss");
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.resize(3);
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
return 0;
}
有效字符个数改变,字符串的内容也改变,空间总大小不变。
2️⃣参数的值大于有效字符个数、小于空间总大小
int main()
{
string s1("hello yss");
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.resize(12);
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
return 0;
}
有效字符个数改变,字符串内容不变(没有填充字符),空间总大小不变。
3️⃣参数的值大于空间总大小
int main()
{
string s1("hello yss");
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.resize(20);
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
return 0;
}
有效字符个数改变,字符串内容不变(没有填充字符),空间总大小改变。
2.有第二个参数(填充字符)的写法:
1️⃣参数的值小于有效字符个数
int main()
{
string s1("hello yss");
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.resize(3, 'a');
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
return 0;
}
有效字符个数改变,字符串内容受影响,空间总大小不变。
2️⃣参数的值大于有效字符个数、小于空间总大小
int main()
{
string s1("hello yss");
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.resize(12, 'a');
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
return 0;
}
有效字符个数改变,字符串增加的空间填充字符,空间总大小不变。
3️⃣参数的值大于空间总大小
int main()
{
string s1("hello yss");
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.resize(20, 'a');
cout << s1 << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
return 0;
}
有效字符个数改变,字符串增加的空间填充字符,空间总大小增加。
总结:
1.resize影响有效字符个数,小于原来有效字符个数时,字符串内容随之改变;
2.当n的值(第一个参数)大于原来的有效字符个数会在后面填充字符;
3.n小于空间总大小,空间总大小不变;否则会扩容。
2.3 遍历访问
2.3.1 下标遍历——operator[]
有两种写法,一个是operator关键字+运算符,另一个是直接方括号下标。
int main()
{
string s1("hello yss");
for (size_t i = 0; i < s1.size(); i++)
{
cout << s1.operator[](i) << " ";
//cout << s1[i] << " ";
}
cout << endl;
return 0;
}
2.3.2 范围for
int main()
{
string s1("hello yss");
for (auto e : s1)
{
cout << e << " ";
}
cout << endl;
return 0;
}
2.3.3 迭代器遍历——iterator
它是可以检查容器内的元素并遍历元素的数据类型,写法先看以下代码:
int main()
{
string s1("hello yss");
string::iterator s = s1.begin();
while (s != s1.end())
{
cout << *s << " ";
s++;
}
cout << endl;
return 0;
}
s相当于一个指针,首先得到的是字符串的第一个字符,只要它不等于斜杠0,循环打印出每个字符,等于斜杠0跳出。
那么begin()和end()又是什么呢?
begin返回容器的首元素,end返回容器的最后一个元素的下一个地址。
还有加const的写法,用于函数调用时为了不改变字符串的内容形参加const,对应的迭代器也要加const
void Func(const string& ss)
{
string::const_iterator s = ss.begin();
while (s != ss.end())
{
cout << *s << " ";
s++;
}
cout << endl;
}
int main()
{
string s1("hello yss");
Func(s1);
return 0;
}
注意:是const_iterator,中间是下划线,与const iterator是不同的。const_iterator限制的指针指向的内容,所以指针指向的内容不能修改;const iterator限制的是指针本身,也就是说地址不能修改。
如果要逆置字符串该怎样操作呢
1️⃣先用以前的方法:
int main()
{
string s1("hello yss");
string::iterator s = s1.begin();
while (s != s1.end())
{
cout << *s << " ";
s++;
}
cout << endl;
size_t begin = 0;
size_t end = s1.size() - 1;
while (begin < end)
{
char tmp = s1[begin];
s1[begin] = s1[end];
s1[end] = tmp;
begin++;
end--;
}
s = s1.begin();
while (s != s1.end())
{
cout << *s << " ";
s++;
}
return 0;
}
这里交换两个元素时要自己写,感觉有点麻烦,可以使用模板
2️⃣交换模板:
template<class T>
void Swap(T& a, T& b)
{
T t = a;
a = b;
b = t;
}
int main()
{
string s1("hello yss");
string::iterator s = s1.begin();
while (s != s1.end())
{
cout << *s << " ";
s++;
}
cout << endl;
size_t begin = 0;
size_t end = s1.size() - 1;
while (begin < end)
{
Swap(s1[begin], s1[end]);
begin++;
end--;
}
s = s1.begin();
while (s != s1.end())
{
cout << *s << " ";
s++;
}
return 0;
}
不看顶上的交换模板函数,循环里的代码比前面简洁了一些,但是还是感觉挺繁琐的
3️⃣逆置算法函数:
int main()
{
string s1("hello yss");
string::iterator s = s1.begin();
while (s != s1.end())
{
cout << *s << " ";
s++;
}
cout << endl;
reverse(s1.begin(), s1.end());
s = s1.begin();
while (s != s1.end())
{
cout << *s << " ";
s++;
}
return 0;
}
不用逆置算法函数,也可以逆序遍历
int main()
{
string s1("hello yss");
string::reverse_iterator s = s1.rbegin();
while (s != s1.rend())
{
cout << *s << " ";
s++;
}
cout << endl;
return 0;
}
这两个接口的也有const,与前面的同理
void Func(const string& ss)
{
string::const_reverse_iterator s = ss.rbegin();///
while (s != ss.rend())
{
cout << *s << " ";
s++;
}
cout << endl;
}
int main()
{
string s1("hello yss");
Func(s1);
return 0;
}
有没有发现其中一段代码太长了,所以这里就就可以用以前学过的auto来简化代码
auto s = ss.rbegin();//自动推导类型
2.4 修改操作
2.4.1 尾插字符——push_back
int main()
{
string s1("hello yss");
s1.push_back('x');
cout << s1 << endl;
return 0;
}
2.4.2 尾插字符串——append
这个接口的可实现方式比较多,所以这里只介绍常见的
int main()
{
string s1("hello yss");
s1.append("a");
cout << s1 << endl;
s1.append("vvvv");
cout << s1 << endl;
return 0;
}
因为是字符串,所以要双引号,但是双引号里也可以是一个字符。
2.4.3 字符串追加字符串——operator+=
这个比前面两个都要实用得多了,既可以尾插字符串,也可以尾插字符。而且写起来也方便。
int main()
{
string s1("hello yss");
s1 += "vvvv";
cout << s1 << endl;
string s2("hello yss");
s2 += 'd';
cout << s2 << endl;
string s3("hello yss");
s3 += s1;
cout << s3 << endl;
return 0;
}
2.4.4 赋值——assign
1️⃣用一个字符串赋给另一个字符串
int main()
{
string s1("hello yss");
string s2 = s1.assign(s1);
cout << s2 << endl;
return 0;
}
2️⃣从一个字符串中提取子字符串赋给另一个字符串
int main()
{
string s1("hello yss");
string s2;
s2.assign(s1,2,5);
cout << s2 << endl;
return 0;
}
3️⃣一个字符串赋给另一个字符串
int main()
{
string s2;
s2.assign("yyyyyyyy");
cout << s2 << endl;
return 0;
}
4️⃣一个字符串赋给另一个字符串,限定个数
int main()
{
string s2;
s2.assign("yyyyyyyy", 3);
cout << s2 << endl;
return 0;
}
5️⃣字符赋给一个字符串,有个数控制
int main()
{
string s3;
s3.assign(10, 'x');
cout << s3 << endl;
return 0;
}
2.4.5 插入——insert
1️⃣
从pos位置插入一个字符串,和可以限制插入字符串的个数。
int main()
{
string s1("hello yss");
s1.insert(1, "ccc");
cout << s1 << endl;
string s2("hello yss");
s2.insert(3, "cccccc",3);
cout << s2 << endl;
return 0;
}
2️⃣
从pos位置插入一个字符串。第二个有点长,在第一个的基础上可以选择要插入的字符串的起始的位置,并且限制插入的个数。
int main()
{
string s3("hello yss");
string s4("hello world");
s4.insert(5, s3);
cout << s4 << endl;
string s5("hello yss");
string s6("hello world");
s6.insert(5, s5,2, 3);
cout << s6 << endl;
return 0;
}
3️⃣
插入字符,可以控制个数
int main()
{
string s7("hello yss");
s7.insert(5, 4, 'x');
cout << s7 << endl;
return 0;
}
2.4.6 删除——erase
1️⃣从pos位置开始删除len个字符,这两个参数都是缺省值,如果不写参数,就是一个字符串全部删除;
2️⃣删除某个位置的字符;
3️⃣删除从一个位置到另一个位置的字符串。
int main()
{
string s1("hello yss");
s1.erase();
cout << s1 << endl;
string s2("hello yss");
s2.erase(3,5);
cout << s2 << endl;
string s3("hello yss");
s3.erase(s3.begin());
cout << s3 << endl;
string s4("hello yss");
s4.erase(s4.begin() + 1, s4.end() - 1);
cout << s4 << endl;
return 0;
}
2.4.7 查找——find
查找有一个字符串或者字符,后面没有参数就默认从字符串的起始位置开始找,有写参数从pos位置开始找
int main()
{
string s1("hello yss");
size_t pos = s1.find(" ");
cout << pos << endl;
return 0;
}
2.4.8 替换——replace
用len个90#替换pos位置的字符
int main()
{
string s1("hello yss");
s1.replace(5, 1, "90#");
cout << s1 << endl;
return 0;
}
小练习:把一个字符串内的所有空格替换成20%
1️⃣做法1:
int main()
{
string s1("hello yss hello world");
size_t pos = s1.find(" ");
while (pos != string::npos)
{
s1.replace(pos, 1, "20%");
pos = s1.find(" ", pos + 3);
}
cout << s1 << endl;
return 0;
}
假如一个字符串内有多个空格,先用pos记录第一个空格的位置,当pos不等于npos成立,说明在字符串中找到了这个pos位置,然后在这个位置替换成20%。因为前面的pos已经用过一次了,所以这里要重新定义新的pos位置找后面的空格。参数为pos+3是因为20%有3个字符,所以要跳过这3个字符开始查找。直到后面没有空格,找不到,返回整型的最大值,为-1,然后跳出循环。
find()接口的一个说明:找不到返回npos
这里再介绍下npos:
它是静态成员常量,值为-1,是无符号类型的,所以也是整型的最大值。
2️⃣做法2:
int main()
{
string s1("hello yss hello world");
string s2;
for (auto e : s1)
{
if (e == ' ')
{
s2 += "20%";
}
else
{
s2 += e;
}
}
s1.swap(s2);//交换
cout << s1 << endl;
return 0;
}
创建一个临时字符串,遍历原来的字符串,如果是空格,在临时的字符串+=空格,否则+=原来字符串中的字符。如果不想用临时字符串打印出来,可以交换。
2.4.9 返回C格式字符串——c_str
可以将 const string* 类型 转化为 const char* 类型,因为在c语言中没有string,所以要把它转变成C语言中字符串的形式。
在文件操作中C语言和C++混着用:
int main()
{
string filename("test.cpp");
FILE* pf = fopen(filename.c_str(), "r");
char ch = fgetc(pf);
while (ch != EOF)
{
cout << ch;
ch = fgetc(pf);
}
return 0;
}
2.4.10 截取字符串——substr
因为两个参数都有缺省值,两个都不写,就相当于直接取整个字符串了;后面的参数不写,从pos位置开始截取剩下的全部字符串。
1️⃣截取 . 和它后面的字符串
int main()
{
string s1("test.cpp");
size_t pos = s1.find('.');
if (pos != string::npos)
{
string s2 = s1.substr(pos);
cout << s2 << endl;
}
return 0;
}
2️⃣如果一个字符串内有多个点,要截取最后一个点以及后面的字符串
int main()
{
string s1("test.cpp.xxx");
size_t pos = s1.rfind('.');
if (pos != string::npos)
{
string s2 = s1.substr(pos);
cout << s2 << endl;
}
return 0;
}
用原来的find()要从头开始找,比较麻烦。所以string还提供了另一个查找的函数——rfind(),可以逆向查找,即从后向前。
3️⃣分割网址
int main()
{
string s("https://cplusplus.com/reference/string/string/substr/");
string s1, s2, s3;
size_t pos1 = s.find(':');
if (pos1 != string::npos)
{
s1 = s.substr(0, pos1);
cout << s1 << endl;
}
size_t pos2 = s.find('/', pos1 + 3);
if (pos2 != string::npos)
{
s2 = s.substr(pos1 + 3, pos2 - pos1 - 3);
cout << s2 << endl;
}
s3 = s.substr(pos2 + 1);
cout << s3 << endl;
return 0;
}
网址的格式:
<协议>://<服务器名称>.<域名>/<目录>/<文件名>
我们是想分割成3个部分,所以定义3个string 对象,一个一个分割。首先分割出协议,先要找到冒号的位置,冒号的位置的pos1的值即为前面字符串的有效个数,所以截取字符串从起始位置开始截取pos1个。第二个分割出服务器名称和域名,要找的截至位置是 / ,但是要注意从pos1+3的位置开始找,找到后也是从pos1+3的位置开始截取,截取pos2 - pos1 - 3个。最后一个不必多说。
2.5 输入操作
2.5.1 获取一行字符串——getline
istream& is表示一个输入流,例如cin;char delim表示终止符,例如回车,或者别的符号。
1️⃣自己定义终止符
int main()
{
string s1;
getline(cin, s1,'#');
cout << s1 << endl;
return 0;
}
2️⃣自己没有定义终止符
int main()
{
string s1;
getline(cin, s1);
cout << s1 << endl;
return 0;
}
自己没有写终止符,默认的终止符为回车
那么这个有什么用呢?在输入一行字符串的时候,如果我们使用的是cin,它的默认终止符为空格或者换行,所以如果我们输入的字符串中有空格,打印出来的结果就不会是一行。
看以下代码:
int main()
{
string s1;
cin >> s1;
cout << s1 << endl;
return 0;
}
所以如果在做题中还是在其他写代码的场景要求输出一整行字符串,getline就是一个好的选择。