目录
2.4.2 begin和end(正向迭代器和const正向迭代器)
2.4.3 rbegin和rend(逆向迭代器和const逆向迭代器)
一、STL
1.1 什么是STL
STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。
1.2 STL的版本
- 原始版本
Alexander Stepanov、Meng Lee 在惠普实验室完成的原始版本,本着开源精神,他们声明允许任何人任意运用、拷贝、修改、传播、商业使用这些代码,无需付费。唯一的条件就是也需要向原始版本一样做开源使用。 HP 版本--所有STL实现版本的始祖。
P. J. 版本
由P. J. Plauger开发,继承自HP版本,被Windows Visual C++采用,不能公开或修改,缺陷:可读性比较低,符号命名比较怪异。
RW版本
由Rouge Wage公司开发,继承自HP版本,被C+ + Builder 采用,不能公开或修改,可读性一般。
SGI版本
由Silicon Graphics Computer Systems,Inc公司开发,继承自HP版 本。被GCC(Linux)采用,可移植性好,可公开、修改甚至贩卖,从命名风格和编程 风格上看,阅读性非常高。我们后面学习STL要阅读部分源代码,主要参考的就是这个版本。
1.3 STL的六大组件
二、string类
2.1 string类的基本介绍
C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
总结:
- string是表示字符串的字符串类
- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
- string在底层实际是:basic_string模板类的别名, typedef basic_string<char, char_traits, allocator>string; string类是basic_string模板类的一个实例
- 不能操作多字节或者变长字符的序列。
2.2 string类的默认成员函数
(constructor)构造函数 |
Construct string object (public member function ) |
(destructor)析构函数 |
String destructor (public member function ) |
(operator=) 赋值运算符重载 |
String assignment (public member function ) |
2.2.1 构造函数
string类常见的构造函数
(constructor)函数名称 | 功能说明 |
---|---|
string() (重点) | 构造空的string类对象,即空字符串 |
string(const char* s) (重点) | 用C-string(c风格的字符串)来构造string类对象 |
string(size_t n, char c) | string类对象中包含n个字符c(用n个字符c来构造string类对象) |
string(const string&s) (重点) | 拷贝构造函数 |
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s1;
string s2("aa aaa bbbbb");
string s3(s2);
string s4(10, 'c');
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
return 0;
}
2.2.2 析构函数
销毁string对象
2.2.3 赋值运算符重载
分配一个新的值的string类对象来代替当前的容器
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s1("aa aaa bbbbb");
string s2(10, 'c');
cout << s1 << endl;
cout << s2 << endl;
return 0;
}
2.3 string类对象的容量操作
函数名称 | 功能说明 |
---|---|
size(重点)(常用) |
返回字符串有效字符长度 |
返回字符串有效字符长度 | |
返回空间总大小 | |
empty(重点) |
检测字符串是否为空串,是返回true,否则返回false |
clear(重点) |
清空有效字符 |
reserve(重点) |
为字符串预留空间 |
resize(重点) |
将有效字符的个数该成n个,多出的空间用字符c填充 |
2.3.1 size和length
这两个函数都是返回当前对象的有效字符长度。
由于历史的种种原因,还是推荐使用size()函数。
int main()
{
string s2("aa aaa bbbbb");
cout << s2.size() << endl;
cout << s2.length() << endl;
return 0;
}
2.3.2 capacity
格式:size_t capacity() const
- 返回值:返回当前为字符串分配的存储空间的大小
int main()
{
string s1("hello");
cout << s1.size() << endl;
cout << s1.capacity() << endl;
return 0;
}
注意一个问题:当构建一个空string类对象时,它的容量是0还是其他数?
- 通过上图可以看出,在VS2022下默认会给生成的字符串对象开辟15字节的空间(由于编译器的不同,默认开辟的空间大小也会不同)。Linux的g++环境下默认不给对象开辟空间。
- 当字符串添加新字符后,字符串当前长度大于原本为其开辟的空间。对象就会自动扩容(即重新分配空间)。
- 在VS2022下,string类对象自动扩容是按照原大小的1.5倍进行扩容。在不同的编译器下,扩容的倍数也会不同。Linux下的g++环境按照原大小的两倍进行扩容。
- capacity函数返回的是当前对象的空间大小,但由于在c/c++中'\0'并不是有效字符,所以capacity函数并没有将'\0'算进去。所以对象实际的空间大小应该在原有的基础上多加1字节,因为字符串的末尾还有个'\0'.
2.3.3 reserve和resize
2.3.3.1 reserve
- 格式:void reserve (size_t n = 0)
- 返回值:None
请求根据计划改变字符串容量,长度最多为n个字符。(只影响容量,不影响数据)
对于reserve函数,可以有两种改变容量的情况。一个是扩容,另一个则是缩容。
1、扩容
int main()
{
string s1;
// reserve()的使用最好是已经确定需要多少空间,根据需求开好空间即可
s1.reserve(100);
size_t old = s1.capacity();
cout << old << endl;
for (size_t i = 0; i < 100; i++)
{
s1.push_back('x');
if (old != s1.capacity())
{
old = s1.capacity();
cout << old << endl;
}
}
return 0;
}
2、 缩容
在VS2022下是不会发生缩容的,但是在Linux的g++环境下就会发生缩容,最多只能缩到对象的size()。
int main()
{
string s1("hello");
s1.reserve(1);
cout << s1.size() << endl;
cout << s1.capacity() << endl;
cout << s1 << endl;
return 0;
}
2.3.3.2 resize
格式:void resize (size_t n) 或 void resize (size_t n, char c)
返回值: None
将字符串的大小改为n个字符c的长度。(既影响容量,也影响数据)
注意:有关容量大小n的问题
1、如果n小于当前字符串的长度,则当前值将缩短为其前n个字符并删除第n个字符以外的字符。
int main()
{
string s1("hello");
cout << s1.size() << endl;
cout << s1.capacity() << endl;
cout << s1 << endl;
// n < size --> 将容量缩短为前n个字符,并删除第n个字符以外的字符
s1.resize(3);
cout << s1.size() << endl;
cout << s1.capacity() << endl;
cout << s1 << endl;
return 0;
}
2、如果n大于当前字符串存储空间的大小,则通过在末尾插入所需数量的字符来扩展当前内容,以到达n的大小。如果指定了补充的字符为c,则新元素将初始化为c的副本。否则,它们的值将初始化为空字符('\0')。
int main()
{
string s1("hello");
cout << s1.size() << endl;
cout << s1.capacity() << endl;
cout << s1 << endl;
// n > capacity --> 不指定字符,默认插入空字符'\0'并扩容到n个字节
s1.resize(100);
cout << s1.size() << endl;
cout << s1.capacity() << endl;
cout << s1 << endl;
return 0;
}
3、 如果n小于当前字符串的存储空间的大小且大于当前字符串有效字符的个数,会将当前字符串有效字符的个数的位置的后面的空间全部设置为字符 c。
int main()
{
string s1("hello");
cout << s1.size() << endl;
cout << s1.capacity() << endl;
cout << s1 << endl;
// size < n < capacity --> 不指定字符,默认插入n个空字符'\0'。但不扩容
s1.resize(8);
cout << s1.size() << endl;
cout << s1.capacity() << endl;
cout << s1 << endl;
return 0;
}
2.3.4 empty
- 格式:bool empty() const
- 返回值:如果字符串长度为0就返回true,否则返回true
int main()
{
string s1;
cout << s1.empty() << endl;
string s2 = "aaaaaa";
cout << s2.empty() << endl;
return 0;
}
2.3.5 clear
格式: void clear()
返回值:None
清空字符串内的有效字符,但并不会影响底层空间的大小
int main()
{
string s2 = "aaaaaa";
s2.clear();
cout << s2.empty() << endl;
return 0;
}
总结:
- 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不会改变容量大小。
2.4 string类的迭代器(iterator)
2.4.1 介绍
在C++中,迭代器(lterator) 是一种设计模式,它提供了一种统一的方式来遍历容器(如vector、list、map 等)中的元素,而无需暴露容器的内部实现细节。迭代器的行为类似于指针,允许你逐个访问容器中的元素。
迭代器名称 | 迭代器的作用 |
---|---|
begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 | |
rbegin获取最后一个字符的迭代器 + rend获取一个字符的前一个位置的迭代器 |
string类提供了不同类型的迭代器:
- 正向迭代器(iterator):遍历可修改字符串的迭代器
- 常属性正向迭代器(const_iterator):只能读取字符串的元素不能修改的迭代器
- 反向迭代器(reverse_iterator):逆向遍历字符串
- 常属性反向迭代器(const_reverse_iterator):只能逆向读取字符串中的元素不能修改的迭代器
2.4.2 begin和end(正向迭代器和const正向迭代器)
begin:获取第一个字符的迭代器
const_begin:获取第一个字符但不能修改的迭代器
end获取最后一个字符下一个位置的迭代器(类似于哨兵)
const_end:获取最后一个字符下一个位置但不能修改的迭代器
int main()
{
string s1 = "abcdefghijk";
string::iterator sit = s1.begin();
string::iterator eit = s1.end();
// 利用迭代器遍历字符串
while (sit != eit)
{
cout << *sit << " ";
++sit;
}
return 0;
}
2.4.3 rbegin和rend(逆向迭代器和const逆向迭代器)
用法类似于正向迭代器begin和end以及const正向迭代器const_reverse_iterator和const_reverse_iterator,但方向是相反的。
2.5 string类对象的访问及遍历操作
函数名称 | 功能说明 |
---|---|
operator[](重点) |
返回pos位置的字符,const string类或string类对象都可以调用 |
begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 | |
rbegin获取最后一个字符下一个位置的迭代器 + rend获取一个字符的迭代器 | |
范围for | C++11支持更简洁的范围for的新遍历方式 |
2.5.1 operator[]
得到字符串中的字符,但要注意的是返回值的类型是char&。说明使用operator[]不仅可以访问字符串中的字符,还可以修改字符。
int main()
{
string s1 = "abcdefghijk";
cout << s1[0] << endl; // 访问字符串s1的第一个字符
s1[0] = '0'; // 修改字符串s1的第一个字符
cout << s1[0] << endl; // 访问字符串s1的第一个字符
return 0;
}
2.5.2 at
at和operator[]的功能是一样的,唯一的区别就是处理越界的方式不同。
int main()
{
try
{
string s1("hello world");
cout << s1[11] << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
为什么访问数组的下标已经越界,却不报错?
原因在于还有一个字符'\0',在C++中还是允许访问的。
1、operator[]处理越界的方法
int main()
{
try
{
string s1("hello world");
cout << s1[20] << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
2、at()处理越界的方法
int main()
{
try
{
string s1("hello world");
cout << s1.at(20) << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
2.5.3 范围for循环
int main()
{
string s1 = "abcdefghijk";
// 遍历字符串 -- 搭配auto关键字一起使用
for (auto& c : s1)
{
cout << c << " ";
}
return 0;
}
2.5.4 使用迭代器iterator(推荐使用)
int main()
{
string s1 = "abcdefghijk";
string::iterator sit = s1.begin();
string::iterator eit = s1.end();
// 利用迭代器遍历字符串
while (sit != eit)
{
cout << *sit << " ";
++sit;
}
return 0;
}
2.6 string类对象的修改操作
函数名称 | 功能说明 |
---|---|
在字符串后尾插字符c | |
append | 在字符串后追加一个字符串 |
operator+= (重点) |
在字符串后追加字符串str |
c_str (重点) |
返回C格式字符串 |
find + npos(重点) | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置 |
从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置 | |
在str中从pos位置开始,截取n个字符,然后将其返回 |
2.6.1 Push_back
格式:void push_back (char c);
用法:在字符串末尾尾插字符c。
int main()
{
string s1 = "abcdefghijk";
cout << s1 << endl;
s1.push_back('2');
cout << s1 << endl;
return 0;
}
2.6.2 append
int main()
{
string s1 = "hello";
string s2 = "world";
s1.append(" ");
s1.append(s2);
cout << s1 << endl;
return 0;
}
2.6.3 operator+=(最常用的尾插方法)
将str的内容追加到当前字符串的末尾
int main()
{
string s1 = "hello";
string s2 = "world";
s1 += " ";
s1 += s2;
cout << s1 << endl;
return 0;
}
2.6.4 c_str
返回指向一个不可修改数组的指针,该数组包含以null结尾的字符序列(即C字符串),表示字符串对象的当前值。
int main()
{
string filename("Test.cpp");
FILE* fout = fopen(filename.c_str(), "r");
char ch = fgetc(fout);
while (ch != EOF)
{
cout << ch;
ch = fgetc(fout);
}
return 0;
}
2.6.5 npos
在谈及下一个函数前,先得了解一个数值,他的名叫npos
npos是一个静态且不可被修改的无符号整数,它表示无符号整型的最大值(-1)。
2.6.6 find
寻找在字符串中的内容
注意:如果找到目标内容就返回目标的下标,如果没找到目标则返回npos。
int main()
{
string s1 = "hello";
cout << s1.find("e") << endl;
cout << s1.find("d");
return 0;
}
2.6.7 rfind
返回最后一个匹配字符/字符串的位置,整体与find相似。
2.6.8 substr
返回一个新构造的string对象,它的初始值为当前字符串的子字符串(在str中从pos位位置开始,截取n个字符)。
int main()
{
string s1 = "hello";
cout << s1.substr(0 , 4) << endl;
return 0;
}
2.6.9 erase
删除字符串的一部分,减少字符串的长度。
int main()
{
string s1("hello world");
string s2("xxx");
s1.erase(3, 2);
cout << s1 << endl;
s1.erase(3);
cout << s1 << endl;
s2.erase();
cout << s2 << endl;
return 0;
}
2.6.10 insert(效率很低,能不用就不用)
insert函数由于每次插入数据时都得挪动字符串中的数据,导致内存占用时间过长。所以非必要就不要使用insert函数。
一些常用的重载
int main()
{
string s1("hello world");
string s2("xxx");
s1.insert(s1.begin(), 1, '3');
cout << s1 << endl;
s1.insert(0, 1, 'x');
cout << s1 << endl;
return 0;
}
int main()
{
string s1("hello world");
string s2("xxx");
s1.insert(0, s2);
cout << s1 << endl;
s1.insert(5, s2, 2, 4);
cout << s1 << endl;
s1.insert(5, "p");
cout << s1 << endl;
return 0;
}
注意:
- 在string尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c'三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
- 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。
2.7 有关string类的迭代器的补充
2.7.1 string类迭代器的底层实现
typedef char* iterator;
typedef const char* const_iterator;
// 非const版本的迭代器
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
2.7.2 范围for循环与迭代器iterator的关系
int main()
{
string s1("hello world");
string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
it++;
}
cout << endl;
// 范围for循环实际上是傻瓜式替换为迭代器
for (auto ch : s1)
{
cout << ch << " ";
}
}
从结果上看,范围for循环的结果跟用迭代器iterator的结果是一样的。那是不是它们的底层实现逻辑也是一样的?
实际上是这样的,范围for循环实际上就是傻瓜式替换为迭代器。
只要将任意一个迭代器改名了,那范围for循环就使用不了了。
typedef char* Iterator;
typedef const char* const_iterator;
// 非const版本的迭代器
Iterator begin()
{
return _str;
}
Iterator end()
{
return _str + _size;
}