C++标准库
# IO类
# IO类型介绍
IO类可以用输入输出数据流,其对象不仅可以是控制台窗口,也可以是其他可IO对象,例如文件。
C++提供多种IO类型供我们读写数据,其中iostream提供控制台IO,fstream提供文件IO,stringstream提供内存IO,如下表所示:
头文件 | 类型 | 作用 |
---|---|---|
iostream | istream | 从流中读取数据 |
iostream | ostream | 向流中写入数据 |
iostream | iostream | 读写流,可读可写 |
fstream | ifstream | 从文件中读取数据 |
fstream | ofstream | 向文件中写入数据 |
fstream | ffstream | 读写文件,可读可写 |
sstream | istringstream | 从string中读取数据 |
sstream | ostringstream | 向string中写入数据 |
sstream | stringstream | 读写string,可读可写 |
另外为了支持宽字符的语言,标准库还定义类一系列w开头的类型来操作wchar_t类型的数据。其类型名以w开头,例如istream要使用wchar_t版本就是wistream类型,其他IO类型也是一样,以此类推。
# IO类型间的关系
C++的IO类型之间存在继承关系:
- ifstream和istringstream继承自istream。
- ofstream和ostringstream继承自ostream。
- ffstream和stringstream继承自iostream。
因此我们在使用时,都是可以通过"<<"和">>"重定向符对这些IO类型进行读写的。
# 条件状态
IO类定义了一些函数和标志位,帮助我们检查和操纵IO流的状态。
# 标志位
strm::iostate
- 它是一种类型,用于指出了流的当前状态(strm为IO类型),以下是它可能的值:
strm::badbit
- 它是流的一种状态,指出流已崩溃。strm::failbit
- 它是流的一种状态,指出IO操作失败。strm::eofbit
- 它是流的一种状态,指出流到达文件结尾。strm::goodbit
- 它是流的一种状态,指出流处于有效状态,未处于错误状态。
badbit表示系统级错误,如不可恢复的读写错误,一旦badbit被置位,流就无法再使用了。
failbit通常表示数据转换、输入格式等错误,是可恢复的非系统级错误,修正后流可以继续使用。
如果到达文件末尾,则eofbit和failbit都会被置位,然后goodbit的值会置0,表示流未发生错误。
如果badbit、failbit、eofbit中任何一个被置位,流状态的条件检测都会为失败。
# 函数
流本身可以作为状态条件,但只能检查流是否有效。所以C++提供了一系列函数用于检查和操纵IO流的状态标志位。
s.eof()
- 如果检测到IO对象的流到达文件结尾,即eofbit置位,则返回true。
s.fail()
- 如果检测到IO对象的流处于崩溃或IO操作失败的状态,即failbit置位,则返回true。
s.bad()
- 如果检测到IO对象的流处于崩溃状态,即badbit置位,则返回true。
s.good()
- 如果检测到IO对象的流处于有效状态,即goodbit置位,返回true。
s.clear()
- 用于将IO对象的所有流状态标志位复位,将流的状态设为有效,无返回值。
s.clear(flags)
- 用于将IO对象的指定流状态复位,无返回值,flags为strm::iostate类型的值。
s.setstate(flags)
- 用于将IO对象的指定流状态置位,无返回值,flags为strm::iostate类型的值。
s.rdstate()
- 用于返回当前IO对象的流状态,返回strm::iostate类型的值。
# 例如
char word;
// 流本身可以作为查询条件,同等于(cin >> word).good()
while (cin >> word){
cout << word << endl;
}
// 记住流的当前状态
auto old_state = cin.rdstate();
// 使流有效
cin.clear();
// 使用流
process_input(cin);
// 还原流的状态
cin.setstate(old_state);
2
3
4
5
6
7
8
9
10
11
12
13
14
# 输出缓冲
每个输出流都管理一个缓冲区,用来暂存程序读写的数据。缓冲机制使操作系统可以将程序的多个输出操作组合成单一的系统级写操作,在缓冲刷新时才会真正进行输出。
# 缓冲刷新的情况
- 程序正常结束时,main函数的return操作会执行缓冲刷新。
- 缓冲区满时,会执行缓冲刷新,以空出空间存储新的数据。
- 使用控制缓冲区的操作符来显式刷新缓冲区。
- 默认情况下cerr是设置了unitbuf的,因此写到cerr的内容会立即刷新。
- 一个输出流可能被关联另一个流,当读写被关联的流时,关联的流的缓冲区会被刷新。默认情况下cin和cerr都关联到cout,因此读写cin或cerr都会导致cout的缓冲区执行刷新。
# 缓冲区控制操作符
endl
:输出一个换行,并立即刷新缓冲区。ends
:输出一个空字符,不会立即刷新缓冲区。flush
:立即刷新缓冲区。unitbuf
:设置输出流缓冲方式为每次输出后立即刷新缓冲区。nounitbuf
:设置输出流缓冲方式为不立即刷新,回到正常模式。
例如:
// 输出字符串,然后添加一个换行再刷新缓冲区
cout << "hhh111" << endl;
// 输出字符串,然后添加一个空字符
cout << "hhh222" << ends;
// 将cout流状态设置为输出后立即刷新缓冲区
cout << unitbuf;
cout << "hhh333"; // unitbuf状态中,会立即刷新缓冲区
cout << "hhh444"; // unitbuf状态中,会立即刷新缓冲区
// 设置输出流为不立即刷新,回到正常模式,之后的输出不再立即刷新
cout << nounitbuf;
2
3
4
5
6
7
8
9
10
# 关联输入输出流
当一个输入流被关联到另一个输出流时,任何读取输入流的操作都会先刷新关联的输出流。
IO类中有一个tie函数,它即能用于查询当前流对象所关联的流对象,也能用于将当前流对象与指定流对象进行关联。
不指定参数是查询当前流对象所关联的流对象的指针,无关联则返回空指针。如果参数指定流对象的指针,则是将当前流对象与指针所指向的流对象进行关联。
我们既可以将一个istream对象关联到另一个ostream对象,也可以将一个ostream对象关联到另一个ostream对象。每个流最多关联一个其他流,但一个流可以被多个流可以同时关联。
例如:
// 查询当前关联流,cin默认关联cout
cout << cin.tie() << endl;
// 可以看到两者输出一致
cout << &cout << endl;
// 将cin的关联流对象修改为cerr
cin.tie(&cerr);
// 使cin不关联任何流对象
cin.tie(nullptr);
2
3
4
5
6
7
8
# 文件输入输出
# 文件IO操作
想要在C++中操作文件可以使用fstream头文件中定义的三个IO类型,即ifstream、ofstream、fstream。
文件的输入输出操作也是使用"<<"和">>"操作符,除此之外我们还需要使用文件IO类型特有的成员函数来管理流关联的文件。
操作 | 作用 |
---|---|
fstream fstrm; | 创建一个未绑定的文件流。fstream是头文件fstream中定义的类型。 |
fstream fstrm(s, mode); | 创建一个文件流。 并打开名为s的文件,s可以是string类型或字符串指针类型。 按指定mode打开文件。不指定则默认mode取决于使用的fstream的类型。 |
fstrm.open(s, mode); | 打开名为s的文件,并将文件与fstrm绑定。 按指定mode打开文件。不指定则默认mode取决于使用的fstream的类型。 |
fstrm.close(); | 关闭与fstrm绑定的文件,返回void。 |
fstrm.is_open(); | 如果fstrm关联的文件成功打开且尚未关闭,则返回true。 |
std::getline(rfstrm, line); | 我们可以使用getline函数来读取读模式文件流中的数据,将其一行一行赋值给指定的字符串变量。 |
# 文件模式
文件模式即打开文件时的mode参数,用来指出如何使用文件,它们定义在命名空间std::ios下。
istream默认为in模式,ostream默认为out和trunc模式。
模式名 | 作用 | 使用条件 |
---|---|---|
in | 以读模式打开文件,如果文件不存在则打开操作失败。 | ifstream、fstream |
out | 以写模式打开文件,如果文件不存在则创建一个新文件,文件存在则清空文件内容。 | ofstream、fstream |
app | 以追加模式打开文件,用于在文件末尾添加内容。如果文件不存在则创建一个新文件。 | 不能和trunc一同使用 |
ate | 以可读可写模式打开文件,并将文件指针定位到文件末尾。如果文件不存在则打开操作失败。 | 无 |
trunc | 以截断模式打开文件,会清空文件内容。如果文件不存在则创建一个新文件。 | 使用out时才能使用 |
binary | 以二进制模式打开文件,用于处理二进制文件。 | 无 |
使用格式
// 指定单个模式
fstream fstrm(s, std::ios::mode_name);
// 指定多个模式,用|隔开
fstream fstrm(s, std::ios::mode_name | std::ios::mode_name |...);
2
3
4
# 使用例子
int main() {
// 创建一个ifstream对象,打开./test.txt文件
ifstream inf("./test.txt", ios::in);
// 检查文件流是否打开成功,打开失败failbit会置位,此处等于if(inf.good())
if (inf) {
cout << "in file open successed." << endl;
}
else {
cout << "in file not open." << endl;
return 1;
}
// 创建一个ofstream对象。
ofstream outf;
// outf打开并关联./res.txt文件。
outf.open("./res.txt");
// 检查文件流是否打开成功
if (outf) {
cout << "out file open successed." << endl;
}
else {
cout << "out file not open." << endl;
return 1;
}
// 将inf的内容输出到outf中
string line;
while (getline(inf, line)) {
// 使用重定向符号控制输入或输出流
outf << line << endl;
}
// 关闭文件
inf.close();
outf.close();
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# string流
# 内存IO操作
在C++中还可以进行string类型的内存IO,通过sstream头文件中定义的三个IO类型来实现,即istringstream、ostringstream、stringstream。
string流的输入输出操作也是使用"<<"和">>"操作符,除此之外我们还需要使用string流类型特有的成员函数来管理string流。
操作 | 作用 |
---|---|
sstream strm; | 创建一个未绑定的string流。sstream是头文件sstream中定义的类型。 |
sstream strm(s); | 创建一个string流,并保存传入的string对象的拷贝到流中。 |
strm.str(s) | 不指定参数则返回strm所保存的string对象的拷贝。 指定参数则会将传入的string对象的拷贝保存到流中,返回void。 |
# 使用例子
#include <iostream>
#include <string>
#include <vector>
#include <fstream>
#include <sstream>
using std::vector;
using std::string;
using std::cout;
using std::cerr;
using std::endl;
using std::ifstream;
using std::istringstream;
// 例如我们要读入一个以下内容格式的文件,并要对其进行处理
/*
John 17672011881 16723131022
Aunn 13432110212
Vector 15430299177 18092877800
*/
// 首先定义类型,以存储数据
struct PersonInfo {
string name;
vector<string> phones;
};
int main() {
// 定义存储类型的容器
vector<PersonInfo> people;
// 定义临时存储读取内容的变量
string line, word;
// 打开文件流
ifstream rf("./test.txt");
// 判断是否打开成功
if (!rf.is_open()) {
cerr << "file open error.";
return 1;
}
// 按行循环读取文件流进行处理
while (getline(rf, line)) {
// 实例化一个info对象
PersonInfo info;
// 将行字符串作为参数实例化一个string输入流
istringstream record(line);
// >>会按空白符分隔,所以第一列名字直接输出给名称
record >> info.name;
// 循环读取输入流中的数据,因为只剩下手机号,全部push到phones成员
while (record >> word) {
info.phones.push_back(word);
}
// 最后将info对象push到people容器
people.push_back(info);
}
// 关闭文件
rf.close();
// 检查是否正常读入
cout << people[2].name << endl;
cout << people[2].phones[1] << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# 顺序容器
# 顺序容器介绍
在C++的标准模板库(STL)中,顺序容器是一类用来顺序存储元素数据的容器。所有容器类型都共享着一些公共的接口,但不同容器类型可能会按不同方式在其基础上进行拓展。
# 顺序容器类型
C++中有多种顺序容器类型,每种容器类型都在增删元素效率、访问元素效率上有不同的性能取舍。
# vector
动态数组。支持快速随机访问。在末尾增删元素非常高效,但在中间或头部增删元素则效率较低,适合用于元素数量经常变化,但主要在末尾进行元素增删的情况。
# deque
双端队列。也支持快速随机访问,但通常比vector类型慢。在头部和末尾增删元素非常高效,适合需要在两端进行插入和删除操作的应用场景。
# list
双向链表。只支持双向顺序访问,不支持快速随机访问。在任何位置增删元素都比较高效,适合于需要频繁在列表中间进行插入和删除操作的场景。
# forward_list
单向链表。只支持单向顺序访问。在任何位置进行插入、删除的速度很快。
# array
固定大小的数组。提供快速随机访问,并在内存上表现为连续存储。当数组大小固定且需要快速访问每个元素时使用。
# string
string是与vector相似的容器,专门用于保存字符。支持快速随机访问。在末尾增删元素非常高效。
# 详细介绍
vector和string
- 这两种容器都会将元素保存在连续的内存空间中,因为是连续存储的,所以通过下标计算元素地址非常高效。
- 这两种容器如果想在中间或头部执行元素的增删操作,就需要移动增删位置之后的所有元素,以保持连续存储,因此在中间或头部增删元素效率较低。
- 这两种容器在添加元素还可能需要分配额外的存储空间,此时每个元素都必须移动到新的存储空间。
list和forward_list
list和forward_list都是由节点组成的链表。
list的每个节点包含元素本身和指向前后节点的指针。
forward_list的每个节点包含元素本身和指向下一个节点的指针,因此占用内存更小。
在执行增删操作时仅涉及修改指针,而无需移动元素,因此它在容器任何位置的增删元素操作都很快速。作为代价他们不支持元素的随机访问,在访问一个元素时会遍历整个容器,直到找到想要的元素。
deque
deque是分段连续存储的,他的元素被存储在多个固定大小的数组中,这些数组通过内部指针连接。
这种结构让它在两端增删元素时无需移动其他元素。但如果在中间增删元素则还是需要移动部分元素。不过因为是分段存储,所以这种移动通常少于vector和string。
array
- array和内置数组在内部结构和性能上相似,但array提供了更好的类型安全性、接口一致性以及与STL的集成。
# 容器类型操作
操作 | 作用 |
---|---|
a.swap(b) | 交换a和b的元素,与swap(a, b)等价 |
c.size() | 返回c容器中的元素数量(forward_list不支持) |
c_max_size() | 返回c容器可保存的最大元素数量 |
c.empty() | 如果c容器为空,则返回true,否则返回false |
c.begin()、c.end() | 返回c容器的首元素和尾后元素的迭代器 |
c.cbegin()、c.cend() | 返回c容器的首元素和尾后元素的const迭代器 |
c.rbegin()、c.rend() | 返回c容器的首前元素和尾元素的迭代器(反向遍历) |
c.crbegin()、c.crend() | 返回c容器的首前元素和尾元素的const迭代器(反向遍历) |
c.push_back(val) | 在c容器的尾部追加元素,会拷贝val作为元素值 (forward_list不支持) |
c.emplace_back(initval) | 在c容器的尾部追加元素,会将val作为参数构造元素值 (forward_list不支持) |
c.push_front(t) | 在c容器的首部追加元素,会拷贝t作为元素值 (vector和string不支持) |
c.emplace_front(t) | 在c容器的首追加元素,会将t作为参数构造元素值 (vector和string不支持) |
c.insert(p, t) | 在迭代器p当前指向的元素之前追加元素,会拷贝t作为元素值。返回指向新添加的元素的迭代器。 |
c.emplace(p, t) | 在迭代器p当前指向的元素之前追加元素,会将t作为参数构造元素值。返回指向新添加的元素的迭代器。 |
c.insert(p, t) | 在迭代器p当前指向的元素之前追加元素,会拷贝t作为元素值。返回指向新添加的元素的迭代器。 |
c.insert(p, n, t) | 在迭代器p当前指向的元素之前追加元素,会拷贝n个t作为元素。返回指向新添加的第一个元素的迭代器。 |
c.insert(p, b, e) | 在迭代器p当前指向的元素之前追加元素,会拷贝迭代器b和迭代器e范围内的元素,另外b和e范围内不能是c中的元素。返回指向新添加的第一个元素的迭代器。 |
c.insert(p, il) | 在迭代器p当前指向的元素之前追加元素,会拷贝元素值列表il(花括号包裹的值)中的元素。返回指向新添加的第一个元素的迭代器。 |
c.pop_back() | 删除c容器的尾元素 (forward_list不支持) |
c.pop_front() | 删除c容器的首元素 (vector和string不支持) |
c.erase(p) | 在c容器中删除迭代器p当前所指的元素。返回指向被删除元素的下一个元素的迭代器 |
c.erase(b, e) | 在c容器中删除迭代器p和迭代器b之间的元素。返回指向被删除的最后一个元素元素的下一个元素的迭代器 |
c.clear() | 清空c容器中的所有元素 |
c.resize(s, v) | 改变容器的大小。缩小会删除多出来的元素。扩展会新增少了的元素,指定了v值则新增的元素值为的v,否则为元素类型的默认值。 |
c.capacity() | 返回不重新分配内存空间时,c容器可保存的最大元素数量,他会将那些保留内存空间也计算进来。 只能用于vector和string类型。 |
c.reserve(n) | 给c容器预分配至少能容纳n个元素的内存空间。 如果c容器内存空间不足,即size()返回值和capacity()一致时,再多存储一个元素程序就会自动分配内存空间。默认是翻倍分配内存空间。 只能用于vector和string类型。 |
c.shrink_to_fit() | 请求将容器的预分配内存空间调整至当前元素数量大小。 即将多余内存退回给系统,也就是capacity()调整至size()相同大小。 注意:它只是请求,并不保证退还内存。 只能用于vector、string、deque类型。 |
# string类型额外操作
# 构造操作
操作 | 作用 |
---|---|
string s1(p, n) | 会将字符串指针p的前n个字符拷贝给s1进行初始化。 |
string s1(s2, pos) | 会将字符串s2从下标pos开始到结尾的字符,拷贝给s1进行初始化。 |
string s1(s2, pos, n) | 会将字符串s2从下标pos开始往后n个字符,拷贝给s1进行初始化。 |
# 截取操作
操作 | 作用 |
---|---|
s.substr(pos, n) | 返回对象从下标pos开始往后n个字符的拷贝。 pos的默认值为0,n的默认值为s.size()-pos,即拷贝从pos开始的所有字符。 |
# 修改操作
操作 | 作用 |
---|---|
s1.insert(pose, s2) | 在字符串s1[pose]位置插入字符串s2。 |
s1.insert(pose, n, c) | 在字符串s1[pose]位置插入n个字符c。 |
s1.insert(pos1, s2, pos2, n) | 在字符串s1[pose1]位置插入s2[pos2]开始往后n个字符的拷贝。 |
s1.erase(pose, n) | 删除字符串s1[pose]位置往后n个字符。 |
s1.append(s2) | 在字符串s1的尾部追加字符串s2的拷贝。 |
s1.replace(pos, n, s2) | 删除字符串s1[pose]位置往后n个字符,然后在s1[pose]位置插入字符串s2的拷贝。 |
# 搜索操作
搜索操作会返回无符号整数string::size_type类型的值,搜索失败则返回string::npos成员,npos即const size_type npos = -1,又因为size_type是无符号,所以npos是string最大可能得大小。
操作 | 作用 |
---|---|
s1.find(s2) | 在字符串s1中搜索字符串s2。如果搜索成功则返回s2[0]第一次出现的下标位置。搜索失败则返回npos。 |
s1.rfind(s2) | 与find不同的是返回s2最后一次出现的位置。 |
s1.find_first_of(s2) | 在字符串s1中搜索字符串s2中的任意一个字符。如果搜索成功则返回所匹配字符第一次出现的下标位置。搜索失败则返回npos。 可以用于搜索第一个出现的数字、字母等的位置。 |
s1.find_first_not_of(s2) | 与find_first_of不同的是返回非s2中的字符,最后一次出现的位置。 |
s1.find_last_of(s2) | 与find_first_of相反,在字符串s1中搜索不在字符串s2中的字符。如果搜索成功则返回所匹配字符第一次出现的下标位置。搜索失败则返回npos。 可以用于搜索第一个非数字、非字母等的位置。 |
s1.find_last_not_of(s2) | 与find_last_of不同的是返回s2中的字符,最后一次出现的位置。 |
# 转换操作
操作 | 作用 |
---|---|
to_string(val) | 将数值类型val转换为字符串,并返回 |
stoi(s) | 将字符串s转换为int类型,并返回 |
stol(s) | 将字符串s转换为long类型,并返回 |
stoul(s) | 将字符串s转换为unsigned long类型,并返回 |
stoll(s) | 将字符串s转换为long long类型,并返回 |
stoull(s) | 将字符串s转换为unsigned long long类型,并返回 |
stof(s) | 将字符串s转换为float类型,并返回 |
stod(s) | 将字符串s转换为double类型,并返回 |
stold(s) | 将字符串s转换为long double类型,并返回 |
# 容器适配器
适配器是标准库的一个通用概念,容器、迭代器、函数都有适配器,一个适配器是一种机制,能使某种事物的行为看起来像另一种事物。
容器适配器能基于现有容器提供特定的接口和行为。适配器会对这些基础容器进行包装,以提供不同的功能或使用模式。主要的容器适配器包括 stack
、queue
和 priority_queue
。
# 适配器类别
# stack(栈)
- stack提供了后进先出(LIFO)的数据结构,适用于需要后进先出访问元素的场景,如表达式求值、回溯算法、函数调用堆栈等。
- 它只允许在容器的顶端进行添加(push)和删除(pop)操作。stack支持deque(默认)、vector、list作为其底层容器。
# queue(队列)
- queue提供了先进先出(FIFO)的队列数据结构,适用于需要按元素进入顺序处理数据的场景,如任务调度、事件处理系统等。
- 它允许元素从队列的后端(back)添加,从前端(front)移除。queue支持deque(默认)、list、vector作为其底层容器。
# priority_queue(优先级队列)
priority_queue提供一个根据元素优先级进行排序的数据结构,其元素顺序与插入顺序无关。适用于需要快速访问最大(或最小)元素的场景中使用,如任务优先级调度、数据流中的最大值查询等。
priority_queue使用vector(默认)、deque作为底层容器,并需要比较函数来确定元素的优先级。
# 使用栈适配器
stack定义在stack头文件中,定义格式如下:
// 定义一个基于deque的空stack
stack<元素类型> 名称;
// 定义一个stack,并使用指定deque对象的元素进行拷贝初始化
stack<元素类型> 名称(deque对象);
// 定义一个基于指定容器类型的stack,指定的容器类型需要是stack支持的类型
stack<元素类型, 容器类型<元素类型>> 名称;
2
3
4
5
6
相关操作如下:
操作 | 作用 |
---|---|
stk.push(val) | 拷贝val值作为新元素并压入栈顶。 |
stk.emplace(val) | 使用val值构造一个新元素并压入栈顶。 |
s.pop() | 删除栈顶元素,但不返回该元素值。 |
s.top() | 返回栈顶元素值,但不将元素弹出栈。 |
适配器只能使用它自身支持的操作,不能使用底层的容器类型的操作。
# 使用队列适配器
queue和priority_queue定义在queue头文件中,定义格式如下:
// 和栈适配器的定义方式差不多
queue<元素类型> 名称;
queue<元素类型, 容器类型<元素类型>> 名称;
priority_queue<元素类型> 名称;
2
3
4
相关操作如下:
操作 | 作用 |
---|---|
q.push(val) | 拷贝val值作为新元素,插入queue末尾或priority_queue中恰当位置。 |
q.emplace(val) | 使用val值构造一个新元素,并插入queue末尾或priority_queue中恰当位置。 |
q.pop() | 删除queue的首元素或priority_queue中优先级最高的元素。 |
q.front() | 返回首元素,但不会删除此元素,适用于queue |
q.back() | 返回尾元素,但不会删除此元素,适用于queue |
q.top() | 返回priority_queue中优先级最高的元素,但不会删除元素。 |
# 泛型算法
# 介绍
泛型算法是能够操作多种数据类型和多种容器类型的通用算法。这些算法定义在标准模板库中,可以与任何符合接口标准的容器配合使用。泛型算法的核心特性是它们是通过模板编写的,可以适应任何数据类型。
泛型算法本质上就是函数或函数模板,它们被称为"算法"是因为它们实现了特定的算法逻辑,用于处理数据集合,比如排序、查找、变换等。这些操作在计算机中被广泛认为是算法的实现。
对于容器类型的泛型算法来说,算法一般并不会直接操作容器,而是遍历传入的两个迭代器之间的元素范围来进行操作。
大多数算法都定义在algorithm头文件的std命名空间中,在numeric头文件中还定义了一组数值泛型算法。
# 谓词
谓词是一个返回布尔类型的可调用的表达式(例如函数、lambda表达式等)。标准库书算法所使用的谓词分两类:一元谓词,仅接收单一的参数。二元谓词,接收两个参数。
# 例子
find
find算法用于查找指定元素,遍历开始迭代器A和迭代器B之间的元素范围,如果元素值等于要查找的值则返回其指针,失败则返回迭代器B指向的指针。
int iarray[] = { 1, 44, 1241, 1245, 745, 124, 33, 12 };
int* res = std::find(std::begin(iarray), std::end(iarray), 124);
cout << typeid(res).name() << " : " << *res << endl;
2
3
sort
sort用于对容器进行排序。
// 按值进行排序
vector<int> iv = { 34, 22, 23, 12, 44, 2 };
sort(iv.begin(), iv.end());
// 比较函数,二元谓词
bool isShorter(const string &s1, const string &s2){
return s1.size() < s2.size();
}
// 传入比较函数,按长度进行排序,从小到大
vector<string> sv = { "what", "is", "Hello", "world" };
sort(sv.begin(), sv.end(), isShorter);
2
3
4
5
6
7
8
9
10
11
# lambda表达式
可调用对象除了函数和函数指针外,还有lambda。lambda表达式由返回类型、参数列表、函数体组成。
lambda表达式一般用在函数代码比较简单、且调用地点很少的场景。对于可能会在不同地方使用、且函数代码较多的场景,我们一般使用函数。
与函数的区别:
- lambda表达式不能声明名称,但是可以通过赋值将表达式绑定给名称。
- 能够定义在函数内部。
- 必须使用置尾方式声明返回类型。
- 需要多定义一个捕获列表,该列表用于定义要捕获(拷贝)进lambda函数体的现有对象的列表,可以捕获多个值(变量)、引用等等,也可以为空。
- 如果捕获列表为
[=]
表示使用隐式值捕获,捕获列表为[&]
表示使用隐式引用捕获,会由编译器根据函数体代码自行推断捕获列表。 - 可以显式和隐式混合使用,但使用了&则只能显式值捕获,而不能显式捕获引用,反之=也一样。
- 默认的值捕获方式是拷贝,如果想要修改捕获的变量的值,则需要在参数列表后面加上关键字mutable,使lambda成为可变lambda,例如:
[val]()mutable{...};
- 如果捕获列表为
定义格式:
// lambda表达式必须包含捕获列表和函数体,可以忽略参数列表和返回类型,忽略参数列表等于无参数,忽略返回类型会根据函数体代码推断出返回类型。
[捕获列表](参数列表) -> 返回类型 { 函数体 }
2
例子:
// 同等于int f = []() -> int {return 42;};
auto f = []{return 42;};
// 实现按大小排序,可以直接将lambda表达式作为谓词
vector<string> sv = { "what", "is", "Hello", "world" };
sort(sv.begin(),
sv.end(),
[](const string &s1, const string &s2)->bool{
return s1.size() < s2.size();
}
);
// std::for_each算法,可以对给定范围内的每个元素执行一个函数,元素类型需要适用于函数参数
vector<string> sv = { "what", "is", "Hello", "world" };
for_each(sv.begin(), sv.end(), [](const stirng &s){
cout << s << endl;
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 参数绑定
在C++中,参数绑定是一种将函数参数部分设为特定值的操作。使函数在调用时只需要提供剩余的参数。
参数绑定使用std::bind函数,定义在头文件functional中,它能在原有函数基础上生成一个新的函数对象,新生成的函数对象会绑定一个或多个固定的实参。
参数绑定还可以使用占位符,用于表示新函数的实参传入的位置。占位符定义在std::placeholders
中。占位符名称的格式为placeholders::_n
,例如新函数传入的第一个实参对应占位符_1
,第二个实参对应_2
,以此类推。占位符在bind时没有顺序要求,可以_2
在前_1
在后。
使用格式
// 参数绑定使用格式,参数会按顺序传给函数
auto new_func = bind(func, arg1, arg2, ..., argN);
2
使用例子
#include <iostream>
#include <functional>
using namespace std::placeholders;
using std::bind;
using std::cout;
int sub(int a, int b, int c) {
return a - b - c;
}
int main() {
auto nsub = bind(sub, _2, 2, _1);
// 调用nsub(3, 5)同等于sub(5, 2, 3);
// _1=3=c,_2=5=a,则a-b-c=5-2-3=0
cout << nsub(3, 5);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
需要注意bind生成的函数对象可能比直接调用略慢,因为涉及到额外的间接调用。
# 特定容器算法
链表类型的list和forward_list中还定义了几个成员函数形式的算法。这几个成员函数算法相较通用算法做了更适合链表类型的优化。
操作 | 作用 |
---|---|
lst1.merge(lst2) | 将来自lst2的元素合并进lst1中,lst1和lst2都必须是有序的。 |
lst.remove(val) | 删除lst中值等于val的所有元素。 |
lst.reverse() | 反转lst中的元素顺序 |
lst.sort(comp) | 排序lst中的元素,不指定comp则按<排序。 |
lst.unique() | 删除同一个值的连续拷贝,也就是元素值连续出现则去重。 |