一、使用C++文档查看函数功能
1. 网站查看C++手册
a.cplusplus网站
链接:cplusplus旧版网站
也可以直接搜索cplusplus.com进里面转旧版本
旧的版本可以更好的搜索各容器、函数
b. cppreference网站
链接:cppreference网站
如果英文不好看,翻到底下可以选语言,也可以在网址框出现的en前缀改成zh
习惯性查看英文也好,翻译不一定正确,一些专业术语翻译会出错,看文档要会连蒙带猜
2. 搜索string容器,查看容器所拥有的函数
cplusplus为例,也可以往下找,能发现string容器
进去之后会展示多个string容器,我们通常使用最普通的,其他是针对不同格式的
string简单的可以分为几个区域:
我们需要完成框里的函数,了解功能并简单实现该功能
在函数名后面的解释中也能简单了解该函数功能
3. 了解函数
string容器是类似于数据结构中的顺序表的存储,完成的也是动态数组的管理
a.构造函数
(constructor)构造函数,了解过类的会懂多一点,简单来说是初始化类对象的函数
我们不需要重载那么多,只需要重载三个,可以结合成两个构造函数
分别为:
(1)构造空的类对象(空内容)
(2)以另一个对象构造该对象
(4)使用字符串构造该对象
其他的不实现,但可以简单了解:
(3)使用另一个对象内容从pos开始的len个字符构造该对象,默认到最后
(5)使用字符串的前n个字符构造该对象
(6)使用n个字符c构造该对象
(7)迭代器一段区间的内容构造该对象
下面就不讲那么细了
b. 析构函数
关键在于对申请的内存的释放
c. 赋值重载
两个以已存在的对象,使用其中一个赋值给另外一个
只需完成第一个,第二个是会有隐式类型转换的,会转换成string类型,第三个不考虑
d. begin()、end()
返回的是迭代器的开始与结束,这里使用指针实现,返回地址就好了
end()函数与之相同,返回的是最后一个字符的下一个位置(也就是’\0’位置)
要实现const,避免出现const的string对象返回地址是可以修改内容的(权限放大)
e. size()、capacity()、clear()、empty()
不上图了,看文档
size()是返回字符的个数,以数组来可以说是返回'\0'的下标
capacity()是返回容量,已经申请了多少内存,大于等于字符个数,通常用来判断要不要扩容
clear()清除数组内的字符,可以不覆盖,只需要把有效字符个数size设置为0
empty()判断有效字符个数是否为0,如果为0返回true,否则返回false
f. resize()、reserve()
resize():
(形参中的n代表着把size大小调整到n个,第二个重载中的char c是一个字符,用来填充的)
(1)如果resize()中的n小于原本的size,就把size调整为n
(2)如果resize()中的n大于原本的size并且小于capacity容量,把size调整到n,如果有c就用c填充扩展的那部分,如果没有就'\0'填充
(3)如果resize()中的n大于capacity,那就扩容,size调整到n,看是否有填充,扩容机制看人家的实现
reserve()函数就比较一般,就是为了扩容,如果比capacity小就不动作,比capacity大就扩容并更新capacity
g. 运算符重载[]、关系运算符>、<、==等一些判断的运算符
[] 运算符重载,可以让类对象类似于数组下标访问,例如string对象的str,可以str[i]访问
关系运算符重载可以让两个同类型的类对象比较,例如str1 >= str2,方便判断
h. 运算符重载+=、append()
运算符重载是主要重载两个,一个是+=字符串,一个是+=字符,+=string不考虑,因为它可以导出字符串
append()函数只简单完成从尾部追加字符串,普通的使用str.append("hello word");
i. push_back()、pop_back()
push_back()是在最后插入一个字符
pop_back()是删除最后一个字符
使用是
str.push_back('a');
str.pop_back();
两个如果在顺序表中规范实现是的时候是很常见的
j. insert()、erase()
文档中的insert()有点多重载,主要的功能不过是插入字符串和字符而已
里面插入字符总想着插入几个同样的字符,没必要
不常用,常用的就插入字符串,插入单个字符就"s"插入就好了
如:
str.insert(pos, "s");
str.insert(pos, "hello");
在什么位置插入什么字符串,单个字符用双引号也是字符串
erase()就比较简单了,就为了删除某个位置的向后多少个字符
第三个就是删除某个区间的字符
k. c_str()、find()
c_str()就是返回字符串的第一个字符的地址,从string返回char*
find函数我们重载两个:
(2)从string里面的pos位置查找字符串s,找到返回在string中存在字符串s的第一个位置的下标
(4)从string里面的pos位置查找字符c,找到就返回在string中的下标位置
两个重载,如果找不到就返回无符号整型最大值(-1)
l. swap()
swap()函数是两个string的内容交换,字符串交换,size和capacity交换
在内部简单实现的话可以指针交换和两个变量的交换
m. 流输出<<运算符重载与流输入>>运算符重载
流输出与流输入重载是为了让对象能像普通内置类型一样正常输入输出
在这如果不自己实现,那么就无法像上面一样使用,因为string也是自定义类型,编译器不知道输入的东西会放到哪
其中第一个参数输入输出流的对象,第二个是string对象,无法在类内部实现
因为如果在内部实现,左边参数一定得是string类型,这就不符合流的使用了,而且与函数名不相符
n. getline()
getline()函数是为了一行输入,因为cin >> strign; 是遇到空格或者换行回车就结束了
这个函数能拾取空格,到换行就退出写入
第一个中最后一个参数是,遇到delim字符就退出写入
第二个默认是遇到换行回车就退出写入
二、模拟实现常用成员函数
1. 函数需要的成员变量及各自功能
看这结构和数据结构中的顺序表大差不差,指针、容量、大小
容量和大小听着相同,但是各自负责的功能不同
_str是一个指针,这个指针用于指向动态开辟的内存,主要是负责存储字符和'\0'(也可以说字符串)
capacity是一个用来说明开辟的空间的大小的变量,通常来讲,开辟的会比记录的大1,这个空间用来储存'\0'
size是一个说明已经存储了多少个字符的变量,不包含'\0',但当成下标时就是'\0'的位置
npos是静态变量,并且无法改变,size_t是无符号整型,-1和无符号整型对比时会转换成整型最大值
这里有缺省值的存在,这个是为了防止空构造时指针是野指针
2. swap()
这swap函数重载了一个,为了防止出现,s1.swap(s1, s2);
调用了std命名空间的swap函数,主要功能是互换两个指针指向,互换size,互换capacity
2. 构造函数、析构函数、拷贝构造、赋值重载
构造函数使用全缺省,为了应付string str;的空构造
:主要是申请空间,初始化capacity和size,当有字符串时也能构造一个完整的string
拷贝构造函数是一个已经存在的string对象初始化另外string对象赋值
:使用了已经存在的string对象的指针指向的字符串构造一个string对象,然后this和构造出来的交换内容
:这思维比较先进,还能利用构造函数,不用自己写申请空间啥的
赋值重载是两个已经存在的对象,一个给另一个赋值
:函数的参数列表可以看到是没有引用的,所以在进入赋值重载的时候已经走了一遍拷贝构造
:这时s是临时string对象,但对象里的申请的空间可是new出来的,所以下面走了一遍swap就能交换内容了
:这个交换不影响外面s1 = s2 的s2,swap(s)等价是this->swap(s)
析构函数是对象出了作用域时自动调用的
:为了内存不泄露,里面理应释放申请的空间_str指向的空间,下面的清空是要养成一个好习惯
多看这前三个函数,会发现不一样的感觉,代码复用率高,所用的时间不增加
可以提升对构造函数的理解
3.reverse
查看文档中的reverse的实现会知道,reverse是用来扩容的,可以通过传参来知道是否要扩容
reverse里面使用判断传进来的n和容量capacity比较,如果大于就扩容,反之就什么都不做
扩容的过程是:
新开一片空间,把数据拷贝过去,释放之前旧的空间,把_str指向过去,更新_capacity
如果有兴趣也可以实现缩容
4. resize()
resize不记得了可以看一下上面的介绍规则
resize主要负责调整里面字符的个数,如果比capacity小就不会影响总容量
使用断言来提示resize传参不能为负数
这里总体分为三段,分别位:
1、n比_size小,在_size位置替换为'\0'
2、n比_size大,并且比_capacity大,走reserve扩容,并且_size到n的位置使用字符c补充
3、n比_size大,但比_capacity小,不走扩容,并且_size到n的位置使用字符c补充
还有一个就是如果和size一样就啥都不做了
5. push_back()
push_back()是在最后插入一个字符
push_back是在最后插入字符,_size就是记录最后一个字符的下一个位置
所以可以在_size位置插入字符之后让_size往后调整一位,并重新在_size位置插入'\0'
但其中要考虑,如果对象里的容量不够了需要扩容,扩容的大小看个人,通常是2倍或1.5倍
判断的就是_size与_capacity是否相等,可以自己画图思考一下为啥这样子判断
这里扩容可以复用reserve
6. pop_back()
pop_back是删除最后一个字符
很简单的实现,pop_back是不需要传参的,最后一个位置是_size的前一个位置
我们可以把_size往前挪一个位置之后把_size位置覆盖成'\0'
但是其中要判断对象里面是否有数据,不然_size往下减就越界了
7. insert()
insert()的重载有点冗余,我们只简单实现插入字符串和字符就行了
insert的主要功能是在什么地方插入字符/字符串
insert插入(字符)主要过程是:
1、插入前判断容量够不够,需不要扩容
2、根据插入的位置,从后向前挪动数据,把pos位置的字符往后挪动之后在pos位置插入字符c
3、更新_size,让_size加1,容量已经在复用reverse之中更新了
insert插入(字符串)主要过程是:
1、计算传进来的字符串大小length
2、使用总容量_capacity减去大小_size之后的结果与length对比得出剩余的空间是否足够,不够就扩容
3、挪动足够的空间插入字符串(挪动范围可以看图计算)
4、更新_size
最后都需要返回该对象的引用
8. erase()
erase是删除pos位置向后多少个字符
erase主要过程思维:
1、断言pos不能越界
2.、判断pos+len是否大于_size或者len是不是npos,大于或等于就直接在pos位置覆盖成'\0',并且把_size更新到pos位置
3、pos+len小于_size,把pos+len之后的数据移动到pos及之后的位置,移动完之后更新_size
4、返回该对象的引用
9.+=运算符重载与append()
+=运算符重载可以认为是在最后插入一个字符或者一个字符串
append就是在最后插入字符串,和+=字符串一样
这里的两个+=运算符重载可以自己手搓,也可以复用insert()在_size位置插入字符或者字符串
因为根据a = b += c;可知道+=出来是可以给其他的变量赋值,所以这里有返回值,返回引用是减少调用拷贝构造
append就是在最后插入字符串,和+=字符串一样,可以直接复用insert(),该函数可以有返回值,也可以没有
10. []运算符重载
方括号[]运算符重载是为了对象像数组一样访问
这里重载了两个是因为普通对象和const对象,const对象是可以访问,但不能修改,所以返回了const char&
首先assert断言防止越界访问,其次直接返回对象里的数组对应的下标的引用
11. 关系运算符重载
主要是两个对象的内容(字符串)的对比
这里主要是对字符串的对比,可以使用strcmp函数来判断大小
自己实现了两个之后可以复用这两个来完成另外的运算符重载
12. size()、capacity()、clear()、empty()、c_str()
我们可以看名字就大概了解这几个函数的作用了
大小、容量、清除、是否为空
size(), capacity()主要功能也是返回大小、容量,能让使用者大概了解底层
clear()就是清除对象里面的数据,因为底层是字符数组,所以直接_size更新为0,并且_size位置放入'\0'就行了
empty()是判断对象是否有内容,直接判断_size是否为0就行了
c_str()是返回指针,因为我们之前每次插入都会在最后插入'\0',所以可以直接返回数组首元素地址就好了
13. find()
find是查找字符/字符串从pos位置开始第一次出现的位置
查找(字符)从pos开始第一次出现的位置:
1、断言pos是否越界
2、从pos开始循环遍历底层数组,判断是否出现该字符,如果出现直接返回该位置,如果遍历完都没找到就返回npos
查找子串(字符串)从pos开始第一次出现的位置:
1、断言pos是否越界
2、使用函数strstr函数偏移pos开始查找子串,并且用另外一个指针接收
3、如果接收的指针为空就返回npos,否则返回接收的指针和_str相减(指针相减得到直接相离的个数)
14. begin()、end()
begin()、end()是返回开始位置和最后一个字符的下一个位置的迭代器
这里说迭代器可能不理解,但其实迭代器就是屏蔽底层的差异,把迭代器类似于指针的使用
迭代器是为了更方便使用者的使用,可以屏蔽底层类似于指针的使用
例如:如果是链表,我指针是可以变量,但是我指针解引用*node能直接拿到里面的东西吗?
++node可以移动到下一个结点吗?答案是不可以,但是stl库里面链表迭代器是可以这样子使用的
这里的底层因为是数组,空间是连续的,我们可以直接用指针实现(改个名字)
指针可以++/--,也不需要继续完善其他什么语法问题,解引用是可以直接拿到值
那么迭代器移动可以了,解引用可以了,并且const对象也实现了const的函数不能修改里面的内容
14. 流输出<<运算符重载与流输入>>运算符重载
流输入流输出运算符重载是为了让自定义类型能像普通内置类型一样的输入输出
例如:
流输出:
这里流输出比较简单,可以用于范围for遍历(本质也是迭代器)输出
还可以直接_cout << s.c_str();直接输出
返回_cout ,为了实现类似 cout << a << b;的多组输出功能
流输入:
流输入也不算麻烦,但是为了少些扩容就在里面定义了缓冲区
1、clear()清空数据
2、定义中间变量ch和缓冲区buffer[128]数组,定义i下标防止越界
3、循环,把ch的值放假buffer缓冲区,判断buffer是否满(多留一个空放'\0'),满了可以复用+=来往对象写入,并且重置i
4、重新获取字符到ch
5、退出循环之后判断i来知道buffer里面是否有数据,有就再用+=写入对象
6、返回_cin,为了能实现类似 cin >> a >> b;的多个输入
15. getline()
主要实现第二个,当然,也可以参数列表半缺省实现第一个函数
getline()实现和>>流输入>>运算符重载一样,只不过是循环的判断条件少了空格而已
也是使用了buffer缓冲区来防止多次扩容
三、总结
如果不懂如何使用可以一边看文档一边使用,多用就会了
这里简单实现是无法和库里面的对比的,特别是完善程度上无法对比
但是库中的string容器的函数实现比较冗余,我们注意的是关键的函数
在函数使用上vector也类似,可以自己拓展vector的使用,vector也是类似于顺序表
加油吧,少年