目录
前言:
1.stl的认识
stl的版本:
stl的六大组件:
stl的一些缺陷:
2.string的构成
3.Constructor接口
4.Capacity接口
length和size:
Maxsize:
capacity:
reverse:
resize:
5.Modifiers接口
push_back和append:
assign:
insert:
erase:
replace:
find:
库中的swap和string中的swap:
6.Iterators接口
迭代器:
反向迭代器:
const迭代器:
c++11规范:
7.Element access接口
at和[]:
front和back:
8.String operations
c_str:
data:
copy:很少用:
空间配置器:
find和substr:
rfind:
find-first-of,find-last-of,find-not-first-of,find-not-last-of很少用:
compare:
9.Non-member function overloads
operator+:
getline:
10.整形转字符串的一些用法:
总结:
介绍了很多,实际常用的就那几个,在string实现中会调重要的实现例如迭代器。
参考文档:string - C++ Reference
前言:
从本篇开始介绍c++中的stl标准模版库中的一些容器以及底层实现,由于是开始,所以在string和vector中还会提到这两个容器的使用(使用只会挑常用的介绍举例),会介绍的比较详细一些,再到list,deque,stack,queue,priority_queue都会将实现与介绍放到一起,因为有些内容就相似了。本篇还会简单介绍一下stl。
1.stl的认识
首先,stl(standard template library)是c++的标准模板库,是c++标准库的一部分。
stl的版本:
stl的六大组件:
stl的一些缺陷:
2.string的构成
可以看到string是一个basic_string<char>类型的typedef,string类出现的比较早,在stl模版库之前就有了。
这里的一些补充就是为了适应不同的编码,wchar_t是2字节的,char16_t是2字节的,char_32_t是4字节的。这里扩展一下不同的编码:
像像unicode统一码,UTF-16或UTF-32有时用2字节或者3字节表达某个国家的一个符号,UTF-32可能更统一,但没有UTF-8节省空间,且UTF-8兼容ASCII码,UTF-8更常用。ASCII仅是美国的标准,所以stl出现了不同的string类;而对于编码,国内发明了属于自己的编码GBK,主要采用双字节编,一个汉字用2个字节,可能生僻字会用3个字节:
int main(){char str[] = "博客";cout << sizeof(str) << endl;str[3]--;cout << str << endl;str[3]--;cout << str << endl;str[3]--;cout << str << endl;str[3]++;cout << str << endl;str[3]++;cout << str << endl;return 0;}
前2个字节编'博',后面编其他汉字,这里对第4位进行修改,可以发现都是k为声母,改第4位都是k什么什么,现在就能理解游戏里面的违规词是打不出来的,而且还能做到谐音的违规词也是不行的。
3.Constructor接口
在使用这些接口之前先认识一下,string的一些成员函数,以及对一些操作符的重载:
size()也就是返回的是这块字符串的有效字符个数(不包含'\0')。
返回下标对应的字符。
然后我们使用一下接口1.2:
int main(){string s1;string s2("hello world");string s3 = "hello world";//隐式类型转换cout << s2.size() << endl;for (size_t i = 0; i < s2.size(); ++i){s2[i]++;}cout << s2 << endl;for (size_t i = 0; i < s2.size(); ++i){cout << s2[i] << " ";}cout << endl;return 0;}
这里s1是无参的初始化,s2是带参的初始化,而s3是一个常量字符串赋值给给string类型的对象,并初始化,所以发生了隐式类型转换。
这里我们调试看看s2的内容:
这里的'\0'可以访问到,但是没有显示。这里的capacity是容量,后面实现会提到是怎么扩容的。allocator就跟内存池有关了,先不提。
使用一下接口3:
string s3 = "hello world";string s4(s3, 6, 3);cout << s4 << endl;string s5(s3, 6, 13);cout << s5 << endl;string s6(s3, 6);cout << s6 << endl;
接口3就是从pos位置取len个字符进行构造初始化,如果取的超出了范围就有多少取多少,知道字符串的结束。
这里的s6没有给第三个参数,会使用给的缺省值npos,而npos是什么呢?当我们见到文档中的一些不认识的,就去文档的其它地方找找,有可能是一个内容的typedef等等:
所以npos是一个类里面的静态成员常量,值为-1,并且是无符号整形,所以最大为42亿多,也就是超过了字符串的范围,所以直接取到字符串的结束,我们直接想取到字符串的结束就可以使用这个值。
接口5与6:
分别意思就是拷贝从开始的第n个进行构造与填n个字符。
4.Capacity接口
length和size:
二者一模一样,length更早,建议使用size。
由于string字符串不包含'\0',所以后面的size,capacity做下标都是表示最后一个有效位置(有效数据不包括'\0')的下一个,即'\0'(这个字符0可以访问到,但是调试看不到,不是有效的数据):
string s1("hello world");cout << sizeof(s1[s1.size()]) << endl;//1cout << s1[s1.size()-1] << endl;//d
Maxsize:
知道就行,64位最大40多亿字节。
capacity:
string s1("hello world");cout << s1.capacity() << endl;//15
结果是15,实际是16字节(不包含/0),为什么是16字节?因为返回值是整型,返回为字符串分配的大小,同样也不包含'\0'。简单来说就是给capacity开大小的时候开的是16字节,并且不包括'\0',但是返回的为给字符串分配的大小也就是15字节(同样没有包括'\0',其实string类的对象后面都有'\0',但是大部分情况都不包括)
reverse:
先介绍:
这里buf是为了不让小空间再去堆上申请空间而造成内存浪费,所以s是28字节,进而capacity想上堆上开辟空间起始就是32字节,然后vs按1.5倍继续申请,而g++又是2倍扩容,起始大小也不一样,后面会详细说。
vs观察扩容情况,插入数据capacity会自动扩容:
而我们提前使用reverse开好空间:
这里使用reverse就可以提前开空间,减少扩容,提升效率,vs这里可能会多开,而g++可能又正好100,但都不会低于100。
resize:
resize不但会扩capacity,还会对size初始化,这里初始化为100,多出来的补缺省值'\0',
如果想让多的空间初始化为其他值,再传一个参数即可。
注意,size变了但capacity不会变,缩容不能原地缩,因为缩容会去开一块新的空间去拷贝,再把原空间释放,节省空间了但是性能会付出代价,所以不会随意的缩容。
5.Modifiers接口
push_back和append:
相比上面两个字符或字符串追加,更喜欢用operator+=,但是实际都是尾插,而头插效率不好,需要扩容,有时需要配合reserve使用:
string s("hello world");cout << s.capacity() << endl;s += "world";cout << s << endl;cout << s.capacity() << endl;
assign:
给字符串赋值,不常用,了解即可 。
insert:
第一个insert是在开始的位置插入一个字符串,注意0是下标,所以是在开始;第二个是在字符串的下标5的位置插入一个字符,这个字符是空字符(不包括'\0',但是不能直接插入一个'\0'会报错);第三个insert就是在下标为5的位置插入一个空字符串,包括'\0';第四个insert使用了一个迭代器,begin指向第一个字符,由于迭代器底层是指针,所以加5指向第六个。
在string里面不推荐使用insert,能少用就少用,因为插入数据就要往后挪,往后挪数据效率低。
erase:
第一个erase是从第五个位置后面开始删除1字符;第二个是从第六个字符开始删除后面所以的字符;如果什么都不传,缺省值分别是0和npos,也就是全部删完(这里的npos跟前面的用法一样)。
如果不够删了,就有多少删多少,知道字符串的结尾:
erase也不推荐使用,删中间的还有挪动数据,效率低下。
replace:
替换某个位置的字符,能不用就不用,空间不够需要扩容,还要挪到数据。
5也是下标,5这里是被第一个替代位置的下标即空白字符,1是在原字符串里被替代的长度,如果不够替换,尽可能多的替换直到结束,'\0'不会被替换进去,反正调试的窗口不显示,不算有效字符。
find:
结合上面的replace,写一个将第一个空格替换为字符串的代码:
find找到返回对应的位置(也是下标),找不到返回npos,这也解释了为什么find的返回值类型是size_t。
那如何让所有的空白字符都替换成%呢?
先遍历一遍,看为了replace需要开多少空间,这里使用的reserve,resize如果替换的字符比较多,size的大小还会改变。
这里循环里需要让pos往下走,因为find是从头找的,第一个pos是空白字符的位置的下标即3,+1跳过替换的字符串(没有'\0')然后指向第一个替换字符后面的位置。
效率更快不用挪到的方法,但是是空间换时间了:
空间换时间,也可以给newStr提前开好空间,因为+=空间不够也会扩容,扩容需要不断的开空间拷贝,一步开好更好。
库中的swap和string中的swap:
需要进行深拷贝和赋值。
由于是string内部的,只需要改变指针的指向即可。
6.Iterators接口
迭代器:
迭代器是比较重要的内容,在实现部分也会重点使用,其实底层就是指针的封装(linux下是指针的封装,vs下是一个复杂的封装)。
迭代器目前理解成指针,左开右闭,end()指向最后一个字符的后一个位置,linux下是指针,vs经过封装。
范围for(底层是迭代器)也能访问每个元素,上面讲的string实现的[]重载也可以访问每一个元素。
反向迭代器:
这里的++rit是反向的,是往左走的。
const迭代器:
这里const的含义是表达*it不能修改(暂时理解为指针),it还是可以改的,和直接的const关键字不一样。
注意普通迭代应该用普通迭代器接收,const迭代用const迭代器接收,reverse迭代用reverse迭代器接收,同时也有string::const_reverse_iterator迭代器,我们可以使用auto来自动识别右边的类型,但是可读性不高:
c++11规范:
c++11为了规范const迭代器用const,const_reverse迭代器用const_reverse,但可能用的不多。
7.Element access接口
at和[]:
二者有些不一样,访问越界是,at会抛异常,可以用catch捕获,而[]里面实际含有assert,会直接报错。
front和back:
用[s.size()-1]等也可以实现back的效果。
8.String operations
c_str:
c_str返回const char*的指针,char*不会按照指针的形式打印地址,而转为void*就可以了。
string自己重载的<<会根据size打印,而c_str识别到'\0'就停止了(这里加个'\0'是因为string的的字符串本身不带字符0),识别字符0是因为它要兼容C,不然会出问题。
应用场景:
文件名类型是string,要用c_str来读,要识别到字符0。
data:
与c_str相似:
copy:
很少用:
空间配置器:
一般用不上:
find和substr:
从pos位置开始,pos也取,取后面的子串,用缺省值一样是有多少取多少。
取文件后缀:
substr取子串,从pos开始,往后取len个字符,suffix是后缀的意思。
取网络协议和域名:
rfind:
找到的位置是这个字符在这个字符串最后一次出现的位置。
应用:如果想取一个文件的真正后缀也就是最后一个后缀呢?
find-first-of,find-last-of,find-not-first-of,find-not-last-of很少用:
find-first-of:
find-not-first-of与上面的相反,找不匹配的。
find-last-of,找对应字符串中匹配的字符的最后一个,对应的not和它相反:
compare:
比较函数,几乎不会用,一般用重载的操作符:
这里的==的重载其实有点多余:
s1=="hello world"其实是跟s1==s2是一样的,因为对于单参数的构造函数(c++98),内置类型支持隐式类型转换,const char*可以转换为自定义类型
9.Non-member function overloads
operator+:
少用,+比+=多了传值返回,多了拷贝,在日期类的实现中介绍过。
getline:
应用:
题目想计算最后一个字符串的长度,但是遇到例如abcd a就不行了呢?
因为cin>>默认空格是分割两次输入内容的,这相当于只输入了一个字符串abcd,所以不行,这时就要用到getline了:
加上getline表示就算有空格输入也是一次的输入内容(不像cin),只有读到'\n'才结束第一次的输入。