️作者:@malloc不出对象
⛺专栏:C++的学习之路
个人简介:一名双非本科院校大二在读的科班编程菜鸟,努力编程只为赶上各位大佬的步伐
首先我要说明string其实并不是一个"真正"的类,在C++标准库中,std::string
是模板类 std::basic_string
特化而得到的一种类型。std::basic_string
是一个通用的字符串模板类,它可以用于处理不同字符类型的字符串,包括 char、wchar_t
等。
具体而言,std::string
是 std::basic_string
的一个特化版本,即使用 char
类型作为字符串的字符类型。但尽管 std::string
是基于模板类实现的,在用户角度上我们还是可以将其视为一个独立的字符串类,通过 std::string
,我们可以使用成员函数、操作符重载等方式来处理和操作字符串,而不需要关心其底层实现细节。之所以把这个概念抛出来是为了让大家对string
有一个更清晰的了解,而这也解决了一部分人对于string
为什么不是容器的疑惑,尽管 std::string
在某种程度上类似于容器,但它不是 STL 容器,而是标准库中专门用于字符串操作的类。
从图片上我们可以看到string类的构造函数是有很多种的,但是我们只需要了解重要的一些部分就可以了,其他的部分稍微了解一下即可。
string类的常见构造
constructor函数名称 | 功能说明 |
---|---|
string() (重点) | 构造空的string类对象,即空字符串 |
string(const char* s) (重点) | 用C-string来构造string类对象 |
string(const string&s) (重点) | 拷贝构造函数 |
string()是string类中的无参构造函数,从监视窗口我们可以看到它的size字符个数的起始为0,而capacity容量大小却是15,这是为何?
STL是一个标准,各商家根据STL标准开发了不同的STL版本,但是它们都能实现相同的功能,只是底层实现机制不同罢了。在VS中采取的是P.J.版本,G++采用的是SGI版本,在VS中capacity起始设定为15,(实际上是16,因为\0也需要占据一个空间,只是编译器未把\0算进来罢了,但是我们心中一定要有数),而在Linux中起始容量为0,大家不用太纠结这个问题。
这其实也很好理解,我们字符想要存起来底层一定是靠数组支撑起来的,那么这其中就必定会涉及到扩容等操作,在VS下一开始我们的容量大小就被设定为16,当size字符个数超过15时就不再使用buffer这个字符数组来存储数据了,而是通过重新new一块空间将这个长度为15的字符数组的内容拷贝到新空间上,而buffer这个字符数组里面存的什么内容我们就不再关心了。当容量满了之后又需要进行扩容操作,而不同的版本下扩容的机制又不同,这个问题后续我们谈到容量时再具体进行分析。
该构造函数是用常量字符串去构造一个对象出来,使用起来非常的方便。
另外我想问下面这段代码中string s3 = "hello world"
是调用拷贝构造函数吗?
从功能上我们确实看到s3
与s2
都接收了"hello world"
,但它实际上不是调用了拷贝构造函数,而是经编译器优化之后变成了调用构造函数。"hello world"
是一个常量字符串类型为const char*
,而s3
是一个string
类型,所以这里其实是发生了隐式类型转换,先构造出一份临时变量再进行拷贝构造,这里编译直接优化成了一个构造,要想禁止类型转换这种行为我们可以用explicit
修饰构造函数,另外我们可以看到cout <<
将自定义类型输出到显示器上了,说明这里<<
在string
中也实现了运算符重载。
关于string的构造函数以上三个是最常用的,以下的构造函数用的较少稍微了解一下即可!!
这个构造函数是用 n个字符来创建一个对象。
取前n个字符进行构造,如果取出的字符超过了字符串的长度,此时后面的内容是未知的。
通过上图的观察我有了一个疑问,从[11]开始后面的内容都是随机的,但是此时[11]的内容为\0,那么cout输出到显示器上的内容为什么还会出现后续的乱码呢???在C语言中我们不是说printf打印字符串直到找到\0就停止嘛,那这里为什么出现了这种现象呢?
下面我们在VS2019下测试一下\0在string中有什么体现?
结论:通过种种测试我们发现string中\0是不被记录的,说的再通俗一点在string类中\0不起任何作用,而且在不同的编译器下会有不同的现象,在VS2019中\0不会显示在控制台窗口,而在VS2013中\0充当一个空格在控制台中显示,string中cout<<重载操作符在底层实现其实是按照对应的size来输出内容的!!
但是string由于需要与C语言中的"字符串"保持兼容,所以就出现了c_str这个接口,它是返回C格式的字符串的函数,C风格的"字符串"是以\0结尾来输出内容的。
复制str中从字符位置pos开始并跨越len个字符的部分(如果str太短或len为string::npos,则复制到str的末尾)。
那如果我们想要取的字符个数大于从pos位置开始到字符串末尾的长度呢?会进行报错吗?
从上述图片中我们可以看到编译器没有报错,而且是将pos位置到末尾的字符全部取出来构造对象。让我们看看文档对这块的描述:
如果从pos位置开始截取的字符长度短于我们想要取得的字符长度那么有多少就取多少。
我们继续看向string (const string& str, size_t pos, size_t len = npos)这个半缺省构造函数,如果我们未传递实参给len,那么它将会使用npos这个缺省值,npos 是string的静态const size_t成员变量,其值为 - 1但是它是无符号类型的,所以它其实是一个很大的正数,字符串是达不到这个长度的,并且结合前面的文档我们知道如果 len 很大,就会用 pos 位置到末尾位置之间的字符来创建对象,所以第三个实参我们其实也是可以不传递的。
constructor函数名称 | 功能说明 |
---|---|
operator[] (重点) | 返回pos位置的字符,const string类对象调用 |
at | 返回对字符串中pos位置的字符的引用 |
font | 返回字符串第一个字符的引用 |
back | 返回对字符串最后一个字符的引用 |
begin+ end | begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 |
rbegin + rend | begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 |
范围for | C++11支持更简洁的范围for的新遍历方式 |
返回字符串中pos位置的字符的引用。
在C语言中我们使用[]越界访问可能还不会直接造成程序中止,有时候会出现随机值,而在string类中越界访问时直接暴力的使程序停止,这是由于operator[]在实现时采用了暴力的检查方法assert断言,一旦越界访问了直接中止程序!!
返回字符串中pos位置的字符的引用。该函数自动检查pos是否是字符串中某个字符的有效位置(即pos是否小于字符串长度),如果不是则抛出out_of_range异常。
返回头部和尾部的字符,我们平常用的非常少,稍微了解一下即可。
迭代器是一种设计模式,它是一个对象,它允许我们遍历一个容器(如数组、向量、列表、集合、映射等)中的元素,而不需要了解容器的内部实现细节。
C++ STL(Standard Template Library)提供了多种迭代器类型,如随机访问迭代器(Random Access Iterator)、双向迭代器(Bidirectional Iterator)、前向迭代器(Forward Iterator)和输入迭代器(Input Iterator)等,这些迭代器类型可以用于不同类型的容器,并支持不同的操作。使用迭代器,我们可以在不直接访问容器元素的情况下遍历容器,这种方式更加安全和方便。此外,C++ STL 还提供了一些算法,如 std::for_each()、std::transform() 和 std::accumulate() 等,它们可以帮助我们更加高效地处理迭代操作。
迭代器有两个基本操作,即 operator* 和 operator++。operator* 用于获取迭代器指向的元素,而 operator++ 用于将迭代器指向下一个元素。对于不同类型的迭代器,还可以支持其他操作,如随机访问迭代器还支持 operator+ 和 operator- 操作,用于将迭代器向前或向后移动指定的位置。目前关于迭代器我们了解的不是很多,现阶段我们就先简单的把它理解成一个指针。
正向迭代器
end 迭代器实际上是指向容器最后一个元素之后位置的迭代器,也就是一个指向容器尾部的迭代器,而不是指向容器最后一个元素后面的位置。在使用 STL 容器时,可以使用 end() 函数来获取该容器的 end 迭代器。需要注意的是,end 迭代器并不指向容器中的任何一个元素,而是表示容器的结束位置。在使用迭代器范围时,通常使用 begin() 和 end() 迭代器来表示容器的开始和结束位置,当迭代器到达 end() 位置时,迭代就会停止。
反向迭代器
const修饰的正向迭代器
这里的const修饰迭代器是从名字上体现出来的,它设计出来是为了防止迭代器指向位置的内容被修改。我们可以简单的举个例子:int a = 10; const int* p = &a; const修饰的是p指向的内容,它不可被修改。
const修饰的反向迭代器
如果我们想修饰迭代器本身就必须在类型前面加上const修饰,例如:int a = 10; int* const p = &a;const修饰的是迭代器本身,迭代器的位置不能发生变化。
范围for底层实际上是使用迭代器工作的,我们通过汇编来看看:
在 C++11 中,范围 for 循环的底层实现主要依赖于迭代器(Iterator)和自定义类型的能力。对于标准库中的容器(如数组、vector、list 等),它们都实现了迭代器,因此可以直接使用范围 for 循环,而对于自定义类型,如果要支持范围 for 循环,则需要实现对应的迭代器。
函数名称 | 功能说明 |
---|---|
size(重点) | 返回字符串有效字符长度 |
length(重点) | 返回字符串有效字符长度 |
capacity(重点) | 返回空间总大小 |
empty(重点) | 检测字符串释放为空串,是返回true,否则返回false |
clear(重点) | 清空有效字符 |
reserve(重点) | 为字符串预留空间 |
resize(重点) | 将有效字符的个数改成n个,多出的空间用字符\0填充 |
size 和 length 都是返回字符串的有效字符长度,它们都能达到目的,但是只有 string 类有 length 的函数,而其它容器只有 size 的函数接口,所以我们这里也是推荐使用size函数。
返回 string 类对象的容量大小的函数接口。
我们首先来看看VS以及G++下string类对象的大小:
我们发现在不同的环境下string类对象大小竟然不同,这是由于它们采用的版本不同,所以string类在底层实现上有所区别。以上结果采用VS32位环境以及G++64位环境,所以指针的大小是有区别的!!
string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字
符串的存储空间:
- 当字符串长度小于16时,使用内部固定的字符数组来存放.
- 当字符串长度大于等于16时,从堆上开辟空间.
这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建好之后,内
部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。
其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量
最后:还有一个指针做一些其他事情。
故总共占16+4+4+4=28个字节。
下面是简单的对VS下string类对象大小进行的分析:
G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指
针将来指向一块堆空间,内部包含了如下字段:空间总大小、字符串有效长度和引用计数。指向堆空间的指针,用来存储字符串。
关于G++下的string的底层实现方式,大家可以先看看这位大佬的一些文章https://coolshell.cn/articles/12199.html、https://coolshell.cn/articles/1443.html,关于string的模拟实现后续我们也将采用VS下的版本!!
下面我们来看看它们两者之间的扩容机制:
VS下:
下面我们通过调试来看看VS中的扩容机制:
G++下:
通过上述VS与G++的对比,我们发现在VS中起始容量为16,下一次在堆上开辟容量为32,之后再进行1.5倍扩容,而对于G++来讲一直是按照2倍进行扩容操作。
reserve可以修改 string 类对象的容量,在我们知道需要多大空间时我们可以直接利用reserve为我们开辟好空间,这样就可以减少扩容所带来的消耗。
VS下:
G++下:
将字符串大小调整为n个字符的长度。
通过上图我们可以发现reserve可以进行扩容,但是不会改变size大小,而resize既会改变capacity又会改变size。
void resize(size_t n, char c)
如果n大于当前字符串长度,则通过在末尾插入尽可能多的字符来扩展当前内容,以达到n的大小。如果指定了c,则将新元素初始化为c的副本,否则,它们是值初始化的字符(空字符)。
resize既然能修改size的个数,那么也就可以被用来"删除"数据,但是对象的容量不会发生变化,因为缩容是有一定代价的,必须重新开辟一块空间将数据拷贝过来再释放掉旧空间,这样会影响性能,所以即使我们将这段空间浪费掉也不考虑缩容。
shrink_to_fit()
在string类中我们提供了一个缩容的函数接口shrink_to_fit(),但是我们很少会使用它进行缩容就是因为缩容会影响性能的原因。
这个函数接口与size大小有关,容器实现可以自由地进行优化,并使字符串的容量大于其size,capacity的大小取决于扩容机制capacity的增长。
函数名称 | 功能说明 |
---|---|
push_back() | 在字符串后尾插字符 |
append | 在字符串后追加一个字符串 |
operator+= (重点) | 在字符串后追加字符串 |
c_str(重点) | 返回C格式字符串 |
find + npos(重点) | 从字符串pos位置开始往后找字符xxx,返回该字符在字符串中的位置 |
rfind | 从字符串pos位置开始往前找字符xxx,返回该字符在字符串中的位置 |
substr | 在str中从pos位置开始,截取n个字符,然后将其返回 |
operator += 运算符重载才是最经常用的函数接口,它使用起来简单方便可读性高!!
insert接口函数使用的非常少,因为插入数据只有在末尾处不需要挪动数据,其他位置前插入都需要挪动数据,时间复杂度为O(N),这样会影响效率。
同样的erase接口函数也要尽量少使用,因为删除数据只有在末尾处不需要挪动数据,其他位置进行删除都需要挪动数据。
这个接口函数我们也是不推荐去使用的,因为它可能需要挪动数据和进行扩容操作。
在字符串中搜索由其参数指定的序列的第一次出现,find 函数是一个查找的函数接口,是比较重要的函数接口,需要熟练掌握。
与find的功能相反,在字符串中搜索由其参数指定的序列的最后一次出现。
功能:截取pos位置开始截取len个长度的字符返回
实现截取最后一个空格位置后面的字符:
substr与find经常结合进行使用,我们要熟练掌握它们的用法。
以上就是本文的所有内容了,如有错处或者疑问欢迎大家在评论区相互交流orz~