💖作者:小树苗渴望变成参天大树
❤️🩹作者宣言:认真写好每一篇博客
💨作者gitee:gitee
💞作者专栏:C语言,数据结构初阶,Linux,C++
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!
文章目录
- 前言
- 一、什么是string类
- 1.1类的基本使用
- 1.1.1string类构造函数的使用
- 1.1.2string类的修改操作
- 1.1.3 string类对象的容量操作
- 1.1.4string类非成员函数
- 1.2string类对象的访问及遍历操作
- 二、总结
前言
今天我们来讲一个新的知识,让你开始进入STL的世界,让我们再也不想回到C语言的世界,今天的住店内容是带你体会string类的用法和常用的函数,下一篇在带大家模拟实现这个类,通过底层让大家更好的理解string类,话不多说,我们开始进入正文
一、什么是string类
在我们C语言阶段,我们怎么定义一个字符串?
char*p="nihao,C++";
char s[]="nihao,C++";
我们可以这样定义字符串,我们想要操作这个字符串要调用想要的函数,函数和这个字符串没有任何关系,这显然不符合类和对象的思想,按照我们oop思想,我们的字符串和对应的操作函数应该归为一类,所以这个时候出现了string类,他是管理字符串的一个类,给我们设计了很多操作方便的函数,让我们更好的操作函数,这也是我们为什么要学习string的一个重要原因,可以更好提高开发效率,
其实string就是一个顺序表来存放字符的,在后面的模拟实现也是和顺序表的定义是一样的,这恶鬼大家先理解一下。
接下来我带大家来看文档·,初步带大家了解string类,打开搜索string,就出现下面的界面
我们来看看他实现的函数其实有100多个
这样函数里面,我们不需要都记得,我会把常用给大家一一介绍展示一下,我们一个个的来看。
1.1类的基本使用
我们先来看看构造函数,string一共是实现7个构造函数,今天就简单的讲解几个:
1.1.1string类构造函数的使用
string s1;
string s2("hello world");
string s3(s2);
string s4("nihao,string", 5);
string s5(5, '@');
string s6(s2,5,10);
对于最后一个构造函数我们发现他多了一个缺省参数,我们来看看文档,他具体是什么
我的理解就是,如果你没有给截取子串的右区间,那么就默认一直截取到字符串的结尾,怎么保证每个字符串都可以截取到结尾,只能使用一个特别大的数来,截取到结尾也就停止了。
我们看到结果是-1,这里巧用了无符号整型这个关键字,-1其实是整型的最大值,这样就解决我们刚才说的那个问题,大家下来可以自己去测试一下他的效果
1.1.2string类的修改操作
我们来看文档介绍:
大家可以具体看看每个函数的功能,有的函数可能实现了运算符重载,有不同的功能,大家可以查文档看看。
对于+=运算符其实可以更好的替代push_back和append。
1.1.3 string类对象的容量操作
我们再来看文档:
size和length:
这两个其实是一模一样的效果,其实length是最先出来的,计算字符串长度是一个顺序表的结构,但是后面为了对接其他的结构,比如树,图等一些非线性结构的大小,使用size更好的表示大小,所以后面我们也是使用size多一些
capacity:
在文章开头,我提到过string是通过顺序表的结构来存放字符的,并不是你字符串多大,他开的空间就是多大,是根据不同的平台而定的
在vs下:
在Linux下:
我们上面写过追加字符串,我们有了容量的概念,肯定要进行扩容,我们来看看扩容机制是什么
在vs上:
在Linux上:
在不同的平台的扩容机制是不一样的。
reserve:
这个是为字符串预留n个空间,我们发现在上面循环100次就扩容了三次,扩容肯定会带来效率的降低,所以在我们大约知道字符串有多长的情况下,为他预先留出足够容量的空间。
他可以理解为扩容,在原字符串大小的基础下预留n个空间大小,并不会改变自身的大小,有效字符个数还是原来的个数,这个可以提高效率,减少扩容带来的消耗
我们看到当reverse的参数小于字符串本身的大小或者小于容量大小,并不会影响总大小和减少容量的,所以在底层的时候,这种情况直接返回,不做任何处理了。
resize:
是改变字符串的大小的,改变有效字符个数的,就传一个参数,用\0来填充,不然用字符来填充。
大家可以看到这是可以改变原字符串大小的,大于容量则会自动扩容,但参数小于本身大小会发生什么呢??
有效字符个数也变少了,但是容量不会改变,这个要和上面的reserve区分开,他两有很大的差距,可以简单理解为,一个在capacity上做事情,一个在size上做事情
clear:
这个函数是将有小字符个数清空,但并不会将容量改变,底层空间还是没有改变的
不能理解为把空间都清除了
empty:
字符串是否为空串
通过这个可以看出来我们的判断是否为空是根据有效字符个数,也就是size。
总结:
-
size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
-
clear()只是将string中有效字符清空,不改变底层空间大小。
-
resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
-
reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。
1.1.4string类非成员函数
我们之前再介绍了流插入和流提取的运算符,明白了为什么这个不能作为成员函数,因为不想使用隐藏的this指针,有的函数不止给string类去使用,比如swap,所以也不能写成内部函数,有的是创造者觉得放到外部会显得舒心些,接下来就简单介绍几个吧
operator>>和operator<<:
这两个流插入和流提取运算符重载大家应该有所了解,内部应该使用了友元函数,这样才能访问内部的私有成员,这个的实现其实和日期类的实现几乎差不多,等到底层的模拟实现再具体介绍吧,我们就来看他怎么使用的吧。
operator+:
这个函数为什么设计成外部函数呢??原因就是创造者想要的不止是两个字符串相加,也想实现字符之间的相加,如果写在内部,就必然会有this指针,是string类型的,所以创造者将他写在类外,方便更好的重载
可以理解为是字符串的拼接,我们尽量少用这种方式去操作,因为他是传值返回,有拷贝的消耗,希望大家可以理解
swap:
两个字符串的交换。
getline:
对于这个函数,因为我们目前值知道两个流,所以我将以输入的方式展示给大家,这是获取一行字符串
int main()
{
string name;
cout << "Please, enter your full name: ";
while (getline(cin, name))
{
cout << "Hello, " << name << "!\n";
}
return 0;
}
这个大家先了解一下,以后遇到再介绍,对于非成员函数大家应该知道了
1.2string类对象的访问及遍历操作
接下来讲讲字符串的遍历,既然字符串地城是类似于顺序表的结构,那么应该可以通过下标来遍历每个字符,我们有三种方式进行操作,对于字符串我们常用的第一种
第一种:按照for循环遍历字符串
类中实现了[]这个操作符,所以我们可以通过下标来访问每个字符,都是用字符引用返回,我们来看看怎么使用的:
我们的[]这个操作符就实现这两种重载,大家看看有啥区别
第二种:迭代器
这是一个非常重要的知识点,这个可以说是一个通用的遍历,只要是容器结构的都可以,一会通过两个例子给大家介绍一下,我们先来看看怎么遍历string:
int main()
{
string s1("hello world");
string::iterator sit = s1.begin();
while (sit != s1.end())
{
cout << (*sit) << " ";
sit++;
}
return 0;
}
可以把迭代器简单当成就是指针,**s1.begin()**得到字符串数组的首地址,s1.end()得到字符串数组的末地址,通过变量sit变量,类似于指针的操作,再string中体会不到他的优势,我们再其他两个结构里面看看,这个知识大家可能不了解,大家看看再说:
我们的链表和容器也都可以使用这种办法,我们的迭代器不止这些,上面只适合非const的正向遍历,还有其他三种方式,我们一起来看看
string s1("hello world");
cout << "非const的正向遍历:";
string::iterator sit = s1.begin();
while (sit != s1.end())
{
cout << (*sit) << " ";
sit++;
}
cout << "\n非const的反向遍历:";
string::reverse_iterator sit1 = s1.rbegin();
while (sit1 != s1.rend())
{
cout << (*sit1) << " ";
sit1++;
}
cout << "\nconst的正向遍历:";
const string s2(s1);
string::const_iterator sit2 = s2.cbegin();
while (sit2 != s2.cend())
{
cout << (*sit2) << " ";
sit2++;
}
cout << "\nconst的反向遍历:";
string::const_reverse_iterator sit3 = s2.crbegin();
while (sit3 != s2.crend())
{
cout << (*sit3) << " ";
sit3++;
}
这是再来看一下文档:
这个刚好四组:大家可以自己来记忆一下,这个后面用的特别多。
所以这是一种统一访问和修改数据的方法。
第三种:范围for
int main()
{
string s1("hello world");
for (char s : s1)
{
cout << s << " ";
}
return 0;
}
就是把s1的数据赋值给s然后再访问数据,也可以修改数据。
这三种方法相信大家应该都知道了吧,对于string类我们更偏向第一种,对于迭代器后面遇到再好好讲,反正大家一定监视过了,并且会使用一点了,对于范围for不是反向遍历,也是缺点之一。
二、总结
对于string的使用我就先讲到这里,前前后后应该讲了二十几个常用的函数,其余的我们可以查文档去使用就可以了,这个不是很难,大家要会使用才是关键,接下来的一篇我讲模拟实现一个string类,这样大家可以更好的理解string类。我们下篇再见