前言
本文将详细讲解STL中string类的常用的接口函数。
一、为什么学习string类?
1、字符串类型的重要性
在现实生活中有很多复杂类型是以字符串来表达的,比如我们在搜索引擎输入的“数据”,一个人的姓名、身份证号等等。
所以字符串类型是很重要的。
2、C语言中的字符串
C语言中,字符串是以‘\0’结尾的一些字符的集合。
- 在C语言中并没有字符串类型,但是我们可以使用字符数组或字符指针来表示。
- C标准库中提供了一些strxx系列的库函数管理字符串,但是这些库函数与字符串是分离的,不太符合OOP的思想。
- 并且字符串的底层空间需要用户自己管理,一不小心可能就会越界访问。
因此C++中引入了string——管理字符串的类。
在常规工作中,为了简单、方便、快捷,我们基本都会使用string类,很少有人去使用C库中的字符串操作函数,所以我们需要学习string。
3、怎样学习STL?
- 学习STL 不要想 着把每一个用法都掌握,因为STL的知识点太多了,所以我们只需掌握它一些常用的,其余不常用的了解有这么一个用法,使用时查看文档即可。
- 学习STL我们要学会查看文档,推荐两个查看文档的网站
- 正统版本:优点更新快,缺点可读性差
- 山寨版本:只更新到C++11,可读性好,推荐
- STL(standard template libaray-标准模板库)使用之前需要声明对应的头文件,并且展开命名空间std。
二、string类对象的常见接口函数
1、string类对象的常见构造
学习string,我们首先得学会它的构造。
我们知道一个类有多种初始化对象的方式,string也是如此,如下图:
tip:
- 需要掌握的几个常见构造
string()
:默认构造,构造一个空的string对象,即空字符串(0个字符)。string(const char* s)
:用C字符串来构造string对象。string(size_t n, char c)
:用连续n个字符的c来构造string对象。string(const string& str)
:拷贝构造函数,构造一个str的副本。
- 只需要了解的几个构造函数
string(const string& str, size_t pos, size_t len = npos)
:用str的部分序列来构造string对象,从pos位置开始,取len个字符。注意:如果len太大或者len为缺省值npos时,会把str从pos位置开始直到结束的字符序列用来构造string对象。(npos是string类的一个静态成员变量,因为npos = -1且又为无符号类型,所以它在内存中表示整形的最大值)string(const char* s, size_t n)
:用s指向的字符数组的前n个字符来构造string对象。
代码示例:
#include<iostream>
#include<string>//使用string,要声明头文件
using namespace std;//展开命名空间
int main()
{
//常用的构造函数
//1、默认构造函数——构造一个空的string对象,即0个字符的空字符串
string s1;
//虽然string是自定义类型,但是string文件中operator<<,所以可以直接使用<<
cout << s1 << endl;
//2、使用C字符串来构造string对象
string s2("hello string");
cout << s2 << endl;
//3、用连续n个字符的c来构造string对象
string s3(10, '*');
cout << s3 << endl;
//4、拷贝构造——构造一个s2的副本
string s4(s2);
cout << s4 << endl;
//其余需要只需了解即可
//1、取s2中的hello来构造一个string对象,所以pos = 0;len = 5.
string s5(s2, 0, 5);
cout << s5 << endl;
//取s2中的string来构造一个string对象,所以pos = 6,len >= 6或不传参,使用npos缺省值
string s6(s2, 6);
cout << s6 << endl;
//string的operator=支持三种类型的直接赋值
s1 = s5;//string
cout << s1 << endl;
s1 = "abc";//const char*
cout << s1 << endl;
s1 = 'a';//char
cout << s1 << endl;
return 0;
}
tip:
- 注意:因为某些历史原因,也为了与C兼容,所以C++语言中的字符串字面值并不是标准库类型string的对象。切记,字符串字面值与string是不同的类型。
- 因为析构函数会自动调用,所以析构我们就不做讲解了。
- string的operator=支持三种类型的直接赋值,如下图
2、string类对象的容量操作
掌握
:string对象的常用容量操作
函数名称 | 功能说明 |
---|---|
size | 返回字符串有效字符长度(注:不包含\0) |
capacity | 返回当前为字符串分配的存储空间,以字节为单位 |
clear | 清空字符串的有效字符,使之成为空字符串 |
reserve | 申请n个字符的字符串容量 ,使用场景:当我们知道字符串需要的容量,一次reserve扩容,避免多次扩容 |
resize | 调整字符串的长度 ,将有效字符的个数改成n个,多出的空间用字符c填充 |
empty | 检测字符串是否为空串,是返回true,否则返回false |
(1)size与length成员函数
size和length两个成员函数的功能一样,都是返回字符串有效字符长度。
代码示例:
int main()
{
string s("hello string");
//计算s的长度
cout << s.size() << endl;
cout << s.length() << endl;
return 0;
}
运行结果:
tip:
- 字符串的有效字符长度不包含\0,因为\0是字符串的结束标志。
- string的产生比str早一些,严格来说string不属于STL容器,但是string又与容器属于同一类的,所以不从历史角度看也可以将其归为容器。
- size与length方法底层实现完全相同,引入size的原因是为了其他容器接口保持一致,一般情况下基本都是使用size。
(2)max_size成员函数(了解)
max_size返回字符串可以达到的最大长度。
tip:
- STL是一个规范,但它有很多版本,所以底层实现大同小异。
- max_size的文档只是说返回字符串可以达到的最大长度,但是并未说明具体说明,所以在不同版本下max_size不一样。
- max_size不靠谱,使用中无意义。
(3)capacity成员函数
capacity返回空间的总大小。
代码示例:
int main()
{
string s("hello string");
cout << s.size() << endl;
//返回空间的总大小
cout << s.capacity() << endl;
size_t old = s.capacity();//返回值类型为无符号类型
//观察每一次扩容空间的总大小
for (int i = 0; i < 100; ++i)
{
s += 'x';
if (old != s.capacity())
{
cout << "扩容:" << s.capacity() << endl;
old = s.capacity();
}
}
return 0;
}
VS下的运行结果:
g++下的运行结果:
tip:
- capacity返回当前为字符串分配的存储空间,以字节为单位。
- capacity不一定等于字符串的长度,它可以大于或等于。(STL是一个规范,有许多版本,所以底层大同小异。)
- capacity的代码在VS和g++下分别运行会发现:
- VS下capacity在开始会申请15个字节的空间,第一次扩容会capacity2倍扩容,之后capacity1.5倍扩容。
- g++下的capacity开始时是按需申请,size多大capacity就多大,capacity都是2倍扩容。
- VS是PJ版本STL,g++是SGI版本STL。
(4)clear成员函数
clear清空字符串的有效字符,使之成为空字符串(即长度为0)。
代码示例:
int main()
{
string s("hello string");
cout << "清空前:" << endl;
cout << s.size() << endl;
cout << s.capacity() << endl;
s.clear();
cout << "清空后:" << endl;
cout << s.size() << endl;
cout << s.capacity() << endl;
return 0;
}
运行结果:
tip:
- clear清空字符串的有效字符,不会影响capacity。
(5)reserve成员函数
我们知道扩容是有代价的,那有什么可以操作消除这个代价呢?
reserve申请一个长度为n的字符串容量。
代码示例:
int main()
{
string s("hello");
//申请100个字符的字符串容量
s.reserve(100);
cout << s.size() << endl;
cout << s.capacity() << endl;
return 0;
}
tip:
- reserve申请n个字符的字符串容量:
- 如果n大于当前字符串容量,则该函数申请扩容到n或比n大。
- 如果n小于当前字符串容量,则该函数申请缩容,注意该申请是不具约束力的,即底层的实现缩容或不缩容都可以(了解:字符串有数据的不会缩容,无数据会缩容)。一般我们不会缩容,因为效率低。
- reserve只是单纯的开空间,不会影响字符串的长度和内容。
(6)resize成员函数
resize将字符串大小调整为n个字符的长度。
代码示例:
int main()
{
string s("hello");
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << endl;
//将字符串大小调整为n个字符的长度。
//情况1:n大于当前字符串的长度——开空间+填值初始化
s.resize(100, 'x');
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << endl;
//情况2:n小于当前字符串的长度——将当前值长度缩小到n,删除第n个字符以外的值
s.resize(10);
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << endl;
return 0;
}
tip:
- resize将字符串大小调整为n个字符的长度:
- 如果n大于字符串长度,则开空间+填值初始化(第二个参数为char类型,在扩容时用于填值初始化。注意:当不传第二个参数时,使用\0填值初始化)
- 如果n小于字符串长度,会删除n后的字符,但不会缩容。
- resize与reserve的区别:
- resize是改变字符串的长度,reserve是改变字符串的容量。
- 如果长度大于容量,长度会影响容量,但是容量什么情况都不会影响长度。
2、string类对象的访问及遍历操作
函数名称 | 功能说明 |
---|---|
operator[] | 返回字符串pos位置的字符的引用 |
begin + end | begin获取第一个字符的迭代器+end获取最后一个字符下一个位置的迭代器 |
rbegin + rend | rbegin获取最后一个字符的反向迭代器+rend获取第一个字符前一个位置的反向迭代器 |
范围for | C++11支持更简洁的范围for的新遍历方式 |
(1)operator[]和at()成员函数
operator[]和at()的功能一样,都是返回字符串pos位置的字符的引用。
代码示例1:
int main()
{
try
{
string s("hello string");
//operator[]和at()都是返回字符串pos位置字符的引用
cout << s[0] << endl;
cout << s.at(0) << endl;
//operator[]和at()的区别
//s[18];//operator[]越界访问是断言报错
s.at(18);//at()越界是抛异常
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
tip:
- at()是早期string没有很好支持运算符重载产生的,一般情况我们更喜欢使用operator[]。
- at()和operator[]的区别:at()越界是抛异常,operator[]越界是断言处理。
- 自定义类型的[]与内置类型的[]在底层有本质的区别:
- 内置类型的[]是指针的解引用
- 自定义类型的[]是调用operator[]成员函数
- operator[]:
- operator[]必须是成员函数
- 为了与下标的原始定义兼容通常以所访问元素的引用作为返回值,优点是下标可以出现在赋值运算符的任意一端
- 如果一个类包含下标运算符,则它通常会定义两个版本:一个返回普通引用,另一个是类的常量成员并且返回常量引用
- 重载函数调用时,会走最匹配的。
代码示例2:遍历字符串
int main()
{
string s("hello");
//遍历
for (size_t i = 0; i < s.size(); ++i)
{
cout << s[i] << " ";
}
cout << endl;
return 0;
}
tip:
- 标准库类型限定使用的下标必须是size_t,而内置的下标运算符所用的索引不是无符号类型。
- string对象的下标必须大于等于0而小于s.size()。使用超过此范围的下标将引发不可预知的结果,以此类推,使用下标访问空string也会引发不可预知的结果。
- 使用下标运算符有一个前提,必须是连续的空间。
(2)迭代器
问题1: 迭代器是什么?
迭代器类似于指针类型,提供了对对象的间接访问,可以用来读写对象。
tip:
- 和指针类似,也能通过解引用迭代器来获取它所指示的元素,执行解引用的迭代器必须合法并确实指示着某个元素
- 注意:与指针类似,试图解引用一个非法迭代器或者尾后迭代器都是未被定义的行为
- 指针也是迭代器。但是我们不容易得到尾后指针(尾后指针指向容器尾元素的下一位置的指针),所以引入了返回迭代器的成员的函数。
- 迭代器的对象是容器中的元素或string对象中的字符。
问题2: 为什么引入迭代器?
- 迭代器是通用的,任何容器都支持迭代器,并且用法是类似的;而[]只有其中少数几种容器才支持,不通用。(虽然从历史角度来看string不属于容器,但是string和容器属于同一类,所以string支持很多与容器类似的操作)
- 算法可以通过迭代器,去处理容器中的数据。(算法需要操作容器中的数据,但是算法不能直接访问容器,因为数据一般封装为私有的,且每种容器结构逻辑上也不一样,需要一种通用的方式——迭代器去操作容器中的数据)
问题3: 怎么使用迭代器?
和指针不一样的是,获取迭代器不是使用取地址符,有迭代器的类型同时拥有返回迭代器的成员。
代码示例1:使用迭代器
int main()
{
string s("hello string");
//使用迭代器:有迭代器的类型同时拥有返回迭代器的成员。
//一般来说,我们不在意迭代器准确的类型到底是什么,所以使用auto自动推导迭代器的类型
auto it1 = s.begin();//auto——》string::iterator
auto it2 = s.end();//auto——》string::iterator
auto it3 = s.cbegin();//auto——》string::const_iterator
auto it4 = s.cend();//auto——》string::const_iterator
auto it5 = s.rbegin();//auto——》string::reverse_iterator
auto it6 = s.rbegin();//auto——》string::reverse_iterator
auto it7 = s.crbegin();//auto——》string::const_reverse_iterator
auto it8 = s.crbegin();//auto——》string::const_reverse_iterator
return 0;
}
tip:
- begin成员负责返回指向第一个元素(或第一个字符)的迭代器。
- end成员负责返回指向容器(或string对象)“尾元素的下一位置(one past the end)”的迭代器。
- 该迭代器指示的是容器的一个本不存在的“尾后(off the end)”元素。
- 该迭代器没什么实际含义,仅是个标记而已,表示我们已经处理完了容器中的所有元素。
- end成员返回的迭代器常被称作尾后迭代器(off-the-end iterator)或者简称为尾迭代器(end iterator)
- 如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器。
- 一般来说,我们不在意迭代器准确的类型到底是什么,使用auto根据右边的表达式自动推导迭代器的类型。
- begin和end有很多版本:
- 带r的版本返回反向迭代器
- 以c开头的版本则返回const迭代器(以c开头版本是C++新标准引入的,用以支持auto与begin和end函数结合使用)
- 当auto与begin或end结合使用时,获得的迭代器类型依赖于容器类型——如果对象是常量,begin和end返回const_iterator;如果对象不是常量,返回iterator。
- 建议:如果对象只需读操作而无需写操作的,最好使用cbegin和cend。
代码示例2:迭代器范围
int main()
{
string s("hello string");
auto it = s.begin();
while (it != s.end())
{
cout << *it << " ";
//移动迭代器,获取下一个元素
++it;
}
cout << endl;
return 0;
}
tip:
- 一个迭代器范围(iterator range)由一对迭代器表示,两个迭代器分别指向同一个容器中的元素或者是尾元素之后的位置(one past the last element)。这两个迭代器通常被称为begin和end,或者是first和last,它们标记容器中元素的一个范围。
- 迭代器范围中的元素包含first所表示的元素以及从first开始直至last(但不包含last)之间的所有元素。
- 这种元素范围被称为左闭右开区间(left-inclusive interval),其标准数学描述为[begin,end]表示范围自begin开始,于end之前结束。
- 迭代器begin和end必须指向相同的容器。
- end可以与begin指向相同的位置,但不能指向begin之前的位置。
- 标准库使用左闭合范围是因为这种范围有三种方便的性质。假定begin和end构成一个合法的迭代器范围,则
- 如果begin与end相等,则范围为空
- 如果begin与end不等,则范围至少包含一个元素,且begin指向该范围中的第一个元素
- 我们可以对begin递增若干次,使得begin==end
- 使用递增运算符将迭代器从一个元素移动到另一个元素。
- 注意:因为end返回的迭代器并不实际指示某个元素,所以不能对其进行递增或解引用的操作。
- 泛型编程:C++程序员习惯性在for循环中使用!=而非<进行循环结束条件的判断,是因为所有标准库容器的迭代器都定义了==和!=,但是它们大多数都没有定义<运算符。因此,只要我们养成使用迭代器和!=的习惯,就不用在意用的到底是那种容器类型。
代码示例3:迭代器的两大意义
int main()
{
//迭代器的两大意义:
//1、迭代器是通用的,任何容器都支持迭代器,并且用法类似
string s("hello");
auto it = s.cbegin();//①迭代器只读,不能写;②auto根据表达式推导迭代器类型为string::const_iterator
while (it != s.cend())
{
//cbegin与cend不等,范围非空
cout << *it << " ";
//移动迭代器,获取下一个元素
++it;
}
cout << endl;
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
auto vit = v.cbegin(); //①迭代器只读,不能写;②auto根据表达式推导迭代器类型为vector<int>::const_iterator
while (vit != v.cend())
{
//cbegin与cend不等,范围非空
cout << *vit << " ";
//移动迭代器,获取下一个元素
++vit;
}
cout << endl;
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
auto lit = lt.cbegin(); //①迭代器只读,不能写;②auto根据表达式推导迭代器类型为vector<int>::const_iterator
while (lit != lt.cend())
{
//cbegin与cend不等,范围非空
cout << *lit << " ";
//移动迭代器,获取下一个元素
++lit;
}
cout << endl;
//2、迭代器跟算法配合
reverse(v.begin(), v.end());
reverse(s.begin(), s.end());
return 0;
}
总结:迭代器提供了一种统一的方式访问或修改容器数据,并且算法可以通过迭代器,去处理容器中的数据。
(3)范围for
在前面C++入门我们已经讲解过什么是范围for了。
范围for是C++11支持的一种更简洁的新遍历方式。
范围for的语法格式是:
for(declaration:expression)
statement
tip:
- expression表示的必须是一个序列
- 注意:这个序列的范围必须是确定的,例如用花括号括起来的初始化列表、数组或者容器或string等类型的对象。
- declaration是定义一个变量,序列中的每个元素都能转换成该变量的类型
- 变量的类型我们一般使用auto类型说明符,auto关键字可以令编译器自动推导变量类型
- 如果需要对序列中的元素执行写操作,循环变量必须声明成引用类型
- statement可以是一条单独的语句也可以是一个块。
代码示例:
int main()
{
string s("hello string");
//使用范围for遍历string对象
//for的底层实现是迭代器
for (auto e : s)
{
//读
cout << e << " ";
}
cout << endl;
for (auto &e : s)
{
//写
++e;
cout << e << " ";
}
cout << endl;
return 0;
}
tip:
- 范围for的底层实现是用迭代器完成的,所以只要支持迭代器就可以使用范围for
- 注意范围for只能正向遍历,不能反向遍历。
- 反向遍历,可以使用反向迭代器
代码示例:反向迭代器
int main()
{
string s("hello string");
//反向遍历
auto it = s.rbegin();
while (it != s.rend())
{
cout << *it << " ";
//递增一个反向迭代器会移动到前一个元素
++it;
}
return 0;
}
tip:
- rbegin成员负责返回指向尾元素(即最后一个)的迭代器。
- rend成员负责返回指向首元素的前一个位置的迭代器。
- 反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。
- 对于反向迭代器,递增(以及递减)操作的含义会颠倒过来。递增一个反向迭代器会移动到前一个元素;递减一个反向迭代器会移动到下一个元素。
3、string类对象的修改操作
函数名称 | 功能说明 |
---|---|
push_back | 在字符串后尾插字符c |
append | 在字符串后追加一个字符串 |
operator+= | 在字符串后追加字符串str |
c_str | 返回C格式字符串 |
find | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置 |
rfind | 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置 |
substr | 在str中从pos位置开始,截取n个字符,然后将其返回 |
(1)push_back&append&operator+=成员函数
push_back:在字符串后尾插一个字符
代码示例:
int main()
{
string s("hello string");
//在字符串后尾插一个字符
s.push_back('!');
cout << s << endl;
return 0;
}
append:一般是在字符串后追加一个字符串
代码示例:
int main()
{
string s1;//空串
string s2("hello string");
//append的常见用法
s1.append(s2);//在s1后追加一个string对象
cout << s1 << endl;
s1.clear();
s1.append("66666");//在s1后追加一个c字符串
cout << s1 << endl;
s1.clear();
//append的其他用法(了解即可)
s1.append(s2, 5);//追加string对象的子串,子串从pos位置开始,取sublen个字符(sublen是缺省参数,默认为npos)
cout << s1 << endl;
s1.clear();
s1.append("hello", 5);//追加s指向的字符数组的前n个字符
cout << s1 << endl;
s1.clear();
s1.append(6, '6');//追加n个字符c
cout << s1 << endl;
s1.clear();
return 0;
}
operator+=:在字符串后追加字符串
代码示例:
int main()
{
string s1("hello");
string s2("string");
//operator+=不仅可以在字符串后追加字符串,还可以追加字符
s1 += ' ';//追加一个字符
s1 += s2;//追加一个string对象
s1 += "666";//追加一个C字符串
cout << s1 << endl;
return 0;
}
tip:
- operator+=不仅可以追加单个字符,还可以追加字符串。
- 一般operator+=就可以满足我们字符串尾追加的大多数情况,并且operator+=的使用更人性化,所以一般追加我们使用operator+=即可。
(2)c_str成员函数
c_str: 返回C格式字符串。
问题: 为什么string对象需要返回C格式字符串?
因为很多C++程序在标准库出现之前就已经写成了,而且有一些C++程序实际上是与C语言或其他语言的接口程序,所以无法使用C++标准库。因此,现代C++程序不得不与那些充满数组或C格式字符串的代码衔接,为了使这一工作简单易行,C++专门提供了一组功能。
简单来说,就是与旧代码做衔接。
代码示例:
int main()
{
string filename = "test.cpp";
filename += ".zip";
//与旧代码做衔接
//例如:fopen函数的第一个参数类型为const char*
FILE* fout = fopen(filename.c_str(), "r");
return 0;
}
tip:
- C格式字符串使用一个指针存储,该指针指向一个以空字符结束的字符数组。
- 指针类型是const char*,从而确保我们不会改变字符数组的内容。
- 注意:我们无法保证c_str函数返回的数组一直有效,事实上,如果后续的操作改变了s的值就可能让之前返回的数组失去效用。
- 建议:如果执行完c_str函数后程序想一直都能使用其返回的数组,最好将该数组重新拷贝一份。
(3)find&rfind&substr成员函数
find: 从字符串pos位置开始往后
找字符c(或字符串str),返回该字符(或字符串)在字符串中第一次出现的位置。
代码示例:
int main()
{
string url = "https://legacy.cplusplus.com/reference/string/string/";
// 协议 域名 资源名
size_t pos1 = url.find("://");
size_t pos2 = url.find('/', pos1 + 3);
cout << pos1 << " " << pos2 << endl;
return 0;
}
tip:
- 如果没有找到,返回string的静态成员变量npos。
- find常常与substr配合使用。
- 注意:pos参数默认为0,即默认从首字符开始找,但有些情况并不能从0开始,需要我们自己控制。
substr: 返回string对象一个子串,子串从pos开始,截取len个字符(len为缺省参数,默认值为npos)。
代码示例:
int main()
{
string url = "https://legacy.cplusplus.com/reference/string/string/";
// 协议 域名 资源名
size_t pos1 = url.find("://");
string protocol;
if (pos1 != string::npos)
{
protocol = url.substr(0, pos1);
}
cout << protocol << endl;
string domain;
string uri;
size_t pos2 = url.find('/', pos1 + 3);
if (pos2 != string::npos)
{
domain = url.substr(pos1 + 3, pos2 - (pos1 + 3));//len是一个左闭右开
uri = url.substr(pos2 + 1);
}
cout << domain << endl;
cout << uri << endl;
return 0;
}
tip:
- 左闭右开的范围,相减可以得到长度。
rfind: 从字符串pos位置开始往前
找字符c,返回该字符在字符串中的位置。
代码示例:
int main()
{
string s("test.cpp.zip");
//获取文件的后缀名
size_t pos = s.rfind('.');
string extension;
if (pos != string::npos)
{
extension = s.substr(pos + 1);
}
cout << extension << endl;
return 0;
}
tip:
- pos默认从最后一个字符开始往前找。(任何大于或等于字符串长度的值(包括string::npos)表示从最后一个字符位置开始)
加油站:string的其他不常用的修改操作(了解即可)
- assign:为字符串重新赋值,类似=运算符。
tip: 因为=运算符重载只有两个操作数,它只能有两个参数,所以assign的2、4、5、6不能重载。(但是实际中我们一般是将赋值内容构造成一个string对象,再通过operator=赋值)- insert:在字符串中pos(或p)指定的字符之前插入额外字符。
tip: 因为string的底层是字符数组的顺序表,pos位置之前的插入需要挪动数据,效率太低,所以我们不易多用。- erase:删除字符串的一部分。
- replace:将字符串pos位置开始的len个字符(或[i1,i2)范围的字符)替换成字符串str(或字符串s的前n个)等。
- **find_first_of:从pos位置开始往后在字符串中找与参数中指定的任何字符匹配的第一个字符,找到返回其位置,找不到返回string::npos。
tip:①find_last_of从pos往前;②find_first_not_of与find_first_of功能相反,找与参数中指定的任何字符都不匹配的第一个字符;③find_last_not_of。
4、string类非成员函数
函数 | 功能说明 |
---|---|
operator+ | 尽量少用,因为传值返回,导致深拷贝效率低 |
operator>> | 输入运算符重载 |
operator<< | 输出运算符重载 |
getline | 获取一行字符串 |
relational operators | 大小比较 |
(1)relational operators关系运算符重载
代码示例:
int main()
{
string s1("abc");
string s2("abd");
if (s1 == s2)
{
cout << "s1 == s2" << endl;
}
else
{
cout << "s1 != s2" << endl;
}
if (s1 <= s2)
{
cout << "s1 <= s2" << endl;
}
else
{
cout << "s1 > s2" << endl;
}
if (s1 >= s2)
{
cout << "s1 >= s2" << endl;
}
else
{
cout << "s1 < s2" << endl;
}
return 0;
}
(2)getline获取一行字符串
getline: 从流中获取一行到字符串,即从is中读取字符并将其存储到str中,直到找到分隔字符delim(或者对于(2)找到换行字符’\n’)。
字符串最后一个单词的长度
#include <iostream>
#include<string>
using namespace std;
int main()
{
string s;
//读取一行字符
getline(cin,s);
//找到最后一个空格的位置
size_t pos = s.rfind(' ');
//左闭右开,相减得出范围
cout << s.size() - (pos + 1) << endl;
return 0;
}
tip:
- cin读到空格或换行就结束,getline读到换行或指定分隔符才结束。
- getline是非成员函数。
- getline如果找到分隔符,则提取并丢弃它(即不存储它,下一个输入操作将在它之后开始)。
加油站:将数值转换成字符串,将字符串转换成数值。
- 将数值转换成字符串。
- 字符串转换成数值
三、标准库中的string类
1、string类
- 字符串是表示字符序列的类
- 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性
- string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(更多模板信息请参考basic_string)
- 注意,这个类独立于所使用的编码来处理字节:如果用于处理多字节或变长字节(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作
总结:
- string是表示字符串的字符串类
- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作
- string在底层实际是:basic_string模板类的别名:
typedef basic_string<char, char_traits, allocator>string;
- 不能操作多字节或者变长字符的序列。
2、basic_string类模板
tip: basic_string是string的一个类模板,适用于任何字符类型
为什么会将其设计为模板,这就需要我们了解编码了。
3、编码
- 在计算机中,所有的数据在存储和运算时都要使用二进制数来表示(因为计算机用高电平和低电平分别表示1和0)。
- 编码:确定一个字符和二进制数的对应规则,这就是编码。
(1)ASCII
计算机是老美发明,老美需要处理的字符只包括英文字母(大小写)、数字、标点符号、特殊字符等等,总共128个字符,所以老美就制作了一套ASCII编码。
tip:
- ASCII (American Standard Code for Information Interchange):美国信息交换标准代码是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。
- 标准ASCII使用一个字节存储一个字符,首位是0,总共可表示128个字符。
(2)GBK
ASCII对于老美还说完全够用了,但是我们中国文化博大精深,所以中国创造了一套GBK编码(汉字内码扩展规范,国标)。
tip:
- 汉字编码字符集,包含了2万多个汉字等字符,所以GBK中一个中文字符编码成两个字节的形式存储
- GBK兼容ASCII字符集
问题: GBK兼容ASCII,那中英混合,解码时如何区分是中文还是英文?
**GBK规定:汉字的第一个字节的第一位必须是1.(即看到是1两个字节为一个整体)
(3)Unicde
Unicode是国际组织制定的,可以容纳世界上所有文字、符号的字符集。
统一码(Unicode)是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。
tip: UTF-8
- 是Unicode字符集的编码方案,采取可变长编码方案,共分四个长度区:1个字节,2个字节,3个字节,4个字节。
- 英文字符、数字等占一个字节(兼容ASCII),汉字字符占用3个字节。
- UTF-8编码格式
(4)总结
tip:
- 字符编码时使用的字符集,和解码时使用的字符集必须一致,否则会出现乱码。
- 英文、数字一般不会乱码,因为很多字符集都兼容ASCII编码。
- 编码:把字符按照字符集编码成对应二进制数
- 解码:把二进制数按照字符集解码成字符
- 建议:使用UTF-8
- Windows下默认编码是GBK,Linux下默认的是UTF-8
3、类模板的实例化
tip: 现阶段我们学的string,是使用char实例化的。
string还有许多操作,这里就不一一列举了,需要的时候查看文档即可。