1. STL是什么
STL(standard templete library-标准模板库);是c++标准库的重要组成部分,不仅是一个可以复用的组件库,而且是一个包含了数据结构和算法的软件框架。
最主流:容器,算法
其中,容器就是数据结构。
算法就是排序,交换之类的。
2. string 是什么
2.1. 引入
c语言中,字符串是以 ‘\0’ 结尾的一些字符的集合。为了方便操作,c标准库中提供了一些 str 系列的库函数,strlen,strcmp,stract,strcpy。
但是这些库函数与字符串是分离开的,字符串可以随意创建,但是关于字符的函数在string.h 的头文件中。
而且 字符串的空间还需要用户自己管理,稍不留神可能就会越界访问。
2.2. string 的优势
string 是管理字符的顺序表。
可以理解为c++库中有一个关于字符串的类,需要的时候可以随时调用。
在c++中,又关字符串的题目通常是以 string 类的形式出现,而且在常规工作中,为了简单,方便,快捷,基本都是使用 string 。
- 这里使用cplusplus网站上的文档,来使用对应的函数。
以下是 string 所支持的函数
下面会对string中的函数类别进行简单说明。
- 支持的类型
- 默认构造函数
其中,因为函数重载,每个函数内都有多个函数重载,方便类型的识别。
比如默认构造函数,拷贝构造函数
这里不仅支持单个字符,也支持字符串,常量字符串,还有多种情况。
析构函数
3. 迭代器
4. 容量
5. 大小关系
string 中一共设计了100多个函数,不过并不是所有的函数都有用,下面会介绍些比较常用的函数。
3. stirng 的使用
3.1. 基本使用
string 需要包头文件 string.h
string 也是个类模板,原型为 basic_string的类。
因为平时使用的是字符串,所以这里直接用char类型实例化了。
3.1.1. 创建string对象
- 构造无参的string
void test_string1()
{
string s1;
}
- 构造有参的string,可以使用第4种,使用常量字符串去初始化
void test_string2()
{
string s2("hello");
}
在string的库函数中,也实现了流插入和流提取,所以直接使用cout和cin输出和输入即可。
void test_string1()
{
string s1;
string s2("hello");
cin >> s1;
cout << s1 << endl;
cout << s2 << endl;
}
string的字符串,空间是动态开辟出来的,也就是说没有静态数组容量不足或者大量浪费空间的问题。
而且含有构造和析构,也不需要我们手动开辟和释放,比c语言中提供的字符串方便的多。
3.1.2. 拼接字符串
库函数内也实现了 operator+的操作。
同样,也有函数重载,方便传入不同的类型。
对象+对象,对象+常量,对象+字符,函数重载都支持。
void test_string1()
{
string s1;
string s2;
cin >> s1;
cin >> s2;
cout << s1 << endl;
cout << s2 << endl;
string s3 = s1 + s2;
cout << s3 << endl;
}
输入 “我的”和“世界”,+ 就会自动将两个字符串拼接在一起。
operator + 不仅支持对象和对象的拼接,也支持和常量字符串相加。
string s4 = s1 + "new world";
cout << s4 << endl;
虽然c语言中也含有拼接字符串的函数 strcat ,但是可读性没有 ‘+’ 强,这方面string又比c语言中的字符串强。
- 隐式类型
构造函数,也可以采用隐式类型转换的形式进行初始化。
void test_string()
{
string s1("hello");
string s2 = "hello";
}
第一个直接拷贝构造
第二个构造+拷贝构造+优化。
3.1.3. 遍历字符串
operator[]也有定义
string 实现了 operator[] 的重载,所以我们访问数组内的元素,可以使用下面的方法
void test_string()
{
string s1("hello");
cout << s1[3] << endl;
}
想要遍历string类,库函数内也有返回字符串长度的函数。
void test_string2()
{
string s1("hello");
for ( int i = 0; i < s1.size() ; i++ )
{
cout << s1[i] << " ";
}
}
这里我们调用的 operator[] 传入的是没有 const 修饰过的,所以这里可以传入引用,直接诶对返回值进行修改,从而改变字符串中的元素。
void string2()
{
string s1("hello");
for( int i = 0; i < s1.size() ; i++ )
{
cout << s1[i] << " ";
s1[i]++;
}
cout << endl;
for (int i =0; i < size(); i++ )
{
cout << s1[i] << " ";
}
}
3.1.4. 迭代器
迭代器iterator是遍历顺序结构的一种方式
begin是返回开始位置的指针,end是指向最后一个字符的下一位,它们都是函数。
这里使用迭代器可以完成对数据的输出
string::iterator it = s1.begin()
while( it != s1.end())
{
cout << *it << endl;
++ it;
}
cout << endl;
这里的++ 也被重载了,所以能指向想要指向的位置。
这里只看最后一行就行,上面是遍历数组中的输出结果。
数组也可以这样写
while( it < s1.end())
{
//...
}
但是最好不要这样写。
迭代器是一种遍历顺序表的结构,也就是说,不管是现在学的数组,还是后面学的list(c++中实现的链表结构),也可以使用相同的写法对链表进行遍历。
string 在物理空间上是连续的,尾部的地址绝对是在最后面,所以这种写法可以使用。
但是 list 在物理空间上不连续,有可能尾节点的物理空间会在头结点前。对链表的指针进行判断 it<s1.end(),可能是没有意义的。
it != s1.end();
上面的写法才是通用的写法。
迭代器是所有容器都可以使用,[] 相对来说只是小众的写法(如果是树的话,拿数组的写法遍历看起来比较别扭)。
- 反向迭代器
上面是正向迭代器,我们输出的内容都是按照字符串的正向顺序进行输出。
如果需要反向输出字符串的内容,也可以。
需要使用下面的 rbegin 和 rend 函数
void test_string3()
{
string s1(“hello world");
string::reverse_iterator rit = s1.begin();
while( it != s1.rend() )
{
cout << *ret << " ";
++rit;
}
cout << endl;
}
rbegin 是在最后一个字符的位置,rend是在第一个字符前。
但是rit的方向不是–,而是++
一般情况下,倒着遍历都会用反向迭代器,也可以对 rit --,进行正向输出,但是用起来不方便。
string::reverse_iterator rit = s1.rbegin()
上面定义 rit ,前面的数据类型很麻烦,但是我们知道 rbegin 肯定会返回这种类型,因此这里直接使用 auto 来定义 rit 。
auto rit = s1. rbegin()
3.1.5. 范围for
它会自动获取容器或者数组的范围,然后进行输出
自动判断结束,自动++
原理:编译器自动替换成迭代器
void string15()
{
string s("hello world");
for (auto ch : s)
{
cout << ch << " ";
}
cout << endl;
string::iterator rit = s.begin();
while (rit != s.end())
{
cout << *rit << " ";
rit++;
}
}
上面是范围for的输出,下面是迭代器的输出
可以看见,范围for 和 迭代器都调用了两个很相似的函数,间接说明范围for和迭代器本质上差不多,就是范围for相对来说写起来方便。
3.1.6. 传入参数
void func(string s)
{
}
一般情况下,不建议这样传参,这样传会有拷贝构造,会降低效率。
可以传入const修饰的引用,但是传入const 又会有新的问题
void func(const string& s)
{
string::iterator it = s.begin();
while(it != s.end())
{
cout << *it <<endl;
}
cout << endl;
}
这里我们传入的是const修饰的s调用的begin函数,就会返回一个const_iterator 类型的数据。
返回来的 无const 修饰的,可读可写
返回来的 有cosnt 修饰的,只可读,不可写。
如果我们想要接收 const 修饰的类型,我们就需要用 const 修饰的类型接收。
string::const_iterator it = s.begin();
如果是反向迭代器,如果不使用 cosnt ,虽然不会显示出来,但是也运行不了。
因此反向迭代器接收 const修饰对象调用的rbegin 函数 ,也需要用const修饰的对象接收。
string::const_reserve_iterator it2 = s.rbegin();
但是像上面这种定义类型,写的太麻烦了,如果我们心里清楚返回的类型,我们直接使用 auto 接收返回值即可。
auto it = s.begin();
void func()
{
string::const_reserve_iterator it1 = s.rbegin();
auto it2 = s.rbegin();
while(it1 != s.rend())
{
cout << *it1 << endl;
it1++;
}
cout << endl;
while(it2 != s.rend())
{
cout << *it2 << endl;
it2++;
}
cout << endl;
}
在c++11中,加入了const修饰的 cbegin,cend 函数,就是
cbegin 函数需要传入const 修饰的对象,然后返回 cosnt修饰的对象。
正向迭代器,反向迭代器,const修饰,非const修饰。
3.2. 构造拷贝
上面简单介绍了一下string创建对象,但是这么多重载,创建对象不止能那样创建,还有其他方法。
我们可以采用下面任何一个重载函数进行构造
void string4()
{
striing s1("hello world");
cout << s1 << endl;
string s2(s1,6,5);
cout << s2 << endl;
string s3(s1,6,3);
cout << s3 << endl;
}
这里,npos是缺省参数,如果我们在这里不传入npos的值,就会默认使用
这里的npos是静态成员变量,但是这里的长度显示为 -1。
size_t 是 无符号整形,和 unsigned 一样,这里的 -1 就是二进制全是1,也就是 2^32 的大小。
这里npos,“until the end of the string”,也就是默认是字符串的长度。
string s4(s1,6);
cout << s4 << endl;
除了使用默认的 npos ,也可以使用 size()-pos 直接算出来 pos 与最后一个字符的位置。
因为有这个缺省参数,这里可以不传入,比如下面的初始化。
void string4()
{
string s1("hello world");
string s2(s1,4);
cout << s2 << endl;
}
这里就默认从第4个字符的位置,一直到字符串结尾。
也可以指定多少个字符去初始化
void string4()
{
string s1("hello world");
string s2(10,'c');
cout << s2 << endl;
}
也可以使用迭代器取区间初始化。
void string4()
{
string s1("hello world");
string s2(10,'c');
cout << s2 << endl;
string s3("s2.begin(),s2.end());
cout << s3 << endl;
}
赋值也有多个重载
s2 = s1;
s3 = "xxx";
s4 = 'x';
赋值操作可以按照上面的写法使用。
3.3. string 的容量
1. length,size
size 和 length,一个是顺序表内个数,一个数字符串大小,正常情况下,两个是一样的。
cout << s3 <<endl;
cout << s3.size() << endl;
cout << s3.length() << endl;
这里 size 和 length 的输出结果一样。
最开始只是字符串的话,用 length 更好,但是为了适配所有的容器,就开始使用 size。
2. clear
clear 是只是对数据的清除,但是不会释放空间。
void string4()
{
string s3(10,'c');
cout << s3 << endl;
cout << s3.size() << endl;
cout << s3.capacity() << endl;
s3.clear();
cout << s3.size() << endl;
cout << s3.capacity() << endl;
}
capacity string开辟的空间大小,size 字符串长度。
调用clear之后,字符串长度为0,容量大小不变。
3. max_size
能打印出来最大能开辟多大的空间。
也就是内存中最多能开多少空间,但是这个空间是写死的,不管开没开空间都会显示这个数。
但是如果已经开了一些空间,max_size 还是显示那个数,所以实际用处不是很大。
cout << s2.max_size() << endl;
注:不同平台下能开辟的空间大小都有不相同。
4. capacity
void string5()
{
string s;
size_t old = s.capacity();
cout << old << endl;
for(int i = 0 ; i < 100 ; i++)
{
s.push_back( i );
if( s.capacity() != old )
{
cout << s.capacity() << endl;
old =s.capacity();
}
}
}
注:这里的capacity,指的是有效容量,‘\0’的位置是不会算入的。
这里就按照capacity+1作为容量大小。
16,32,48,71,106
最开始扩了2倍,后面都是1.5倍的扩。
5. reverse
reserve 保留
reverse 颠倒(迭代器那边使用)
请求改变容量
如果最开始就知道了需要多少空间
s.reserve(100);
直接申请100的空间,但是因为在 vs 下内存会自动对齐,所以这里会申请112的空间。
g++ 下要多少给多少
vs 下会多给
reserve 不同的平台要求不同,一般情况下容量是不能缩小的。
s.reserve(100);
s.reserve(10);
但是这里的空间都是被使用的,也就是内部有数据,如果没数据,是可以缩小的。
但是缩小的代价比较大,会异地开辟空间,然后把空间的内容复制过去。
6. resize
和reserve类似,但是是对size,也就是长度进行请求。
rsize 的情况相对来说会复杂点。
- resize 的大小大于原空间
void string6()
{
string s("hello world");
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.resize(13);
cout << s1.resize() << endl;
cout << s1.capacity() << endl;
}
函数重载,有两个函数。
一个不传入字符,多出来的空间会用赋值 ‘\0’
如果传入字符,新的空间就会使用传入的字符来代替。
s1.resize(13,'x);
cout << s1.size() << endl;
cout << s1.capacity() << endl;
cout << s1 << endl;
如果传入的数量大于capacity,就会自动扩容
s1.resize(20,'x');
- resize小于原来的size
这种情况下,起到的就是删除作用。
s1.resize(5);
cout << s1.size() << endl;
cout << s1.capacity() << endl;
cout << s1 << endl;
这里原本size大小是11,但是resize的空间只有5,后面的空间就被删了。
如果原本内部是没有空间和数据的,resize可以起到初始化的作用。
string s2();
s2.resize(10,'L');
cout << s2.size() << endl;
cout << s2.capacity() << endl;
cout << s2 << endl;
7. at
at这个函数和 [] 类似,但是如果pos越界了,就会抛异常。
而[] 会进行断言,直接结束程序。
s2[0];
s2.at(0);
两种访问的写法都可以。
8. append
在字符后面插入字符串,或者字符
void string7()
{
string s;
s.append("hello");
s.push_back('x');
cout << s << endl;
}
append 支持传入字符串和单个字符,也支持传入string对象。
但是有 += 的操作,+= 可以直接实现在对象后面插入字符串/字符,一般该函数不常用。
== 非成员函数 ==
9. operator
operator+ , >> , << , swap ,都属于非成员函数。
operator+ 能少用就少用,使用 + 会进行拷贝构造,效率比较低。
即使是有优化,但是还是有两次拷贝,不是很建议多用。
虽然 operator+ 完全可以写成成员函数,但是库里面确实是非成员函数。
10.swap
如果我们有两个字符串,需要进行交换
void string5()
{
string s1("hello");
string s2("world");
cout << s1 << endl;
cout << s2 << endl;
swap(s1,s2);
cout << s1 << endl;
cout << s2 << endl;
}
库里面的 swap 支持这个自定义类型的互换,但是这样的互换会很麻烦。
这是swap的模版
会进行三次深拷贝,效率很慢。
库里面也提供了swap函数
这个函数不会直接对两个对象的内容进行修改,是直接对对象的指针进行修改。
void string5()
{
string s1("hello world");
string s2("我的世界");
cout << s1 << endl;
cout << s2 << endl;
printf("s1:%p\n", s1.c_str());
printf("s2:%p\n", s2.c_str());
//swap(s1, s2);
s1.swap(s2);
cout << s1 << endl;
cout << s2 << endl;
printf("s1:%p\n", s1.c_str());
printf("s2:%p\n", s2.c_str());
}
string自带的swap会进行首地址的交换。
两个指针调换,就不需要拷贝之类的环节了。
上面的swap是成员函数,而在非成员函数中也有个swap函数
这个swap函数限制了,传入的两个参数。
因为非string的swap函数,实际上是模版,每次使用需要实例化,但是string中的swap函数,是直接存在的,所以直接使用 swap就是使用string的非成员函数的swap。
11. assign
复制,但是平时不常用
void string8()
{
string s1("xxxxxxxx");
string s2 = "hello world";
s1.assign(s2);
cout << s1 << endl;
}
这里会直接把s2 的内容复制到s1里面。
assign也可以传入pos位置和后面需要复制的大小。
void string8()
{
string s1("xxxxxxxx");
string s2 = "hello world";
s1.assign(s2,3,5);
cout << s1 << endl;
}
12. insert
插入
如果想要头插一个字符,就可以像下面的写法
void string9()
{
string s("hello world");
s.insert(0,1,'x');
s.begin(s.begin(),'c');
cout << s << endl;
}
第一种写法,是在pos位置,插入一个字符’x’。
第二种写法,是在begin()位置插入一个字符
但是对于字符串来说少用,数据向前覆盖,效率太慢。
13. erase
删除某个位置
void string8()
{
string s("hello world");
s.erase(2,3);
s.erase(s.begin());
cout << s << endl;
第一种写法,第二个位置往后删除3个空间
第二种写法,删除begin()位置
14. replace
replace 代替
void string11()
{
string s("hello world");
s.replace(5,1,"666");
cout << s << endl;
}
用 “666” 替换第5个位置的数据。
replace,insert,erase 都尽量少使用,因为涉及到数据移动的问题,效率都不高。
接口设计比较多,需要时可以查文档。
- pop_back//尾删
- shrink_to_fit//缩容
- c_str兼容c语言的
15. find系列
- find
如果想要查找一共文件名的后缀:
test.c
使用find即可
void string11()
{
string s("test.c"):
size_t i = s.find();
string s1 = s.substr(i);
cout << s1 << endl;
}
substr 可以获取pos位置到len为止的字符串,len为缺省参数,默认为npos,所以默认为可以获取从pos 到字符串结束的位置。
此时,我们通过 find 找到了 ‘.’ 的下标 ,然后将 i 位置到字符串结尾作为新的字符串给s1。
但是如果有多个 ‘.’ ,此时就不能直接使用find查找。
find 会从字符串开始位置向后查找,找到就返回,多个相同的字符,我们可以从后向前查找。
test.c.tar.zip
- rfind
我们可以使用 rfind 从 后向前查找
void string1()
{
string s("test.c.tar.zip");
size_t i = s.rfind('.');
string s1 = s.substr(i);
cout << s1 << endl;
}
注意substr传入一个参数是从当前位置到结尾,两个参数是区间的范围。
这里返回的 size_t i 和 j 一定要注意
如果没有找到下面的substr就没有意义,因此需要我们判断一下返回值
void string1()
{
string s("test.c.tar.zip");
size_t i =s.rfind('.');
if( i !=string::npos )
{
string s1 = s.substr(i);
cout << s1 << endl;
}
}
- find_first_of
find_first_of 查找这个子串中的任意一个字符
void string2()
{
string str("fiklgdilshgaiolgfgjdksafgaskudfgakusdfagkusf");
size_t found = str.find_first_of("aeiou");
while(found != string::npos)
{
str[found] = '*';
found = str.find_first_of("aeiou",found+1);
}
cout << str << endl;
}
find_first_of 查找前面字符串是否有传入字符串中的其中一个,找到了返回下标。
4. find_last_of
和上面 find_first_of 一样的,但是是从后往前找。
5. find_first_not_of
查找不是当前字符串内的
find系列,一般用的比较少
16. getline
cin 和 cout 以及 sacnf 和 printf
虽然可以完成输入输出,但是如果想要输入多段字符串“hello world”,中间的空格会被默认停下。
int main()
{
string s1 , s2;
cin >> s1 >> s2;
cout << s1 << endl;
cout << s2 << endl;
}
这里输入一行的 hello world ,会默认读取为两段。
此时就可以使用getline函数
获取一行,遇到空格不结束,遇到换行才结束
int main()
{
string s1;
getline(cin,s1);
cout << s1 << endl;
return 0;
}
使用getline就能实现只有换行才算读取结束。
如果不想以换行或者空格结束,想以某个特定的符号结束,也可以。
int main()
{
string s1;
string s2;
getline(cin,s1,'x');
getline(cin,s2,'x');
cout << s1 << endl;
cout << s2 << endl;
return 0;
}