目录
一. 为什么学习string类?
1.C语言中的字符串
C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
2.string类
1. 字符串是表示字符序列的类
2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
3. string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信息,请参阅basic_string)。
4. string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)。
5. 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
总结:
1. string是表示字符串的字符串类
2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
3. string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>string;
4. 不能操作多字节或者变长字符的序列。
二. string类的常用接口说明
标红的是常用的
1.构造
在string类的成员函数中,最开始讲的是constructor (构造) 、destructor (析构) 以及operator (赋值)
我们主要来看一下构造
在C++98中,给与了7种方式,我们可以对照着后面给的注释分别来看一下
首先,第一种,不用多说,构造一个空的string类(即一个空的字符串)
第二种的参数是str,即另外一个string类 ,功能实质上就是一个拷贝构造
第三种的参数就变成了三个,分别为str、pos、以及len,其中pos指的是拷贝开始的位置(类似于数组的下标,同样是从0开始),而len指的是所拷贝的长度,而后面的npos则是缺省值
通过索引,我们可以知道,npos的值默认为无符号的-1,即2^31-1,由于我们在创建string类时,长度不可能这么大,所以我们可以当做在不写参数len时,默认为将后面所有拷贝进新的string类
第四种所传的参数则是一个类似于c语言中的字符串
第五种在第四种的基础上,增加了一个参数n,意为将字符串的前n个字符进行拷贝
第六种的两个参数n与c,指的是构造一个size为n的string类并初始化为字符c
第七种需要先掌握迭代器,暂时先放一放
我们可以来实践一下
void Test1()
{
string s1;
cin >> s1;
string s2(s1);
string s3(s1, 2, 3);
string s4("abcdef");
string s5("abcdef", 3);
string s6(5, 'a');
cout << "s1:" << s1 << endl;
cout << "s2:" << s2 << endl;
cout << "s3:" << s3 << endl;
cout << "s4:" << s4 << endl;
cout << "s5:" << s5 << endl;
cout << "s6:" << s6 << endl;
}
我们也可以通过监视来看一下string类中的成员变量的情况
当然,在allocator[6]的位置也是存有'\0'的
再往后的destructor(析构),没啥好说的,就固定的一种方式
而operator=的使用方式与拷贝构造类似,这里也就不多做说明
2.容量
后面的Iterators(迭代器)我们先放一放,先来讲一下Capacity(容量)
size和length
都是用来返回字符串的有效长度(即成员变量size)的,那么这两个接口有什么不同呢?没什么不同,那么为什么会存在两个同样方式的接口呢?这是因为,不只是string,其他容器同样也有大小,而就像树一样,它的大小不能使用length(长度)来表示,只能使用size,因此,为了与其他容器保持一致,string的接口就新增了size,而以前用于表示大小的length当然不能舍弃
capacity
返回容量大小(字符串总长度)(即成员变量capacity)
clear
说的也很清楚,清空string,而清空的是字符串中的有效部分
void Test2()
{
string s1("abcdef");
s1.clear();
}
clear前
clear后
empty
即判断字符串有效部分是否为空,空返回1,非空返回0
void Test2()
{
string s1("abcdef");
cout << s1.empty() << endl;
s1.clear();
cout << s1.empty() << endl;
}
reserve
通过解释,我们可以知道该接口是将对象的capacity变为n,当capactiy小于n时,直接扩增到n或者更大,当大于时,其实是一个未定义的行为,会根据编译器进行优化,而同时,不管怎么优化,都不能对字符串的有效部分进行改变。
void Test2()
{
string s1("abcdef");
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.reserve(14);
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.reserve(18);
cout << s1.size() << endl;
cout << s1.capacity() << endl;
}
很显然,我所使用的vs2022是没有对大于的情况进行优化的
而在s1.reserve(18)中,实际将capacity扩增到了31(加上'\0'为32),这是为什么呢?我们先来探究一下扩增的规律(涉及到后面的增添数据,可以之后返回来看)
void Test3()
{
string s1;
size_t sz = s1.size();
for (int i = 0; i < 1000; i++)
{
s1 += 'a';
if (sz != s1.capacity())
{
sz = s1.capacity();
cout << sz << ' ';
}
}
cout << endl;
}
在上面的代码中,我们将size在1000范围内所能扩增到的capacity的大小打印了出来
当然,capacity是不包含'\0'的,因此,我们可以将sz+1打印出来作为真正的大小
可以看到,除了第一次扩增了2倍以外,后面的扩增都大概遵循1.5倍的关系
而在reserve进行扩增时,也会从n向上找一个接近的值进行扩增,这也就是为什么我们上面会扩增到31
resize
简而言之,首先将字符串的长度(size)扩增到n,若是n<=capacity,就往原本字符串的末尾位置到位置n之间存放字符c(若是没有该参数,默认存放‘\0’),若是n>capacity,则先扩容在存放。
而要注意的是,当n<size时,size依旧会改变为n
void Test2()
{
string s1("abcdef");
cout << s1.size() << ' ' << s1.capacity() << endl;
s1.resize(5);
cout << s1.size() << ' ' << s1.capacity() << endl;
s1.resize(12);
cout << s1.size() << ' ' << s1.capacity() << endl;
s1.resize(18);
cout << s1.size() << ' ' << s1.capacity() << endl;
}
3.元素访问
operator[]
实质上就是下标访问操作符的重载,用法上也是类似
void Test4()
{
string s1("abcdef");
size_t i = 0;
while (s1[i] != '\0')
{
cout << s1[i++] << ' ';
}
cout << endl;
}
当然也可以改变一下循环
void Test4()
{
string s1("abcdef");
size_t i = 0;
for(i=0;i<=s1.size();i++)
{
cout << s1[i] << ' ';
}
cout << endl;
}
当然,由于该函数是传引用返回,我们也可以对其进行修改
void Test4()
{
string s1("abcdef");
size_t i = 0;
while (s1[i] != '\0')
{
s1[i] = s1[i] - 'a' + '1';
cout << s1[i++] << ' ';
}
cout << endl;
}
而我们可以看到,还有第二种方式,即当对象被const修饰时,返回类型也就变为const char&,这时就只能完成访问,而无法做到改变
而为了越界,operator[] 采用的方式是断言
void Test4()
{
string s1("abcdef");
size_t i = 0;
for(i=0;i<20;i++)
{
cout << s1[i] << ' ';
}
cout << endl;
}
at
用法其实和operator[]一样
void Test4()
{
string s1("abcdef");
size_t i = 0;
for(i=0;i<s1.size(); i++)
{
cout << s1.at(i) << ' ';
}
cout << endl;
}
而不同的点在于,at检查越界的方式是抛异常
void Test4()
{
string s1("abcdef");
size_t i = 0;
for(i=0;i<20; i++)
{
cout << s1.at(i) << ' ';
}
cout << endl;
}
front、back
一个是返回第一个字符,一个是返回最后一个字符,同样都可以访问,对没有const修饰的对象都可以进行改变,都无法对空的string对象进行使用,而它们与begin和end的不同就放在后面的迭代器里讲吧
void Test4()
{
string s1("abcdef");
cout << s1.front() << " ";
s1.front() = 'A';
cout << s1.back() << endl;
s1.back() = 'F';
for (int i = 0; i < s1.size(); i++)
{
cout << s1[i] << ' ';
}
cout << endl;
}
4.迭代器
begin、end
简单来说,begin是返回的字符串第一个字符的迭代器,end是返回的字符串最后一个字符的下一个的迭代器。
我们可以使用它们来完成遍历
void Test5()
{
string s1("abcdef");
string::iterator it = s1.begin();
for (it; it != s1.end(); it++)
{
cout << *it << ' ';
}
cout << endl;
}
同样,若是对象没有使用const进行修饰,我们也可以进行修改
void Test5()
{
string s1("abcdef");
string::iterator it = s1.begin();
for (it; it != s1.end(); it++)
{
cout << *it << ' ';
*it = *it - 'a' + '1';
}
cout << endl;
for (int i = 0; i < s1.size(); i++)
{
cout << s1[i] << ' ';
}
cout << endl;
}
这里再插一点,除了operator[]、at以及迭代器,我们也可以使用范围for来进行遍历
我们之前已经学过范围for来遍历数组,而在string类中也可以范围for
void Test5()
{
string s1("abcdef");
for (auto& e:s1)
{
cout << e << ' ';
e = e - 'a' + '1';
}
cout << endl;
for (int i = 0; i < s1.size(); i++)
{
cout << s1[i] << ' ';
}
cout << endl;
}
rbegin、rend
与begin、rend类似,不同的是rbegin返回的是最后一个字符的反向迭代器,而rend返回的是第一个字符前一个的反向迭代器
void Test5()
{
string s1("abcdef");
string::reverse_iterator it = s1.rbegin();
for (it; it != s1.rend(); it++)
{
cout << *it << ' ';
*it = *it - 'a' + '1';
}
cout << endl;
for (it = s1.rbegin(); it != s1.rend(); it++)
{
cout << *it << ' ';
}
cout << endl;
}
而我们或许会觉得,使用operator[]进行遍历就足够了,迭代器没有什么必要。
的确,在string中,operator[]的确更方便,但, 这并不是在所有容器中通用的,而迭代器是通用的
cbegin、cend、crbegin、crend
就是把begin、end、rbegin、rend中const的方式给单独摘出来了,没啥其他不同
5.增添、删除、修改
operator+=
实际上就是向后增添字符串,三种方式分别是增添string对象、 增添C形式字符串、增添字符
void Test6()
{
string s1("abc");
string s2("de");
s1 += s2;
cout << s1 << endl;
s1 += "fg";
cout << s1 << endl;
s1 += 'h';
cout << s1 << endl;
}
append
相较于operator,append使用的方式更多一些,大多数的使用方式其实与开始学的构造类似,可以推断出来,而第二种方式简单来说就是将参数str从下标为subpos的位置的长度为sublen的字符串增添到后面,其实参数就是在pos和len的基础上加了一个sub
void Test6()
{
string s1("abc"),s2("abc"),s3("abc"),s4("abc"),s5("abc");
string s6("def");
s1.append(s6);
s2.append(s6, 0, 2);
s3.append("defg");
s4.append("defg",2);
s5.append(3, 'e');
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
cout << s5 << endl;
}
push_back
相较而言,push_back就比较简单了
void Test6()
{
string s1("abc");
s1.push_back('a');
cout << s1 << endl;
}
assign
大概就是重新分配字符串,类似于赋值操作符,只是用法多一些
第一种直接赋值,第二种从下标subpos开始赋值sublen个字符,第三种C类型字符串,第四种C类型字符串前n个,第五种n个字符c
void Test7()
{
string s1("abcdef");
string s2, s3, s4, s5, s6;
s2.assign(s1);
s3.assign(s1, 2, 3);
s4.assign("abcedf");
s5.assign("abcedf",3);
s6.assign(3,'a');
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
cout << s5 << endl;
cout << s6 << endl;
}
insert
就是插入,也很容易理解
除去迭代器相关的,从上到下一次为pos位置插入str、pos位置插入str的subpos往后sublen长度、pos位置插入C类型字符串、pos位置插入C类型字符串前n项、pos位置插入n个字符‘c’
这些方法和前面的众多接口都是相似的,后面类似的就不举例子了
replace
大部分都关联到迭代器,就先不说了
swap
交换字符串,就一种方法
此外,swap还有非成员函数的重载
pop_back
删除尾部字符
6.字符串操作
c_str
说白了,就是将string对象转换为C类型的字符串,返回首元素地址,而要注意的是,不能改变
void Test8()
{
string s1("abcdef");
cout << s1.c_str() << endl;
}
find
查找,查找string对象、C类型字符串、字符
找到即返回第一个元素的位置,找不到返回npos
void Test8()
{
string s1("abcdef");
string s2("bc");
cout << s1.find(s2) << endl;
cout << s1.find("ef") << endl;
cout << s1.find("mn") << endl;
cout << s1.find("ef", 5) << endl;
cout << s1.find('f') << endl;
}
rfind
功能类似,逆序查找
void Test8()
{
string s1("abcdef");
string s2("bc");
cout << s1.rfind(s2,4) << endl;
cout << s1.rfind("ef") << endl;
cout << s1.rfind("mn") << endl;
cout << s1.rfind("ef", 4) << endl;
cout << s1.rfind('f') << endl;
}
我们可以看到第4个,不同于find需要所有字符串在0-pos的范围内,rfind只需要所查找的第一个字符在0-pos内即可找到
substr
从pos位置截断len长度的字符串作为string对象返回
void Test9()
{
string s1("abcdef");
string s2(s1.substr(2, 2));
cout << s2 << endl;
}
7.string类非成员函数
operator+
string对象能和C类型字符串相加
void Test9()
{
string s1("abc");
string s2("def");
cout << s1+s2 << endl;
cout << s1 + "def" << endl;
cout << 'a' + s2 << endl;
}
relational operators
比较运算符重载,和我们之前学的C类型字符串之间的比较一样。
operator>>
经典的流提取与流插入操作符的重载,我们在前面也或多或少的用到了
getline
在流插入中,与scanf有同样的问题,可以以空格为间隔符,这样就无法输入带有空格的字符串
void Test9()
{
string s1;
cin >> s1;
cout << s1 << endl;
}
而getline就能解决这个问题
void Test9()
{
string s2;
getline(cin, s2);
cout << s2 << endl;
}
end