8. IO库
- C++语言不直接处理输入输出,而是通过定义在标准库中的类型来处理IO,这些类型支持从设备读写数据,设备可以是文件、控制台窗口等。还有一些类型允许内存IO,即从string读写数据
- 标准库定义了3仲不同的IO处理操作,分别定义在3个独立的头文件中,
iostream
定义了用于读写流的基本类型,用于处理控制台IO,fstream
定义了读写命名文件的类型,sstream
定义了读写内存string对象的类型 -
fstream
和stringstream
都是继承自类iostream
,输入类都继承自istream
,输出类都继承自ostream
,因此在istream
对象上执行的操作,也可在ifstream
和istringstream
对象上执行 - 不能对IO对象进行拷贝或赋值,因此也不能将形参或返回类型设置为流类型,进行IO操作的函数通常以引用方式传递和返回流,读写一个IO对象会改变其状态,因此传递和返回的引用不能是const的
- IO操作的一个问题是可能发生错误,有些错误是可恢复的,而有些错误是不可恢复的,因此代码应该在使用一个流之前检查它是否处于良好状态,确定一个流对象状态的最简单方法是将它当作一个条件来使用
while(cin >> word)
,while循环检查>>
表达式返回的流的状态,如果输入操作成功,流保持有效状态,则条件为真 - 每个输出流都管理一个缓冲区,用来保存程序读写的数据,如果执行
os << "hello";
,文本串可能会立即打印,也可能被保存在缓冲区中,导致缓冲刷新(数据真正写到输出设备中)的原因有:① 程序正常结束;② 缓冲区满;③ 使用endl
(添加换行)或flush
(不添加)或ends
(添加空字符)操作符显式刷新缓冲区;④ 使用unitbuf
操作符使流在每次写操作后都进行一次flush
操作;⑤ 一个输出流可能被关联到另一个流,当读写被关联的流时,关联到的流缓冲区会被刷新,默认情况下cin
和cerr
都关联到cout
- 如果程序崩溃,输出缓冲区是不会被刷新的
- 定义一个空文件流对象,可以调用成员函数
open()
将它与文件关联起来,如果调用失败则不能使用该对象,此时和cin
一样返回false,对一个已经打开的文件流调用open()
会失败,并导致该对象不能被使用,为了将文件流关联到另一个文件,必须先用成员函数close()
关闭已经关联的文件,当一个fstream
对象离开其作用域时,与之关联的文件会自动关闭(对象销毁时,close()
自动调用)
9. 顺序容器
9.1 顺序容器概述
- 标准库中有多种顺序容器,所有顺序容器都提供了快速顺序访问元素的能力,但是这些容器在以下两方面都有不同的性能折中:① 向容器添加或从容器中删除元素的代价;② 非顺序访问容器中元素的代价
- 顺序容器类型如下:
- vector:可变大小数组。支持快速随机访问。在尾部之外的位置插入或删除元素很慢
- deque:双端队列。支持快速随机访问。在头尾位置插入和删除速度很快
- list:双向链表。只支持双向顺序访问。在list中任何位置进行插入和删除操作速度都很快
- forward_list:单向链表。只支持单向顺序访问。在链表中任何位置进行插入和删除操作都很快
- array:固定大小数组。支持快速随机访问。不能添加或删除元素。与内置数组相比,array是一种更安全、更易用的数组类型
- string:与vector类似,但专门用于保存字符
- 一般来说,每个容器都定义在一个头文件中,文件名与类型名相同,即deque定义在头文件deque中。容器均定义为模板类
- 所有容器都拥有的操作:
- 类型别名:①
iterator
/const_iterator
:此容器类型的迭代器类型;②size_type
:无符号整数类型,可保存此容器类型最大可能大小;③difference_type
:带符号整数类型,可保存两个迭代器之间的距离;④vaule_type
:元素类型;⑤reference
/const_reference
:元素的左值类型- 构造函数:①
C c;
:默认构造函数,构造空容器;②C c1(c2);
:拷贝构造函数;③C c(b,e);
将迭代器b和e范围内的元素拷贝到c(array不支持);④C c{x,y,z};
:列表初始化- 赋值与swap:①
c1=c2;
;②c={x,y,z};
;③c1.swap(c2);
;(swap通常比拷贝快得多)④swap(c1,c2);
(最好统一使用非成员版本的swap,适用于泛型编程)- 大小:①
c.size()
:c中元素数目(不支持forward_list);②c.max_size()
:c中可保存的最大元素数目;③c.empty()
- 添加删除元素(array不支持,不同容器中这些操作的接口都不同):①
c.insert(args)
;②c.emplace(inits)
;③c.erase(args)
;④c.clear()
- 关系运算符:①
==
/!=
:所有容器都支持;②<
/<=
/>
/>=
:无序关联容器不支持- 获取迭代器:
c.begin(),c.end()
/c.cbegin(),c.cend()
- 反向容器的额外成员(不支持forward_list):①
reverse_iterator
/const_reverse_iterator
;②c.rbegin(),c.rend()
/c.crbegin(),c.crend()
- 赋值相关运算会导致指向左边容器内部的迭代器、引用、指针失效。而swap操作将容器内容交换不会导致失效(容器类型为array和string除外)
- 用一个对象来初始化容器时,或将一个对象插入到容器中时,实际上放入到容器中的是对象值的一个拷贝,而不是对象本身
9.2 顺序容器专有操作
- 初始化(array不可用):①
C seq(n);
:seq包含n个元素,对这些元素进行值初始化,此构造函数是explicit的(string不可用);②C seq(n,t);
:seq包含n个初始化为t的元素- 赋值(array不适用):①
seq.assign(b,e);
:将seq中元素替换为迭代器b和e范围内的元素,b和e不能指向seq本身;②seq.assign(il);
:将seq中的元素替换为初始化列表il中的元素;③seq.assign(n,t);
:将seq中的元素替换为n个值为t的元素- 添加元素(array不可用):①
c.push_back(t)
/c.emplace_back(args)
:在c的尾部创建一个值为t或由args创建的元素,返回void(forward_list不可用);②c.push_front(t)
/c.emplace_front(args)
:在c的头部创建一个元素,返回void(vector和string不可用);③c.insert(p,t)
/c.emplace(p,args)
:在迭代器p指向的元素之前创建一个元素,返回指向新添加的元素的迭代器;④c.insert(p,n,t)
/c.insert(p,b,e)
/c.insert(p,il)
- 访问元素(返回的均为引用):①
c.back()
(forward_list不可用);②c.front()
;③c[n]
/c.at(n)
(只能用于string、vector、deque、array)- 删除元素:①
c.pop_back()
:删除c中尾元素(forward_list不可用);②c.pop_front()
:删除c中首元素(vector和string不可用);③c.erase(p)
:删除迭代器p所指向元素,返回被删元素之后元素的迭代器;④c.erase(b,e)
:删除迭代器b和e范围内的元素,返回最后一个被删元素之后元素的迭代器;⑤c.clear()
:删除c中所有元素,返回void- 改变容器大小:①
c.resize(n)
:调整c的大小为n个元素,添加的新元素进行值初始化,若n小于原长度,则多出的元素被丢弃;②c.resize(n,t)
:添加的新元素初始化为值t
- 向一个vector、string、deque插入元素会使所有指向容器的迭代器、引用、指针失效
- 容器中访问元素的成员函数返回的都是引用,如果容器是一个const对象,则返回值是const引用
- 下标运算符并不检查下标是否存在合法范围内,使用越界的下标是一种严重的设计错误,使用
at()
成员函数时如果下标越界,会抛出out_of_range异常 - 删除deque中除首尾位置之外的任何元素都会使所有迭代器、引用、指针失效。指向vector或string中删除点之后位置的迭代器、引用、指针都会失效
- forward_list为单向链表,在单向链表中,没有简单的方法来获取一个元素的前驱,forward_list并未定义常规的插入删除操作,而是定义了名为
insert_after
、emplace_after
、erase_after
的操作,并且定义了首前迭代器before_begin
,该迭代器允许在链表首元素之前添加或删除元素 - 如果resize缩小容器,则指向被删除元素的迭代器、引用、指针都会失效。对vector、string、deque进行resize可能导致迭代器、引用、指针失效
- 如果在一个循环中插入或删除deque、string、vector中的元素,不要缓存end返回的迭代器,每次循环都会重新计算end
- 当容器中元素是连续存储,且容器大小可变时,向容器中添加元素必须分配新的内存空间来保存已有元素和新元素,vector和string的实现通常会分配比新的空间需求更大的内存空间
- vector和string类型提供了一些成员函数,实现对容器大小的管理操作:①
c.capacity()
:不重新分配内存空间的话,c可以保存多少元素;②c.reserve(n)
:分配至少能容纳n个元素的内存空间;③c.shrink_to_fit()
:将能保存的元素空间缩大小减为当前的元素个数
9.3 额外的string操作
- 构造string的其他方法:除通用的构造函数外,string还支持另外三个构造函数:①
string s(cp,n)
:s是cp指向数组中前n个字符的拷贝,此数组至少包含n个字符;②string s(s2,pos2)
:s是s2从下标pos2开始的字符的拷贝;③string s(s2,pos2,len2)
:s是s2从下标pos2开始len2个字符的拷贝;④s.substr(pos,n)
:子字符串操作,返回一个string,包含s中从pos开始的n个字符的拷贝 - 修改string的其他操作:①
s,insert(pos,args)
;②s.erase(pos,len)
;③s.assign(args)
;④s.append(args)
;⑤a.replace(range,args)
。其中args可以是下列形式之一:str
/str,pos,len
/cp,len
/cp
/n,c
/b,e
/初始化列表
- string搜索操作:每个搜索操作都返回一个
string::size_type
值,表示匹配发生位置的下标,如果搜索失败,则返回一个名为string::npos
的static成员,二者均为unsign类型:①s.find(args)
/s.rfind(args)
:查找s中args第一次/最后一次出现的位置;②s.find_first_of(args)
/s.find_last_of(args)
:在s中查找arg中任何一个字符第一次/最后一次出现的位置;③s.find_first_not_of(args)
/s.find_last_not_of(args)
:在s中查找第一个/最后一个不在args中的字符。其中args是以下形式之一:c,pos
/s2,pos
/cp,pos
/cp,pos,n
- 除了关系运算符外,标准库string类型还提供了一组compare函数,与C标准库的strcmp函数类型,根据s是等于、大于、小于参数指定的字符串,返回0、正数、负数,
s.compare(args)
的参数形式有:s2
/pos1,n1,s2
/pos1,n1,s2,pos2,n2
/cp
/pos1,n1,cp
/pos1,n1,cp,n2
- 字符串中常常包含表示数值的字符,新标准引入了多个函数,可以实现数值数据与string之间的转换:①
to_string(val)
:将数值类型转为string;②stoi(s,p,b)
/stol(s,p,b)
/stoul(s,p,b)
/stoll(s,p,b)
/stoull(s,p,b)
:将s转为整型,b表示转换用的基数,默认为10,p是size_t指针,用来保存s中第一个非数值字符的下标,默认为0,即不保存下标;③stof(s,p)
/stod(s,p)
/stold(s,p)
:将s转为浮点型
9.4 容器适配器
- 除了顺序容器外,标准库还定义了三个顺序容器适配器:stack、queue、priority_queue,本质上一个容器适配器是一种机制,能使某种事物的行为看起来像另外一种事物,如stack适配器接受一个顺序容器,使其操作起来像一个stack一样
- 每个适配器都定义两个构造函数:默认构造函数创建一个空对象,接受一个容器的构造函数拷贝该容器来初始化适配器,如
stack<int> stk(seq);
,默认情况下stack和queue是基于deque实现的,可以在创建一个适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型stack<string,vector<string>> str_stk;
- 所有适配器都要求容器具有添加、删除、访问尾元素元素的能力,因此适配器不能构造在array和forward_list上。stack可以用除上述两种外的任意容器类型来构造,queue不能基于vector构造,priority_queue不能基于list构造。可通过适配器的
container_type
类型访问底层容器类型 - 栈操作:①
s.pop()
;②s.push(item)
/s.emplace(args)
:③s.top()
- 队列操作:①
q.pop()
;②q.front()
;③q.back()
(只能用于queue);④q.top()
(只能用于priority_queue);⑤q.push(item)
/q.emplace(args)
10. 泛型算法
10.1 泛型算法概述
- 标准库并没有给容器定义大量的操作,而是提供了一组算法,这些算法大多数都独立于任何容器,称这些算法为泛型的,可用于不同类型的容器和不同类型的元素
- 大多数算法都定义在头文件
<algorithm>
中,还有一组数值泛型算法定义在头文件<numeric>
中。一般情况下,这些算法并不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作,如auto result=find(vec.cbegin(),vec.cend(),val);
,如果发现匹配元素,则返回指向该元素的迭代器,否则返回尾后迭代器 - 标准库提供了超过100个算法,大多数算法遍历输入范围的方式相似,但它们使用范围中元素的方式不同,理解算法最基本的方法就是了解它们是否:读取元素、改变元素、重排元素顺序
- 只读算法:
find()
函数和count()
函数都是只读算法,定义在头文件<numeric>
中的accumulate()
函数也是只读算法,equal(r1.cbegin(),r1.cend(),r2.cbegin())
函数用于比较两个序列是否保存相同的值,该函数接受三个迭代器,前两个表示第一个序列的范围,第三个表示第二个序列的首元素,这种只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长 - 写容器元素的算法:①
fill(vec.begin(),vec.end(),10)
向给定序列中写入数据;②fill_n(vec.begin(),n,0)
:必须保证vec至少包含n个元素/fill_n(back_inserter(vec),n,0)
:使用插入迭代器back_inserter会调用push_back来添加元素;③copy(bigin(a1),end(a1),a2)
:把a1的内容拷贝给a2;④replace(ilst.cbegin(),ilst.cend(),0,10)
:将序列中所有0替换为10/replace_copy(ilst.cbegin(),ilst.cend(),back_inserter(ivec),0,10)
:ilst不变,ilst中0替换为10后拷贝到ivec中 - 重排容器元素的算法:①
sort()
:排序;②unique()
:去除相邻重复项;以下步骤消除vector中的重复单词
sort(v.begin(),v.end()); //排序
auto end_unique = unique(v.begin(),v.end()); //unique函数实现无重复排列,返回指向不重复区域的尾后迭代器
v.erase(end_unique,v.end()); //删除重复单词
10.2 定制操作
- 很多算法都会比较输入序列中的元素,默认情况下,这类算法使用元素类型的
<
或=
运算符来比较。标准库还为这些算法提供了额外的版本,使得能够用自己定义的操作来代替默认运算符 - 谓词是一个可调用的表达式,其返回结果是一个能用作条件的值,标准库算法所使用的谓词分为一元谓词(接受一个参数)和二元谓词(接受两个参数)。接受谓词参数的算法对输入序列中的元素调用谓词,元素类型必须能转换为谓词的参数类型
- 接受一个二元谓词参数的
sort()
函数用谓词代替<
来比较元素,如sort(words.begin(),words.end(),isShorter)
,若想要保证稳定排序,可使用stable_sort()
函数 - 可以向一个算法传递任何类别的可调用对象,对于一个对象或表达式,如果可以对其使用调用运算符,则称它为可调用的。可调用对象包括:函数、函数指针、重载了函数调用运算符的类、lambda表达式
- 一个lambda表达式表示一个可调用的代码单元,可以将其理解为一个未命名的内联函数,与函数类似,一个lambda具有一个返回类型、一个参数列表、一个函数体。但与函数不同,lambda可能定义在函数内部
- 一个lambda表达式具有以下形式:
[capture list] (parameter list) -> return type {function body}
其中,捕获列表是一个lambda所在函数中定义的局部变量的列表,通常为空,参数列表、返回类型、函数体与普通函数一样,但是lambda必须使用尾置返回来指定返回类型。lambda可以忽略参数列表和返回类型,但必须包含捕获列表和函数体:auto f = []{return 10;};
,忽略参数列表即参数列表为空,忽略返回类型则从函数体代码中推断返回类型,若函数体中包含单一return之外的内容且未指定返回类型,则返回void
- lambda不能有默认参数,实参数目永远与形参数目相等,空捕获列表说明此lambda不使用它所在函数中的任何局部变量,lambda使用示例如下:
//将words按长度稳定排序
stable_sort(words.begin(),words.end(),[](const string &a,const string &b){return a.size()<b.size();});
//获取一个迭代器,指向第一个长度大于sz的元素
auto wc = find_if(words.begin(),words.end(),[sz](const string &a){return a.size()>=sz;});
//从wc迭代器开始打印单词,每个单词后接一个空格
for_each(wc,words.end(),[](const string &s){cout<<s<<" "});
- 当定义一个lambda时,编译器生成一个与lambda对应的新的类类型
- 类似于参数传递,变量的捕获方式也可以是值捕获(
[a]
)或引用捕获[&a]
,采用值捕获的前提是变量可以拷贝,被值捕获的变量是在lambda创建时拷贝,而非调用时,因此随后对其的修改不会影响到lambda内对应的值,当以引用方式捕获一个变量时,必须保证在lambda执行时变量是存在的 - 除了显式列出希望使用的来自所在函数的变量之外,还可以让编译器根据lambda体中的代码来推断要使用哪些变量,此时应在捕获列表中写一个
[&]
或[=]
,&告诉编译器采用引用捕获方式,=采用值捕获方式。当混用隐式捕获和显式捕获时,捕获列表中第一个元素必须是&或= - 默认情况下,对于一个值被拷贝的变量,lambda不会改变其值,如果希望能改变一个被捕获的变量的值,必须在参数列表首加上关键字mutable:
auto f=[v]()mutable{return ++v;};
,一个引用捕获的变量是否可以修改依赖于该引用指向的是一个const类型还是非const类型 - 对于捕获列表为空的lambda,通常可以用函数来代替,对捕获局部变量的lambda,需要使用标准库中的
bind()
函数,该函数定义在头文件<functional>
中,可以将bind函数看作一个通用的函数适配器,接受一个可调用对象,生成一个新的可调用对象auto newCallable = bind(callable,arg_list);
,当调用newCallable时,会调用callable,并传给它arg_list中的参数,arg_list是一个逗号分隔的参数列表,其中形如_n
的参数代表占位符,绑定引用参数时要使用ref()
或cref()
函数:bind(print,ref(os),_1,' ')
10.3 再探迭代器
- 除了每个容器自己的迭代器之外,标准库在头文件
<iterator>
中还定义了额外几种迭代器,包括:插入迭代器、流迭代器、反向迭代器、移动迭代器 - 插入迭代器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。插入器有三种类型:①
back_inserter(c)
创建一个使用c.push_back(t)
的迭代器;②front_inserter(c)
创建一个使用c.push_front(c)
的迭代器;③inserter(c,iter)
创建一个使用c.insert(iter,t)
的迭代器。使用插入器时,只需要iter=t;
即可在iter指定的位置处插入值t - 流迭代器可以用泛型算法从流对象读取数据及写入数据,
istream_iterator
读取输入流,ostream_iterator
向一个输出流写数据
istream_iterator<T> in(is); //in从输入流is读取类型为T的值
istream_iterator<T> end; //读取类型为T的尾后迭代器
ostream_iterator<T> out(os); //out将类型为T的值写到输出流os中
ostream_iterator<T> out(os,d); //out将类型为T的值写到输出流os中,且每个值后都输出一个d,d为C风格的字符串
- 反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器,++it会移动到前一个元素,–it会移动到下一个元素,除了forward_list之外,其他容器都支持反向迭代器,可以通过调用
rbegin(),rend(),crbegin(),crend()
成员函数来获得反向迭代器,反向迭代器调用base()
成员函数可以得到正向迭代器,但是指向的元素不同(正向迭代器指向后一个元素)
10.4 泛型算法结构
- 任何算法的最基本特性是它要求其迭代器提供哪些操作,算法所要求的迭代器操作可分为5个迭代器类别:
- 输入迭代器:只读,不写,单遍扫描,只能递增;
- 输出迭代器:只写,不读,单遍扫描,只能递增;
- 前向迭代器:可读写,多遍扫描,只能递增;
- 双向迭代器:可读写,多遍扫描,可递增递减;
- 随机访问迭代器:可读写,多遍扫描,支持全部迭代器运算
- 迭代器也定义了一组公共操作,一些操作所有迭代器都支持,另外一些只有特定迭代器才支持,除了输出迭代器之外,一个高层的迭代器支持低层迭代器的所有操作,各迭代器需要支持的操作包括:
- 输入迭代器:① 用于比较两个迭代器的相等
==
和不相等运算符!=
;② 迭代器的前置和后置递增运算++
;③ 用于读取元素的解引用运算符*
(只会出现在赋值运算符的右侧);④ 用于解引用迭代器并提取成员的箭头运算符->
- 输出迭代器:与1类似,解引用运算符只能出现在赋值运算符的左侧
- 前向迭代器:1和2会破环迭代器原本结构,只能单遍扫描,前向迭代器可以保存当前状态,多次读写同一个元素
- 双向迭代器:还支持前置和后置的递减运算符
--
- 随机访问迭代器:还支持 ① 用于比较两个迭代器相对位置的关系运算符
<,<=,>,>=
;② 迭代器和一个整数值的加减运算+,+=,-,-=
;③ 用于两个迭代器的减法运算-
;④ 下标运算符iter[n],*iter[n]
(两者等价)
- 大多数算法的形参模式是下列4仲形式之一:其中dest参数是一个表示算法可以写入的目的位置的迭代器,常被用于绑定到一个插入迭代器或是一个
ostream_iterator
alg(beg,end,other args);
alg(beg,end,dest,other args);
alg(beg,end,beg2,other args);
alg(beg,end,beg2,end2,other args);
- 算法命名规范:① 一些算法使用重载形式传递一个谓词,来代替
<
或==
;② 接受一个元素值的算法通常会有一个不同名的后缀加上_if
的版本,该版本接受一个谓词代替元素值,如find
和find_if
;③ 默认情况下,重排元素的算法将重排后的元素写回给定的输入序列中,通常提供额外的拷贝版本,加上后缀_copy
,如reverse
和reverse_copy
- 与其他容器不同,链表类型list和forward_list定义了几个成员函数形式的算法,特别定义了独有的
sort(),merge(),remove(),reverse(),unique()
算法,通用版本的sort要求随机访问迭代器,因此不能用于链表类型
11. 关联容器
11.1 关联容器概述
- 关联容器中的元素是按关键字来保存和访问的,两个主要的关联容器类型是
map
和set
,标准库提供8个关联容器,其不同体现在3个维度上:① set或map;② 是否允许重复的关键字,允许重复关键字的容器以multi
开头;③ 是否按顺序保存元素,不按关键字顺序存储的容器以unordered_
开头 - map和
multimap
定义在头文件<map>
中,set和multiset
定义在头文件<set>
中,unordered_multimap
和unordered_multiset
定义在头文件<unordered_map>
和<unordered_set>
中 - 关联容器对其关键字类型有限制,有序容器的关键字类型必须定义元素比较的方法,默认情况下标准库使用
<
运算符来比较两个关键字 - 头文件
<utility>
中,定义了名为pair
的标准库类型,一个pair保存两个数据成员,当创建一个pair时,必须提供两个类型名,pair的默认构造函数对数据成员进行值初始化,map中的元素是pair,标准库中的pair操作包括:①pair<T1,T2> p
;②pair<T1,T2> p(v1,v2)
;③make_pair(v1,v2)
;④p.first
,p.second
11.2 关联容器操作
- 关联容器还定义了3仲类型:①
key_value
:关键字类型;②mapped_type
:map的值类型;③value_type
:对于set为关键字类型,对于map为pair<const key_type,mapped_type>
- 当解引用一个关联容i去迭代器时,会得到一个类型为容器的
value_type
的值的引用。对map而言,会返回一个pair类型,其first成员保存const关键字,不能修改,second成员保存值,可以修改;对set而言,会返回const关键字,不能修改。当使用一个迭代器遍历一个有序关联容器时,迭代器按关键字升序遍历元素 - 遍历关联容器:
auto m_it = m.cbegin();
while(m_it != m.cend()){
cout << m_it->first << ":" << m_it->second << endl;
++m_it;
}
- 关联容器的
insert()
成员向容器中添加一个元素或一个元素范围,由于map和set包含不重复的关键字,因此插入一个已存在的元素对容器没有任何影响:①c.insert(v)
/c.emplace(args)
:v为value_type类型的对象,args用来构造一个元素;②c.insert(b,e)
/c.insert(il)
:b和e是迭代器,il是花括号列表;③c.insert(p,v)
/c.emplace(p,args)
:将迭代器p作为一个提示,指出从哪里开始搜索新元素应该存储的位置。对于不包括重复关键字的容器,添加单一元素的insert和emplace返回一个pair,pair的first成员是一个迭代器,指向具有给定关键字的元素,second成员是一个bool值,指出元素是插入成功还是已存在于容器中 - 从关联容器中删除元素:①
c.erase(k)
:从c中删除每个关键字为k的元素,返回删除元素的数量;②c.erase(p)
:从c中删除迭代器p指定的元素,返回一个p之后的迭代器;③c.erase(b.e)
:删除迭代器b和e范围中的元素,返回e - map和unordered_map容器提供了下标运算符和一个对应的at函数,set、multimap、unordered_multimap不支持下标:①
c[k]
:返回关键字为k的元素,若k不在c中,添加一个关键字为k的元素,对其进行值初始化;②c.at(k)
:访问关键字为k的元素,若k不在c中,抛出一个out_of_range异常 - 在一个关联容器中查找元素:①
c.find(k)
:返回一个迭代器,指向第一个关键字为k的元素,若k不在容器中,则返回尾后迭代器;②c.count(k)
:返回关键字等于k的元素的数量;③c.lower_bound(k)
:返回一个迭代器,指向第一个关键字不小于k的元素(不适用于无序容器);④c.upper_bound(k)
:返回一个迭代器,指向第一个关键字大于k的元素(可与上一个函数一起给定在multimap或multiset中某个关键字对应的迭代器范围);⑤c.equal_range(k)
:返回一个迭代器pair,表示关键字等于k的元素的范围,若k不存在,pair两个成员均等于c.end() - 无序容器:新标准定义了4个无序关联容器,这些容器不使用比较运算符来组织元素,而是使用一个哈希函数和关键字类型的==运算符。无序容器在存储上组织为一组桶,每个桶保存零个或多个元素,使用一个哈希函数将元素映射到桶。如果容器允许重复关键字,所有具有相同关键字的元素都会在同一个桶中,因此无序容器的性能依赖于哈希函数的质量和桶的数量和大小。理想情况下,哈希函数会将每个特定的值映射到唯一的桶,但是将不同关键字的元素映射到相同的桶也是允许的,当一个桶保存多个元素时,需要顺序搜索这些元素来查找想要的那个
12. 动态内存
12.1 对象与内存分配关系
- 程序中所使用的对象都有严格定义的生存期,全局对象在程序启动时分配,在 程序结束时销毁;局部自动对象在进入其定义所在程序块时被创建,在离开块时销毁;局部static对象在第一次使用前分配,在程序结束时销毁。除了自动和static对象外,C++还支持动态分配对象,动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时,这些对象才会销毁
- 动态对象地正确释放是及其容器出错地地方,为了更安全地使用动态对象,标准库定义了两个智能指针类型来管理动态分配的对象,当一个对象应该被释放时,指向它的智能指针可以确保自动地释放它
- 静态内存用来保存局部static对象、类static数据成员、定义在任何函数之外的变量,栈内存用来保存定义在函数内的非static对象。分配在静态或栈内存中的对象由编译器自动创建和销毁,栈对象仅在其定义的程序块运行时才存在,static对象在使用前分配,程序结束时销毁。除了静态内存和栈内存,每个程序还有一个内存池,这部分内存被称为自由空间或堆,程序用堆来存储动态分配的对象
12.2 动态内存与智能指针
- 在C++中,动态内存的管理是通过一对运算符来完成的:①
new
:在动态内存中为对象分配空间并返回一个指向该对象的指针;②delete
:接受一个动态对象的指针,销毁该对象并释放与之关联的内存 - 为了更容易也更安全地使用动态内存,新的标准库提供了两种智能指针类型来管理动态对象,智能指针负责自动释放所指向地对象,这两种智能指针地区别在于管理底层指针的方式:①
shared_ptr
:允许多个指针指向同一个对象;②unique_ptr
:独占所指向的对象。标准库还定义了一个名为weak_ptr
的伴随类,它是一种弱引用,指向shared_ptr所管理的对象,这三种类型都定义在<memory>
头文件中 - 智能指针也是模板,创建一个智能指针时,必须提供指针可以指向的类型,默认初始化的智能指针中保存着一个空指针:
shared_ptr<string> p1;
,智能指针的使用方法与普通指针类似,解引用一个智能指针返回它指向的对象,如果在一个条件判断中使用智能指针,就是检测它是否为空 - shared_ptr和unique_ptr都支持的操作有:①
shared_ptr<T> sp
/unique_ptr<T> up
:空智能指针;②p.get()
:返回p中保存的指针,要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了;③swap(p,q)
/p.swap(q)
:交换p和q中的指针 - shared_ptr独有的操作:①
make_shared<T>(args)
:返回一个指向类型T对象的智能指针,使用args初始化此对象;②shared<T>p(q)
:p是q的拷贝,此操作会递增q中的计数器,q中的指针必须能转换为T*;③p = q
:p和q所保存的指针必须能相互转换,此操作会递减p的引用计数,递增q的引用计数,若p的引用计数变为0,则将其管理的原内存释放;④p.use_count()
:返回与p共享对象的智能指针对象,该操作很慢,主要用于调试;⑤p.unique()
:若p.use_count()为1,返回true,否则返回false - 可以认为每个shared_ptr都有一个关联的计数器,称之为引用计数,当拷贝一个shared_ptr时,计数器会递增,当为shared_ptr赋予一个新值或shared_ptr被销毁(如局部shared_ptr离开其作用域)时,计数器就会递减。用哪种数据结构来记录有多少指针共享对象,完全由标准库的具体实现来决定
- shared_ptr的析构函数会递减它所指向的对象的引用计数,如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它所占用的内存
- 默认情况下,动态分配的对象是默认初始化的:
int *p = new int;
,*p的值未定义,在类型名后跟一对括号即可对动态分配的对象进行值初始化:int *p = new int();
:*p的值为0 - 如果提供了一个括号包围的初始化器,就可以使用auto从该初始化器来推断想要分配的对象的类型:
auto p = new auto(obj);
,p指向一个与obj类型相同的对象,该对象用obj进行初始化,当且仅当括号中有单一初始化器时才可使用auto - 类似于其他任何const对象,一个动态分配的const对象必须进行初始化,对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象必须显式初始化:
const int *p = new const int(10);
,new返回的指针是一个指向const的指针 - 若一个程序用光了它所有可用的内存,new表达式就会失败,默认情况下,如果new不能分配所要求的内存空间,就会抛出一个类型为bad_alloc的异常,可通过
int* p = new (nothrow) int;
的方式阻止抛出异常,这种形式的new称为定位new
12.3 智能指针的使用
- 若不初始化一个智能指针,它就会被初始化为一个空指针,还可以使用new返回的指针来初始化智能指针:
shared_ptr<int> p(new int(10));
,接受指针参数的智能指针构造函数是explicit的,因此不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化的形式 - 定义和改变shared_ptr的方法:①
shared_ptr<T> p(q)
:p管理内置指针q所指向的对象,q必须指向new分配的内存,且能偶转换为T*类型;②shared_ptr<T> p(u)
:p从unique_ptr u那里接管了对象的所有权,将u置为空;③shared_ptr<T> p(q,d)
:p使用可调用对象d来代替delete;④shared_ptr<T> p(p2,d)
:p是shared_ptr p2的拷贝;⑤p.reset()
/p.reset(q)
/p.reset(q,d)
:若p是唯一指向其对象的shared_ptr,reset会释放此对象,并将p置为空;若传递了参数内置指针q,会令p指向q;若还传递了参数d,会调用d来代替delete - 注意事项:① 不要混用普通指针和智能指针:当将一个shared_ptr绑定到一个普通指针时,就将内存的管理责任交给了这个shared_ptr,此时就不该再使用内置指针来访为shared_ptr所指向的内存了,因为无法知道对象何时会被销毁;② 不要使用get()初始化另一个智能指针或为智能指针赋值:这样做会导致内存被二次delete,get()只有在确定代码不会delete指针的情况下使用
- 有些没有定义析构函数的类,需要用户显式地释放所使用地资源,如果忘记释放资源或在释放之前放生了异常,程序会发生资源泄露。可以将释放资源的函数作为删除器与原对象一起传入智能指针:
shared_ptr<Connection> p(&con,disconnection);
,此时资源会自动释放 - 与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象,当unique_ptr被销毁时,它所指向的对象也被销毁。unique_ptr没有类似make_shared的标准库函数,必须将其绑定在一个new返回的指针上,unique_ptr不支持普通的拷贝或赋值
- unique_ptr操作:①
unique_ptr<T,D> u(d)
:创建一个空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete;②u.release()
:放弃对指针的控制权,将u置为空并返回指针;③u.reset()
/u.reset(q)
/u.reset(nullptr)
:释放u指向的对象,如果提供了内置指针q,令u指向这个对象,否则将u置为空 - 可以通过调用release或reset将指针的所有权从一个非const的unique_ptr转移给另一个unique_ptr:
unique_ptr<int> p2(p1.release())
/p2.reset(p1.release())
。直接使用release并不会释放内存,而且会丢失指针。可以拷贝或赋值一个将要被销毁的unique_ptr,如从函数返回一个unique_ptr。重载一个unique_ptr的删除器必须在尖括号中提供删除器类型:unique_ptr<Connection,decltype(disconnection)*> p(&con,disconnection);
- weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象,绑定weak_ptr不会改变shared_ptr的引用计数,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,无论还有没有weak_ptr指向它
- weak_ptr操作:①
weak_ptr<T> w(sp)
/w = sp
:将w指向shared_ptr指针sp指向的对象;②w.reset()
:将w置为空;③w.use_count()
:与w共享的shared_ptr的数量;④w.expired()
:若w.use_count()为0,返回true,否则返回false;⑤w.lock()
:若w.expired()为true,返回一个空shared_ptr,否则返回一个指向w的对象的shared_ptr
12.4 动态数组
- new和delete运算符一次分配和释放一个对象,C++和标准库还提供了两种一次分配一个对象数组的方法:① C++定义了另一种new表达式语法,可以分配并初始化一个对象数组;② 标准库中包含了一个名为allocator的类,可以将分配和初始化分离。大多数应用都没有直接访问动态数组的需求,使用标准库容器会更加简单和快速
- 使用new分配动态数组时,要指明分配的对象的数目:
int *p = new int[10];
,此时会得到一个元素类型的指针(动态数组并不是数组类型),由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin或end。new分配的对象,无论是单个分配还是数组中的都是默认初始化的,加上空括号后变为值初始化:int *p = new int[10]();
,释放动态数组:delete [] p;
- 标准库提供了一个可以管理new分配的数组的unique_ptr版本:
unique_ptr<int[]> p(new int[]10);
,可以使用下标运算符来访问数组中的元素。与unique_ptr不同,shared_ptr不直接支持管理动态数组,如果要使用shared_ptr管理,必须提供自定义的删除器shared_ptr<int> p(new int[10],[](int *p){delete[] p});
,并且不能使用下标运算符,只能用gert()获取内置指针后再访问数组元素 - new将内存分配和对象构造结合在了一起,delete将对象析构和内存释放结合在了一起,这样在分配少量对象时方便使用,但是当要分配大量内存时,希望将内存分配和对象构造分离。标准库
allocator
定义在头文件<memory>
中,提供了一种类型感知的内存分配方法,分配的内存是未构造的 - allocator类算法:①
allocator<T> a
:定义一个可以为类型T分配内存的allocator对象;②a.allocator(n)
:分配一段可以保存n个类型为T的对象的未构造的内存;③a.deallocator(p,n)
:释放从T*指针p开始的n个T类型对象的内存,p必须分配过n个T类型对象,且已被创建的对象必须调用过destroy;④a.construct(p,args)
:在p指向的未构造内存中通过args构造一个对象;⑤a.destory(p)
:对p指向的对象执行析构函数;⑥uninitialized_copy(b,e,b2)
/uninitialized_copy_n(b,n,b2)
:将迭代器b到e中的元素拷贝到迭代器b2指向的未构造内存中,b2的内存必须足够大;⑦uninitialized_fill(b,e,t)
/uninitialized_fill_n(b,n,t)
:在迭代器b到e的未构造内存中放入值为t的拷贝