IO 库
包括三种流:iostream
(基类)、fstream
(文件)、sstream
(字符串),这三种流每种都有宽字符和窄字符两类,每一类又可以细分为输入流、输出流、输入输出流
各种不同类型的流有继承关系
IO 对象没有拷贝或者赋值:
ofstream out1, out2;
out1 = out2; // 错误,不能对流对象赋值
IO 流的条件状态、iostate
、clear
清除错误、setstate
方法
输出缓冲区刷新的原因:
- 程序正常结束,作为
main
函数return
操作的一部分,缓冲刷新被执行 - 缓冲区满
endl
、flush
、ends
等操作显式刷新缓冲区- 可以用操纵符
unitbuf
设置流的内部状态,告诉流接下来的每次写操作后都进行一次flush
操作来清空缓冲区。默认情况下cerr
是设置unitbuf
的,写到cerr
的内容是立即刷新的 - 输出流可能会关联到另一个流,读写被关联的流时,关联到流的缓冲区会被刷新。一般
cin
和cerr
是关联cout
的
文件流在析构时自动关闭打开的文件
顺序容器
标准库中的顺序容器:
vector
:可变大小数组,支持快速随机访问,在尾部之外的位置插入或删除元素可能很慢deque
:双端队列,支持快速随机访问list
:双向链表,只支持双向顺序访问forward_list
:单向链表,只支持单向顺序访问array
:固定大小数组,不能添加或删除元素string
:与vector
相似的容器,但专门用于保存字符
如果没有默认构造函数,那么必须传给容器一个初始化函数
内置数组不支持拷贝,但是 array
支持
array
容器类模板需要指定容器大小,如:
array<int, 42> arr;
容器类型别名
iterator
:迭代器const_iterator
reverse_iterator
size_type
difference_type
:带符号的整数类型,足够保存两个迭代器之间的距离value_type
:元素类型reference
:元素的左值类型,与value_type &
含义相同const_reference
:元素的const
左值类型
顺序容器操作
swap
方法:
- 除
string
和array
外,指向容器的迭代器、引用和指针在swap
操作后都不会失效,它们仍指向swap
操作之前所指向的元素,但是在swap
之后已经属于不同的容器了 string
执行swap
后会导致迭代器、引用和指针失效array
执行swap
会真正交换它们的元素。因此指针、引用和迭代器所绑定的元素保持不变,但进行了值交换
向顺序容器中添加元素:
push_back
:除array
和forward_list
外都支持push_front
:list
、forward_list
和deque
支持insert
:传入一个迭代器,将元素插入到该迭代器之前的位置。还可以传入数量。它返回第一个新加入元素的迭代器,如果数量为 0,则直接返回传入的迭代器emplace
操作:将参数传递给元素类型的构造函数,容器直接在相应内存空间构造元素
当用一个对象初始化容器时,或将对象插入到容器中时,实际上将元素拷贝进容器
访问元素:
front
:每个顺序容器都有back
:除去forward_list
都有at
和下标访问:支持快速随机访问的容器(string
、vector
、deque
、array
)提供。at
会检查下标是否越界,抛出out_of_range
异常
如果容器为空,那么访问 front
和 back
都是未定义行为
这几个方法返回的都是引用,如果容器是 const
,那么返回的是 const
引用
删除元素:
pop_back
、pop_front
erase
:可以传入一个迭代器或两个迭代器(表示范围),返回删除的最后一个元素之后一个位置的迭代器
forward_list
的特殊操作:
由于插入、删除操作需要访问前驱,所以它有所不同,定义了 insert_after
、emplace_after
、erase_after
等方法
并且,定义了首前(off-the-beginning)迭代器 before_begin()
来让我们将元素放到 head 之前
容器操作可能使得迭代器失效
添加元素后:
vector
和string
在添加元素后可能会扩容,此时重新分配空间,原来的迭代器、指针和引用会失效- 对于
deque
:- 它是分段存储,内部存储了段指针数组,而迭代器依赖于这个指针数组
- 插入到首尾位置之外的任何位置,可能在段之间移动元素,位置发生变化,所以可能会导致迭代器、指针和引用失效
- 插入首尾位置,可能会分配新的段,但是底层数据结构不会整体移动,所以引用和指针不会失效,但是迭代器会失效
- 对于列表,添加元素后迭代器、指针和引用仍然有效
删除元素后:
- 对于
vector
和string
,指向被删元素之前元素的迭代器、指针和引用仍有效 - 对于
deque
:- 在首尾之外的位置删除元素,指向被删元素外的其他元素的迭代器、指针和引用失效
- 在首位置删除元素,其他元素不影响
- 在尾位置删除元素,除了
end()
迭代器外,其他元素不影响
- 对于列表,其他元素不影响
注意,不要保存 end
返回的迭代器
容器大小管理操作
如果没有空间容纳新元素,那么容器会分配新空间保存已有的元素和新元素,将已有元素从旧位置移动到新空间中,释放旧存储空间
一般一次扩容会分配 1.5 或 2 倍空间
容器大小管理操作:
shrink_to_fit
:只适用于vector
、string
和deque
,将capacity()
减少为与size()
相同大小。具体实现并不一定退回内存空间capacity
:返回不重新分配空间的前提下可以保存元素的数量reserve
:传入整数 n,分配至少能容纳 n 个元素的空间
下面两个只适用于 vector
和 string
原则:只有当迫不得已时才可以分配新的内存空间
额外的 string
操作
substr
append
、replace
find
、rfind
、find_first_of
等compare
函数- 转换函数:
to_string
、stoi
、stol
、stof
等
容器适配器
除了顺序容器外,标准库还定义了三个顺序容器适配器:
stack
:接受除array
和forward_list
外的顺序容器,默认基于deque
queue
:接受deque
(默认)和list
priority_queue
:接受vector
(默认)和deque
泛型算法
只读算法、写容器元素的算法、重排容器元素的算法
谓词:可调用的表达式,一元谓词、二元谓词
可调用对象、lambda 表达式
lambda 表达式不能有默认参数
lambda 实际上是个匿名对象,可以捕获环境(值方式和引用方式)
对于值方式捕获的变量,如果想要改变被捕获的变量,需要加 mutable
关键字:
void fcn3() {
size_t v1 = 42;
auto f = [v1] () mutable { return ++v1; }
v1 = 0;
auto j = f(); // j 为 43
}
引用捕获的直接改就行
参数绑定
bind
函数进行参数绑定:
int f(int, int, int, int, int);
auto g = bind(f, a, b, _2, c, _1);
上面的代码将有五个参数的函数 f
转换为有两个参数的 g
其中,_1
和 _2
是占位符,在 std::placeholders
命名空间下
默认情况下,bind
通过拷贝的方式将非占位符参数传递给原函数,但是我们可以通过 ref
函数让它通过引用方式传递:
ostream & print(ostring & os, const string & s, char c) {
return os << s << c;
}
for_each(words.begin(), words.end(), bind(print, os, _1, ' ')); // 错误,os 不能拷贝
for_each(words.begin(), words.end(), bind(print, ref(os), _1, ' ')); // 正确
除此之外,还有 cref
函数,传递 const
引用
这些函数都定义在头文件 functional
中
旧版本的 bind1st
和 bind2nd
等已被弃用
额外的迭代器
除了为每个容器定义的迭代器外,还定义了额外的几种迭代器:
插入迭代器
一种迭代器适配器,实现给容器添加元素
it = t
表示在 it
处插入值 t
,根据类型会调用 push_back
、push_front
、insert
*it
、++it
、it++
都不会对 it
做任何事情,每个操作都返回 it
有三种类型:back_inserter
、front_inserter
、inserter
注意插入顺序: copy(a.begin(), a.end(), front_inserter(...))
会将 a
中的元素依次插入到头部,这会导致顺序颠倒
流迭代器
用于 IO 类型对象的迭代器
istream_iterator
用于输入流,ostream_iterator
用于输出流。这两个迭代器要求容器实现了 >>
或者 <<
istream_iterator
:
默认构造函数构造了一个处于尾后位置的迭代器,如:
istream_iterator<int> in_iter(cin), eof; vector<int> vec(in_iter, eof);
允许懒惰读取
ostream_iterator
:
- 构造函数允许传入一个输出流
os
和一个字符串d
(可选),字符串d
表示输出时每个值后面都输出一个d
- 解引用、递增无效
反向迭代器
除了 forward_list
之外其他容器都支持反向迭代器,使用 rbegin
、rend
等来获取反向迭代器
反向迭代器需要递减运算符,因此不能从 forward_list
和流中构造反向迭代器
使用 base
方法可以转回正常的迭代器
移动迭代器
后面的章节会介绍
泛型算法结构
算法要求的迭代器操作可以分为 5 个迭代器类别:
- 输入迭代器:只读不写,单遍扫描,只能递增
- 输出迭代器:只写不读,单遍扫描,只能递增
- 前向迭代器:可读写,多遍扫描,只能递增
- 双向迭代器:可读写,多遍扫描,可递增递减
- 随机访问迭代器:可读写,多遍扫描,支持全部迭代器运算
C++ 标准指明了泛型和数值算法的每个迭代器参数的最小类别
如果名字后面加一个 _copy
,那就是算法的拷贝版本
链表的特定算法
list
和 forward_list
定义了几个成员函数形式的算法,如 sort
、merge
、remove
、reverse
等
它们还定义了 splice
算法
链表特有的操作会改变容器
关联容器
标准库提供 8 个管理容器:
map
set
multimap
、multiset
:关键字可重复出现的map
和set
unordered_map
、unordered_set
、unordered_multimap
、unordered_multiset
:哈希组织的关联容器
有序的关联容器使用 <
运算符来比较两个关键字,并且关键字的比较函数必须严格弱序
关联容器的类型别名:
key_value
mapped_type
:每个关键字关联的类型(仅map
)value_type
:set
中与key_value
相同,map
中为pair<const key_type, mapped_type>
set
的迭代器是 const
的,map
的迭代器中关键字也是 const
的
通常不对关联容器使用泛型算法
插入元素
可以使用 insert
和 emplace
,对于非 multi
容器,它们的返回值通常是一个 pair
:
- 第一个元素是迭代器,指向具有给定关键字的元素
- 第二个元素是一个
bool
值,表示是否插入成功
而对于 multi
容器,无需返回 bool
值,它们直接返回一个指向新元素的迭代器
删除元素
关联容器定义了三个版本的 erase
,可以传入迭代器、元素值、首尾迭代器
返回实际删除的元素数量
访问元素
map
的下标操作:
如果关键字不在 map
中,那么下标操作会添加一个具有此关键字的元素到 map
中
而 at
方法会进行参数检查,如果没有该关键字,返回 out_of_range
异常
下标操作和 at
方法都返回一个左值
可以使用 map
的 find
方法代替下标操作,此外还提供 count
、lower_bound
之类的访问元素的方法
在 multi
容器中,使用迭代器加 count
来访问元素:
string search_item("aaa");
auto entries = authors.count(search_item);
auto iter = authors.find(search_item);
while (entries) {
cout << iter->second << endl;
++iter;
--entries;
}
或者使用 lower_bound
和 upper_bound
的组合
或者直接使用 equal_range
来直接得到两个迭代器表示区间
动态内存
两种智能指针:shared_ptr
和 unique_ptr
,此外还有一个 weak_ptr
shared_ptr
和 unique_ptr
都支持的操作:
- 默认构造函数返回一个空智能指针
operator bool()
返回内部的指针是否为空指针get
方法返回内部的裸指针swap
方法用来交换两个只能指针中的指针
shared_ptr
类支持:
make_shared<T>(...)
方法,传入参数,使用这些参数来初始化一个T
类型的对象shared_ptr<T> p(q)
:拷贝构造函数,会递增计数器,并且q
内的指针要能转换成T*
p = q
:拷贝赋值运算符,会递减p
原来的引用计数,递增q
的引用计数unique
方法:返回是否只有当前这一个引用use_count
方法:返回当前的引用计数,可能很慢- 还有一些传入删除器的方法和构造函数,下面讲
unique_ptr
类支持:
- 带删除器的构造函数
release
方法:放弃控制权,返回指针reset
、reset(q)
、reset(nullptr)
:释放当前指针,然后换成q
或者空指针
unique_ptr
不能被拷贝,只能被移动;shared_ptr
可以被拷贝和移动
new
的一些操作
可以使用 auto
推断要 new
的对象的类型,但只要当括号中仅有单一初始化器时才可以使用
auto p1 = new auto(obj);
使用 new
分配内存时如果发生错误,会抛出 bad_alloc
异常,可以这样阻止:
int * p1 = new int; // 如果分配失败,抛出异常
int * p2 = new (nothrow) int; // 如果分配失败,返回空指针
这是定位 new
的一种
shared_ptr
和 new
结合使用
只支持显式调用构造函数:
shared_ptr<int> p1(new int(42)); // 正确
shared_ptr<int> p2 = new int(42); // 错误
还可以:
- 使用
shared_ptr<T> p(q, d)
构造函数来用d
替代shared_ptr
中的delete
操作 reset()
方法:更新引用计数,并替换为空指针reset(q)
:更新引用计数,并替换为q
reset(q, d)
:注意此时d
是用来释放q
的
不要混合使用普通指针和智能指针
不要使用 get
初始化另一个智能指针或为智能指针赋值
发生异常时,智能指针也可以自动释放资源
上面的 d
被叫做删除器(deleter),如:
void end_connection(connection * p ) { disconnect(*p); }
void f(destination & d) {
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
// 使用连接
// 当 f 退出时(即使是由于异常而退出),connection 会被正确关闭
}
weak_ptr
weak_ptr
不能直接访问对象,需要使用 lock
方法:
if (shared_ptr<int> np = wp.lock()) {
// ...
}
使用 lock
时会增加相应 shared_ptr
的引用计数
动态数组
创建大小为 0 的动态数组是合法的:
char arr[0]; // 错误
char * cp = new char[0]; // 正确
此时 cp
是一个合法的非空指针,但是它不能解引用
unique_ptr
有适用于动态数组的版本,如:
unique_ptr<int[]> up(new int[10]);
up.release();
注意,此时 release
方法会自动销毁这个动态数组!
shared_ptr
不直接支持管理动态数组,我们必须提供一个删除器:
shared_ptr<int> sp(new int[10], [](int * p) { delete [] p; });
sp.reset();
并且必须使用 get
获得原始指针后再使用下标
allocator
类
allocator
类帮助我们将内存分配和对象构造分离开来,它的用法如下:
它默认分配的内存是未构造的(unconstructed),也可以接受额外参数来构造
我们只能对已经真正构造了的元素进行 destory
操作
标准库还为 alloctor
类定义了两个伴随算法: