🔥博客主页: 小羊失眠啦.
🎥系列专栏:《C语言》 《数据结构》 《C++》 《Linux》 《Cpolar》
❤️感谢大家点赞👍收藏⭐评论✍️
文章目录
- 前言
- 一、basic_string
- 二、编码理解
- 三、构造函数相关
- 3.1 无参(默认)构造函数
- 3.2 带参构造函数
- 四、容量操作相关
- 4.1 获取数据
- 4.2 扩容空间
- 4.3 调整长度
- 五、遍历字符相关
- 5.1 下标访问
- 5.2 迭代器
- 六、字符修改相关
- 6.1 尾插字符/字符串
- 6.2 任意位置插入字符/字符串
- 6.3 删除字符/字符串位置
- 6.4 查找字符/字符串位置
- 6.5 截取字符串
- 七、非成员函数
- 7.1 流操作
- 7.2 获取字符串
- 7.3 比较函数
前言
STL
是 C++
的重要组成部分,由六大部分构成:伪函数
、空间配置器
、算法
、容器
、迭代器
和 配接器
,其中各种各样的 容器
可以很好的辅助我们写程序,比如今天要介绍的 string
,有了它之后,我们对字符串的操作就能变得行云流水
注意: string
诞生于 STL
之前,因此存在部分接口冗余的情况
一、basic_string
string
是 basic_string
模板 的一份实例,因为字符串多种多样,所以 string
也有各种各样的版本
-
string
常规字符串类,即每个字符占位1byte
-
wstring
宽字符串类,用来处理较长字符串,Winows下占位2byte
,而 Linux下占位4byte
-
u16string
匹配UTF-16
编码标准,指定字符占位2byte
(C++11
) -
u32string
匹配UTF-32
编码标准,规定字符占位4byte
(C++11
)
世界上有各种各样的语言,其字符长度大多不一样,因此需要使用不同的 string
来匹配输出自己国家的字符
二、编码理解
我们这里介绍的是 string
类,它匹配 UTF-8
标准,而此标准又兼容了 ASCII
码,因此比较常用
ASCII
是美国信息标准交换代码,仅仅通过 1byte
就能满足其字符需求
UTF-8
的特点是能根据不同范围的字符匹配使用不同的标准,因为ASCII
都是 0xxxxxxx
的形式,当识别到其他字符时,会匹配使用对应标准,比如当识别到汉字时,会使用 GBK
编码标准来进行输出(Windows)
后续随着万国码 Unicode
的诞生,提出了能适用更多语言的编码标准,即 UTF-16
和 UTF-32
,而 basic_string
中的 u16string
和 u32string
这两个类就是用来匹配编码标准的
注: 这两个类是在 C++11
标准中制定的
我们的 string
其实就是 basic_string <char>
的别名
三、构造函数相关
现在正式进入 string
类的学习,先从默认成员函数—构造函数
入手
注意: string
包含于 iostream
头文件中,并且还需要展开 std
命名空间
3.1 无参(默认)构造函数
#include<iostream>
using namespace std;
int main()
{
string s; //此时调用的是无参构造函数
return 0;
}
调用无参构造函数
时,默认将对象初始化为空串,即只包含 '\0'
的字符串
3.2 带参构造函数
我们也可以指定 string
对象中的内容
int main()
{
string s("Hello String!"); //指定内容
//string s = "Hello String!"; //这种写法也是完全可以的
return 0;
}
string
也支持将对象构造为 n
个字符 c
int main()
{
string s(10, 'w'); //构造10个字符w
return 0;
}
最后再来看看 string
类的 拷贝构造
函数
int main()
{
string s1("Hello");
string s2(s1); //将 s1 的内容构造给 s2
//string s2 = s1; //这种写法也是可以的
return 0;
}
四、容量操作相关
我们可以把 string
类看作一个专门用来处理字符的顺序表
,因为它有字符指针
、容量
、长度
等信息,我们也可以进行手动扩容等操作
4.1 获取数据
获取 string
对象中指向字符串的指针 _str
C++
兼容C
,在某些场景下需要使用指向字符串的指针,因此 string
类中提供了这个接口
int main()
{
string s("Hello");
cout << s.c_str() << endl;
return 0;
}
此时直接打印内容的原因是当指针指向对象为常量字符串时,编译器会直接打印内容
我们可以通过强转来观察函数 c_str()
cout << (void*)s.c_str() << endl; //此时指针非常量字符指针
通过函数 capapcity()
和 size()
获取当前对象的容量和大小
int main()
{
string s(200, 'H'); //直接构造200个字符H
cout << "The string capacity is " << s.capacity() << endl;
cout << "The string size is " << s.size() << endl;
//cout << "The string size is " << s.length() << endl;
return 0;
}
length()
函数能起到和 size()
函数完全一样的效果, 那为什么会有两个函数呢?
-
string
诞生于STL
之前,当时的设计的获取大小函数为length()
-
后来当
string
并入STL
后,委员会为了统一化,就在string
类中添加了一个size()
函数,因为其他容器中获取大小的函数都是size()
-
为了确保向前兼容性,不能直接删除
length()
,这里推荐使用size()
4.2 扩容空间
new
出来的空间不支持像 realloc
一样直接扩容,而是需要通过函数扩容
realloc
大多数情况下都是异地扩容,即开辟-拷贝-销毁-更改指向
- 而
reserve()
函数实现的就是异地扩容
int main()
{
string s(10, 'H'); //初始化大小为10
cout << "The default capacity " << s.capacity() << endl;
s.reserve(300); //扩容为300
cout << "The expansion capacity " << s.capacity() << endl;
return 0;
}
VS 中的容量都会稍微多一点
假若我们不手动扩容,string
也会像顺序表
一样,识别到容量不够时,自动扩容
VS中 string
的扩容策略
- 默认给一个大小为
15
的数组存储数据,当数组够用时,都是用的数组 - 当数组容量不够时,改用指针,先
2倍
扩容至30
,后续字符都是存在指针中 - 之后的扩容操作,都是以
1.5倍
进行扩容 - 会多开辟一些空间
Linux中 string
的扩容策略
- 默认大小为
0
的空间 - 当第一次扩容时,会先扩至
1
- 扩容时每次都是
2倍
扩容法,比较清晰 - 不会多开空间
int main()
{
string s;
int capacity = s.capacity();
cout << "The default capacity " << capacity << endl;
int n = 0;
while (n <= 100)
{
//尾插字符
s += 'a';
if (capacity != s.capacity())
{
capacity = s.capacity();
cout << "The new capacity " << capacity << endl;
}
n++;
}
return 0;
}
至于 Windows
中为何如此复杂?首先是 STL
版本不同,其次string
在实际使用中,都用不了太大的空间,因此 VS
就直接索性给了一个默认大小为 15
的数组,后续有需要再进行扩容
频繁扩容会导致内存碎片问题,VS在这里的处理方法是比较合理的
小技巧: 在使用 string 时,可以先提前计算好需要的空间,然后通过 reserve 直接提前扩好,避免因自动扩容而导致的内存碎片问题
4.3 调整长度
除了可以扩容外,我们还可以改变 size
int main()
{
string s(50, 'w'); //当前的 size 为50
cout << "The default size " << s.size() << endl;
cout << "The default capacity " << s.capacity() << endl;
cout << endl;
s.resize(30); //改变 size 为30
//s.resize(100, 'Z'); //还可以这样写,更改后50块空间为Z
cout << "The new size " << s.size() << endl;
cout << "The new capacity " << s.capacity() << endl;
return 0;
}
resize()
有两种情况:
- 调整后空间比原空间大,此时相当于扩容
reserve()
,不过resize()
还有一个初始化的功能,即将参数2设为指定字符,如果没有指定就默认为\0
- 调整后空间比原空间小,此时将
_size
调整至目标空间,而 _capapcity 不变,此时我们也无法访问到_size
之外的数据
resize()
并不会缩容,因为缩容的代价比较大,需要先开辟新空间,然后拷贝,释放原空间,才能完成缩容,因此 resize()
在处理时,若新空间比原空间小,是不会改变 _capaciy
的
五、遍历字符相关
字符串当然少不了遍历操作,主要有三种遍历方式:下标
、at()
、迭代器
,因为 下标
和 at()
区别不大,所以可以一起介绍,而 迭代器
是一个很重要的东西,后续容器学习中都会出现它的影子
5.1 下标访问
首先来看看 下标访问
,实现原理很简单:运算符重载 operator[]
int main()
{
string s("chatGPT");
size_t pos = 0; //下标
while (pos < s.size())
{
//直接像数组一样通过下标访问字符
cout << "The " << pos + 1 << " char is " << s[pos] << endl;
pos++;
}
return 0;
}
当我们出现越界行为时,下标访问是直接通过 assert 报错的
下面再来看看 at()
cout << "The " << pos + 1 << " char is " << s.at(pos) << endl;
运行结果与 operator[]
一致,其实这两种方法的实现原理都一样,不过处理问题的方法不一样
当出现越界访问,at() 是抛出异常,而非直接断言报错
总的来说,at()
用的比较少,我们一般都是使用 operator[]
来进行下标的随机访问
5.2 迭代器
下面来看看迭代器 iterator
遍历字符串
int main()
{
string s("chatGPT");
//创建迭代器
string::iterator it = s.begin(); //此时的 it 相当于指向第一个字符的指针
//auto it = s.begin(); //可以利用 auto 自动识别类型
while (it != s.end())
{
cout << *it;
it++;
}
cout << endl;
return 0;
}
注: begin()
获取第一个字符,end()
获取最后一个字符的下一个字符,即 '\0'
除了可以正向遍历外,我们还可以通过反向迭代器 reverse_iterator
进行反向遍历
int main()
{
string s("chatGPT");
//创建反向迭代器
string::reverse_iterator rit = s.rbegin(); //此时的 rit 相当于指向最后一个字符的指针
//auto it = s.rbegin();
while (rit != s.rend())
{
cout << *rit;
rit++;
}
cout << endl;
return 0;
}
注: rbegin()
获取最后一个字符,rend()
获取第一个字符的前一个字符
迭代遍历区间都是左闭右开
除了上面两种普通迭代器外,还有两个 const
修饰的迭代器,用来遍历常量字符串
const_iterator
正向遍历常量字符串const_reverse_iterator
反向遍历常量字符串
注意:
- 迭代器名
const_iterator
中的const
并非是const
操作符,而是与普通迭代器构成重载 - 迭代器不太适合遍历顺序表,适合用来遍历链表
- 所谓的
范围for
其实就是在调用迭代器进行遍历
六、字符修改相关
现在来谈谈字符修改相关接口
6.1 尾插字符/字符串
尾插字符/字符串有三种方式:
push_back()
尾插字符append()
尾插字符/字符串operator+=
尾插字符/字符串
先来看看 push_back()
int main()
{
string s = "Hello ";
//尾插字符
s.push_back('X');
cout << s << endl;
return 0;
}
push_back()
就像是顺序表的尾插,一次只能插入一个字符
再来看看 append()
int main()
{
string s = "Hello ";
//尾插字符
s.append(3, 'X'); //需要指定待插入的字符数
s.append(" YYYY"); //或者直接插入字符,都是可以的
cout << s << endl;
return 0;
}
append()
还有很多其他用法,感兴趣的可以去查看官方文档
最后再来看看 operator+=
,这个是使用频率最高的,因为比较方便
int main()
{
string s = "Hello ";
//尾插字符
s += 'C';
s += "SDN";
cout << s << endl;
return 0;
}
在日常使用中,对于字符串尾插这件事,我们通常都是使用 operator+=
6.2 任意位置插入字符/字符串
string
支持在任意位置插入字符/字符串
int main()
{
string s("cccccc");
cout << "Begin insert:" << s << endl;
s.insert(2, 1, 'A'); //在第 n 个位置插入 m 个字符 c
s.insert(4, "BBBB"); //在第 n 个位置插入字符串
cout << "After insert:" << s << endl;
return 0;
}
insert()
的用法同样还有很多,可以自行查看官方文档
6.3 删除字符/字符串位置
有任意位置插入,当然就有任意位置删除 erase()
int main()
{
string s("ABCDEFG");
cout << "Begin erase:" << s << endl;
//任意位置删除
s.erase(3, 1); //从pos开始, 删除第1个字符
cout << "After erase 1 char:" << s << endl;
s.erase(2, 4); //从pos2开始, 删除4个字符
cout << "After erase 4 char:" << s << endl;
s.erase(); //默认全删
cout << "After erase all:" << s << endl;
return 0;
}
注意: erase()
是一个全缺省参数,参数1为 0
,表示默认从 pos0
开始,参数2为 npos
,这是无符号整型中的 -1
,为无符号整型最大值,意思就是如果不写参数2,默认就全删完了
来看看 npos
它的值是 4294967295
,没有字符串长达 42亿
多,因此可以用来当作默认长度值
6.4 查找字符/字符串位置
它的值是 4294967295
,没有字符串长达 42亿
多,因此可以用来当作默认长度值
int main()
{
string s("My name is Sheep");
//查找,返回的是目标字符/字符串第一次出现的下标
cout << "Find 1 char pos: " << s.find('n') << endl; //找字符,默认从pos0开始
cout << "Find str pos: " << s.find("Sheep", 5) << endl; //找字符串,从pos5开始
cout << "Find not exist str pos: " << s.find("cow", 10) << endl; //假设没找到
return 0;
}
可以看到,当目标不存在时,返回的就是 npos
find()
还有几种形式:
-
rfind()
从后往前找 -
find_first_of(str, pos = 0)
从pos
位置往后,找str
中出现的任意字符 -
find_last_of(str, pos = npos)
从npos
位置往前,找str
中出现的任意字符 -
find_first_not_of()
反向查找 -
find_last_not_of()
反向查找
string
类的接口雀氏很多
6.5 截取字符串
int main()
{
string s("My name is Sheep");
//利用find 和 substr 切割出 Sheep
cout << "The target is " << s.substr(s.find('S'), 5) << endl;
return 0;
}
其实 substr()
通常用来截取网址中的域名
七、非成员函数
string
类中还有很多定义在类外的非成员函数
7.1 流操作
我们可以直接对 string
对象使用流插入 operator<<
和流提取 operator>>
int main()
{
string s;
cin >> s;
cout << s;
return 0;
}
7.2 获取字符串
单纯的流插入是无法满足字符串插入需要的,因为字符串中往往都会包含 ' '
,而 cin
会认为这是结束标志,进而不再读取字符,因此有专门的函数获取字符串 getline()
#include<string>
int main()
{
string s;
getline(cin, s); //需要包含头文件 string
cout << s;
return 0;
}
注意: 需要包含头文件 string
7.3 比较函数
string
类中存在一系列的大小比较函数(18个),光是判断相等就有3个,其实没必要设计这么多函数,这可能也是 string
饱受别人吐槽的原因之一
原文出处:《STL中的string类怎么啦?》