第二部分:C++标准库
1.为了支持不同种类的IO处理操作,标准库定义了以下类型的IO,分别定义在三个独立的文件中:iostream文件中定义了用于读写流的基本类型;fstream文件中定义了读写命名文件的类型;sstream文件中定义了读写内存string对象的类型。
其中宽字符版本(wchar_t类型)的类型和函数的名字以一个w开始(P278)。类型ifstream和istringstream都继承自istream,通过继承机制,可以像使用istream对象一样来使用ifstream和istringstream对象,例如不仅可以对一个istream对象调用getline和>>,也可以对一个ifstream或istringstream对象调用getline和>>。
2.IO对象无法进行拷贝或赋值,因此不能将形参或返回类型设置为流类型,进行IO操作的函数通常以引用方式传递和返回流,读写一个IO对象会改变其状态,因此传递和返回的引用不能是const的(P279)。流的各种状态如下图所示:
一个流一旦发生错误,其后续的IO操作都会失败,只有当一个流处于无错状态时,才可以从它读取数据或向它写入数据。当流的状态位eofbit、failbit或badbit中的任何一个都没有被置位时,cin被视为真,表示输入流处于正常状态,可以继续读取数据(此时if(cin)的判断即为真,反之为假)。
3.每个输出流都管理一个缓冲区,用来保存程序读写的数据,导致缓冲区刷新的原因有:
- 程序正常结束,作为main函数的return操作的一部分,缓冲刷新被执行
- 缓冲区满时,需要刷新缓冲,刷新后新的数据才能继续写入缓冲区
- 可以使用操纵符如endl(会输出一个换行,然后刷新缓冲区)、flush(如cout<<”hi”<<flush,输出hi然后刷新缓冲区,但不输出任何额外字符)、ends(会输出一个空字符,然后刷新缓冲区)来显式刷新缓冲区(P282)
- 在每个输出操作之后,可以用操纵符unitbuf设置流的内部状态(cout<<unitbuf,则所有输出操作之后都会立即刷新缓冲区),来清空缓冲区。默认情况下,对cerr是设置unitbuf的,因此写到cerr的内容都是立即刷新的
- 一个输出流可能被关联到另一个流。在这种情况下,当读写被关联的流时,关联到的流的缓冲区会被刷新。例如,默认情况下,cin和cerr都关联到cout。因此,读cin或写cerr都会导致cout的缓冲区被刷新。流中的tie方法可以用来设置流的关联性,具体见P283
4.文件IO相关的fstream相关的操作如下图,这里的fstream可以是ifstream、ofstream或fstream:
每个文件流都定义了一个名为open的成员函数,创建文件流对象时可以提供文件名(可选的),如果提供了文件名则open会被自动调用,如ifstream in(infile),如果定义了一个空文件流对象,可以随后调用open来将它与文件关联起来,如ofstream out; out.open(“filename”);,当一个fstream对象离开其作用域时,与之关联的文件会自动close。每个流都有一个关联的文件模式(如in表示以读方式打开,out表示以写方式打开,具体见P286),在每次打开文件时,都要设置文件模式,当未显示指定模式时,使用默认值。
5.内存IO相关的sstream相关的操作如下图,这里的sstream可以是istringstream、ostringstream或stringstream:
string流的具体使用方式见P288。
6.C++标准库中提供的顺序容器如下图:
选择所使用容器的原则见P293,通常使用vector是最好的选择,除非有很好的理由选择其他容器,array比内置数组更安全、更容易使用,在使用数组时尽量使用array。
7.容器均定义为模版类,顺序容器几乎可以保存任意类型的元素,可以定义一个容器,其元素的类型是另外一个容器。容器的通用操作如下图所示:
反向迭代器就是一种反向遍历容器的迭代器,与正向迭代器相比,各种操作的含义都发生了颠倒,如对反向迭代器执行++操作,会得到上一个元素。
8.迭代器有着公共的接口,具体见P296,forward_list迭代器不支持递减运算符(--)。一个迭代器范围由一对迭代器表示,两个迭代器分别指向同一个容器中的元素或者是尾元素之后的位置,这两个迭代器通常被称为begin和end,可以通过反复递增begin来到达end,换句话说end不在begin之前。
9.容器的定义和初始化方式如下图所示:
将一个新容器创建为另一个容器的拷贝的方法有两种:可以直接拷贝整个容器,或者(array除外)拷贝由一个迭代器对指定的元素范围。为了创建一个容器为另一个容器的拷贝,两个容器的类型及其元素类型必须匹配。不过当传递迭代器参数来拷贝一个范围时,就不要求容器类型是相同的了。而且新容器和原容器中的元素类型也可以不同,只要能将要拷贝的元素转换为要初始化的容器的元素类型即可(P300)。只有顺序容器的构造函数才接受大小参数,关联容器并不支持。当定义一个array时,除了指定元素类型还要指定容器大小,如array<int,42>。对于内置数组类型是不能进行拷贝或对象赋值操作的,但是可以对array进行拷贝或对象赋值操作(P301)。
10.容器的赋值运算和swap操作如下图(注意区别初始化与赋值):
经过c1=c2赋值运算之后,左边容器将与右边容器相等,如果两个容器原来大小不同,赋值后两者的大小都与右边容器的原大小相同。赋值运算要求左边和右边的运算对象具有相同的类型,顺序容器还定义了一个名为assign的成员,允许从一个不同但相容的类型赋值,或者从容器的一个子序列赋值(P302)。除array外,swap交换两个容器内容的操作保证会很快,因为元素本身并未交换,swap只是交换了两个容器的内部数据结构(如指针)。这意味着,除string外,指向容器的迭代器、引用和指针在swap操作之后都不会失效,它们仍指向swap操作之前所指向的那些元素。但是,在swap之后,这些元素已经属于不同的容器了。例如,假定iter在swap之前指向svec1[3]的string,那么在swap之后它指向svec2[3]的元素。与其他容器不同,对一个string调用swap会导致迭代器、引用和指针失效。与其他容器不同,swap两个array会真正交换它们的元素,因此交换两个array所需的时间与array中元素的数目成正比。对于array,在swap操作之后,指针、引用和迭代器所绑定的元素保持不变,但元素值已经与另一个array中对应元素的值进行了交换。
11.每个容器类型都支持相等运算符(==和!=),除了无序关联容器外的所有容器都支持关系运算符(>、>=、<=)。关系运算符左右两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素。只有当其元素类型也定义了相应的比较运算符时,才可以使用关系运算符来比较两个容器(P304)。
12.可用以下方式向顺序容器中添加元素:
需要注意的是,不同容器使用不同的策略来分配元素空间,而这些策略直接影响性能。在一个vector或string的尾部之外的任何位置,或是一个deque的首尾之外的任何位置添加元素,都需要移动元素。而且,向一个vector或string添加元素可能引起整个对象存储空间的重新分配。重新分配一个对象的存储空间需要分配新的内存,并将元素从旧的空间移动到新的空间中。在用一个对象来初始化容器时,或将一个对象插入到容器中时,实际上放入到容器中的是对象值的一个拷贝,而不是对象本身。容器中的元素与提供值的对象之间没有任何关联,随后对容器中元素的任何改变都不会影响到原始对象,反之亦然。list、vector、deque、string支持push_back;list、forward_list、deque支持push_front;vector、deque、list、string都支持insert(P307)。emplace函数在容器中直接构造元素(而不是拷贝元素),传递给emplace函数的参数必须与元素类型的构造函数相匹配。
13.顺序容器中访问元素的操作如下:
注意上述函数或下标返回的都是引用。删除元素的操作会改变容器的大小所以不适用于array,非array容器的删除元素的方式如下图:
删除元素的成员函数并不检查其参数。在删除元素之前,程序员必须确保它(们)是存在的。可用下图所示的方法改变容器大小:
14.forward_list是单向链表,在单向链表中没有一个简单方法来获取一个元素的前驱,所以forward_list中的函数与前述的那些容器中的函数有所不同,具体如下图所示(P313):
15.容器操作,如添加元素或删除元素可能使迭代器失效,具体情况见P315。由于向迭代器添加元素和从迭代器删除元素的代码可能会使迭代器失效,因此必须保证每次改变容器的操作之后都正确地重新定位选代器。
16.为了支持快速随机访问,vector将元素连续存储,每个元素紧挨着前一个元素存储。vector和string类型提供了一些成员函数,允许与它的实现中内存分配部分互动。capacity操作显示容器在不扩张内存空间的情况下可以容纳多少个元素,reserve操作允许通知容器它应该准备保存多少个元素。如下图:
只有当需要的内存空间超过当前容量时,reserve调用才会改变vector的容量,如果需求大小大于当前容量,reserve至少分配与需求一样大的内存空间(可能更大,依赖具体实现),如果需求大小小于或等于当前容量,reserve什么也不做,所以调用reserve 永远也不会减少容器占用的内存空间。当需求大小小于当前容量时,容器不会退回内存空间。因此在调用reserve之后,capacity将会大于或等于传递给reserve的参数。类似的,resize成员函数只改变容器中元素的数目而不是容器的容量。可以调用shrink_to_fit来要求deque、vector或string退回不需要的内存空间,此函数指出不再需要任何多余的内存空间,但是具体的实现可以选择忽略此请求。容器的size是指它已经保存的元素的数目,而capacity则是在不分配新的内存空间的前提下它最多可以保存多少元素(P319)。
17.除了支持与其他顺序容器一样的构造函数,string还支持下图所示的三种构造函数:
substr操作返回一个string,它是原始string的一部分或全部的拷贝,如下图:
改变string的其他函数如下图所示,具体参考P324:
与string相关的搜索函数如下图所示:
string中的compare函数:
string中与数值转换相关的函数如下:
18.容器适配器:除了顺序容器外,标准库还定义了三个顺序容器适配器:stack、queue 和priority_queue。本质上,一个适配器是一种机制,能使某种事物的行为看起来像另外一种事物一样。一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。默认情况下,stack和queue是基于deque实现的,priority_queue是在vector之上实现的,可以在创建一个适配器时将一个命名的顺序容器作为第二个类型参数来重载默认容器类型,如stack<string,vector<string>>str_stk;表示在vector上实现栈。所有适配器都要求容器具有添加和删除元素的能力,因此适配器不能构造在array之上。类似也不能用forward_list 来构造适配器,因为所有适配器都要求容器具有添加、删除以及访问尾元素的能力。stack支持的栈操作如下图:
queue和priority_queue支持的操作如下图(P331):
19.内存空间重新分配发生在容器添加或删除元素时,特别是在动态数组类容器(如std::vector)中,当现有内存空间不足时,容器会申请更多的内存空间。在添加元素时,容器可能会为避免频繁的分配而增加内存容量,并将现有元素迁移到新的内存区域。在删除元素时,容器可能会释放不再需要的内存,但这不总是自动发生(如std::vector需要手动调用shrink_to_fit())。
20.泛型算法:它们实现了一些经典算法的公共接口,如排序和搜索,可用于不同类型的元素和多种容器类型。泛型算法本身不会执行容器的操作,只会运行在迭代器之上,执行迭代器的操作,算法永远不会改变底层容器的大小,算法可能改变容器中保存的元素的值,也可能移动容器内的元素,但永远不会直接添加或删除元素(P337)。常见的泛型算法如:find(beg,end,val)返回一个迭代器(beg和end是表示元素范围的迭代器),指向输入序列中的第一个值为val的元素;accumulate(beg,end,init)返回输入序列中所有值的和,和的初值由init指定,返回类型与init类型相同,使用+运算计算和;equal(beg1,end1,beg2)比较两个序列是否相等,如果两个序列对应值相等返回true,beg1、end1表示第一个序列的元素范围,beg2表示第二个序列的首元素,像这种用一个单一迭代器表示第二个序列的算法都假定第二个序列至少与第一个一样长;fill(beg,end,val)将值val赋予给定序列中的每一个元素;copy(beg,end,dest)从输入范围将元素拷贝到dest指定的目的序列,返回的是其目的位置迭代器(递增后)的值;repalce(beg,end,old_val,new_val)将给定序列中的old_val替换成new_val;replace_copy(beg,end,dest,old_val,new_val)将给定序列拷贝到dest,并将序列中的old_val替换成new_val;sort(beg,end)将给定序列排序,利用元素类型的<运算符排序;unique(beg,end)对给定序列排序,将相邻的重复序列“消除”,返回一个指向不重复元素尾后位置的迭代器。其他更多泛型算法见P770附录A.2。
21.一种保证算法有足够元素空间来容纳输出数据的方法是使用插入迭代器back_inserter,back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。当通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中(P341)。
22.很多算法都会比较输入序列中的元素,默认情况下,这类算法使用元素类型的<或==运算符完成比较。标准库还为这些算法定义了额外的版本,允许我们提供自己定义的操作来代替默认运算符。例如,sort算法默认使用元素类型的<运算符,但可能我们希望的排序顺序与<所定义的顺序不同,或是我们的序列可能保存的是未定义<运算符的元素类型。这两种情况下都需要重载sort的默认行为。sort的第二个版本sort(beg,end,comp)是重载过的,它接受第三个参数,此参数是一个谓词(谓词是一个可调用的表达式,其返回结果是一个能用作条件的值,有些算法只接受一元谓词,有些算法只接受二元谓词)。如sort(words.begin(),words.end(),isShorter)表示按isShorter函数定义的规则(按照单词长度排序)给words排序,isShorter是一个二元谓词。在将words按大小重排的同时,还希望具有相同长度的元素按字典序排列。为了保持相同长度的单词按字典序排列,可以使用stable_sort算法,这种稳定排序算法维持相等元素的原有顺序(P345)。
23.lambda表达式:一个lambda表达式表示一个可调用的代码单元,可以将其理解为一个未命名的内联函数。与任何函数类似,一个lambda具有一个返回类型、一个参数列表和一个函数体。但与函数不同,lambda可能定义在函数内部。一个lambda表达式具有如下形式:[capture list](parameter list)->return type {function body},其中capture list(捕获列表)是一个lambda所在函数中定义的局部变量的列表,return type、parameter list和function body与任何普通函数一样,分别表示返回类型参数列表和函数体,但与普通函数不同,lambda必须使用尾置返回来指定返回类型,lambda的调用方式与普通函数的调用方式相同。在lambda中忽略括号和参数列表等价于指定一个空参数列表,如果忽略返回类型,lambda根据函数体中的代码推断出返回类型,如果函数体只有一个return语句,则返回类型从返回的表达式的类型推断而来,否则返回类型为void(如果lambda的函数体包含任何单一return语句之外的内容,且未指定返回类型,则返回void),若返回类型被推断为void则不能返回任何值。lambda不能有默认参数,因此lambda调用的实参数目永远与形参数目相等。一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量,但一个lambda可以直接使用定义在当前函数之外的名字。当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型,当使用auto定义一个用lambda初始化的变量时,定义了一个从lambda生成的类型的对象。默认情况下从lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员,类似任何普通类的数据成员,lambda的数据成员也在lambda对象创建时被初始化。lambda可以采取值捕获或者引用捕获,当以引用捕获方式捕获一个变量时必须保证lambda执行时变量是存在的,具体如下图(P352):
对于值捕获,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝,所以创建之后的函数中对所捕获的值的修改不会影响到lambda内对应的值(P350)。默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。如果希望改变这个被捕获的变量的值,就必须在参数列表首加上关键字mutable。一个引用捕获的变量是否可以被修改依赖于此引用指向的是一个const类型还是一个非const类型。
24.for_each(beg,end,unaryOp)对输入序列中的每个元素应用可调用对象unaryOp,unaryOp的返回值(如果有的话)被忽略(P348)。ref函数返回一个对象,包含给定的引用,例如ref(cin)返回对cin的引用,cref函数与ref函数类似,生成一个保存const引用的类(P357)。
25.bind函数:可以将bind函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。调用bind的一般形式为:auto newCallable=bind(callable,arg_list);,其中newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数,当调用newCallable时,newCallable会调用callable,并传递给它arg_list中的参数。arg_list中的参数可能包含形如_n的名字,其中n是一个整数。这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,依此类推。名字_n都定义在一个名为placeholders的命名空间中,而这个命名空间本身定义在std命名空间中。为了使用这些名字,两个命名空间都要写上。可以使用using namespace name;的形式(using namespace std::placeholders;)来说明希望所有来自namespace_name的名字都可以在的程序中直接使用(P355)。
26.标准库在头文件iterator还定义了额外几种迭代器:
- 插入迭代器:这些迭代器被绑定到一个容器上,可以用来向容器插入元素。插入器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。插入迭代器操作如下图:
back_inserter(c)创建一个使用push_back的迭代器;front_inserter(c)创建一个使用push_front的迭代器;inserter(c,p)创建一个使用insert的迭代器,此函数接受第二个参数,这个参数必须是一个指向给定容器的迭代器,元素将被插入到给定选代器所表示的元素之前(c为目标容器,p是迭代器指向容器c中某个元素)(P358)。 - 流迭代器:这些迭代器被绑定到输入或输出流上,可用来遍历所关联的IO流。istream_iterator读取输入流,ostream_iterator向一个输出流写入数据。对于istream_iterator,默认初始化迭代器就创建了一个可以当作尾后值使用的迭代器,对于一个绑定到流的迭代器,值就与尾后迭代器相等。一旦其关联的流遇到文件尾或遇到IO错误,迭代器的值就与尾后迭代器相等。可以为任何定义了输入运算符(>>运算符)的类型创建istream_iterator对象,可以为任何定义了输出运算符(<<运算符)的类型定义ostream_iterator对象,当创建一个ostream_iterator时,可以提供(可选的)第二参数,它是一个字符串,在输出每个元素后都会打印此字符串,不允许任何空的或表示尾后位置的ostream_iterator。istream_iterator的操作如下图:
在将一个istream_iterator绑定到一个流时,标准库并不保证迭代器立即从流读取数据。具体实现可以推迟从流中读取数据,直到使用迭代器时才真正读取。标准库中的实现所保证的是,在第一次解引用迭代器之前,从流中读取数据的操作已经完成了。ostream_iterator的操作如下图:
- 反向迭代器:这些迭代器向后而不是向前移动,除了forward_list之外的标准库容器都有反向迭代器。可以通过调用rbegin、rend、crbegin和crend成员函数来获得反向迭代器。这些成员函数返回指向容器尾元素和首元素之前一个位置的迭代器。与普通迭代器一样,反向迭代器也有const和非const版本。只能从既支持++也支持--的迭代器来定义反向迭代器,毕竟反向迭代器的目的是在序列中反向移动,流迭代器不支持递减运算,因为不可能在一个流中反向移动(P363)。反向迭代器的base方法可以将反向迭代器转换成普通迭代器(P364)。
- 移动迭代器:这些专用的迭代器不是拷贝其中的元素,而是移动它们。
27.任何泛型算法的最基本的特性是它要求其迭代器提供哪些操作,算法所要求的迭代器操作可以分为5个迭代器类别,如下图:
在泛型算法中,对每个迭代器参数来说,其能力必须与规定的最小类别至少相当,向算法传递一个能力更差的迭代器会产生错误。如:算法reverse要求双向迭代器,算法sort要求随机访问迭代器(P366)。在任何其他算法分类之上,泛型算法还有一组参数规范,大多数算法具有以下四种形式之一:
其中alg是算法的名字,beg和end表示算法所操作的输入范围。dest、beg2和end2,都是迭代器参数,分别表示指定的目的位置和第二个范围,除了这些迭代器参数,一些算法还接受额外的、非迭代器的特定参数。向输出迭代器写入数据的算法都假定目标空间足够容纳写入的数据。除了参数规范,算法还遵循一套命名和重载规范,例如算法名以_if、_copy结尾的算法都有特定含义,具体见P369。与其他容器不同,链表类型list和forward_list定义了几个成员函数形式的算法,对于list和forward_list应该优先使用成员函数版本的算法而不是通用版本的算法,因为前者性能更好。如下图:
链表类型还定义了splice算法,如下图:
28.关联容器:关联容器中的元素是按照关键字来保存和访问的。C++标准库共提供了八个关联容器。如下图:
无序容器使用哈希函数来组织元素(P374)。
29.关联容器不支持顺序容器的位置相关的操作,例如push_front或push_back,原因是关联容器中元素是根据关键字存储的,这些操作对关联容器没有意义。当定义一个map时,必须既指明关键字类型又指明值类型;而定义一个set时,只需指明关键字类型,因为set中没有值。每个关联容器都定义了一个默认构造函数,它创建一个指定类型的空容器。也可以将关联容器初始化为另一个同类型容器的拷贝,或是从一个值范围来初始化关联容器,只要这些值可以转化为容器所需类型就可以。也可以对关联容器进行列表初始化(P377)。一个map或set中的关键字必须是唯一的,即,对于一个给定的关键字,只能有一个元素的关键字等于它。容器multimap和multiset没有此限制,它们都允许多个元素具有相同的关键字。
30.对于有序容器:map、multimap、set以及multiset,关键字类型必须定义元素比较的方法。默认情况下,标准库使用关键字类型的<运算符来比较两个关键字。在集合类型中,关键字类型就是元素类型;在映射类型中,关键字类型是元素的第一部分的类型。可以提供自己定义的操作来代替关键字上的<运算符,所提供的操作必须在关键字类型上定义一个严格弱序,可以将严格弱序看作“小于等于”,虽然实际定义的操作可能是一个复杂的函数,无论怎样定义比较函数,它必须具备如下基本性质:
为了指定使用自定义的操作,必须在定义关联容器类型时提供此操作的类型。如前所述,用尖括号指出要定义哪种类型的容器,自定义的操作类型必须在尖括号中紧跟着元素类型给出,具体例子见P379。
31.pair是一个标准库类型,一个pair保存两个数据成员,pair的默认构造函数对数据成员进行值初始化,例如:pair<string,string> anon,则anon是一个包含两个空string的pair。与其他标准库类型不同,pair的数据成员是public的,两个成员分别命名为first和second。pair上的操作如下图(P380):
32.关联容器支持容器共同的类型和操作(见P295),即第7条中的图9.2列出的内容,除此之外关联容器还定义了下图所示的类型:
对于set类型,key_type和value_type是一样的,set中保存的值就是关键字。在一个map中,元素是关键字-值对。即每个元素是一个pair对象,包含一个关键字和一个关联的值。可以使用作用域运算符来提取一个类型的成员,例如map<string,int>::key_type,只有map类型(unordered_map、unordered_multimap、multimap和map)才定义了mapped_type(P382)。当解引用一个关联容器迭代器时,会得到一个类型为容器的value_type的值的引用。对map而言,value_type是一个pair类型,其first成员保存const的关键字,second成员保存值,一个map的value_type是一个pair,可以改变pair的值,但不能改变关键字成员的值。虽然set类型同时定义了iterator和const_iterator类型,但两种类型都只允许只读访问set中的元素。与不能改变一个map元素的关键字一样,一个set中的关键字也是const的。可以用一个set迭代器来读取元素的值,但不能修改。可以使用begin、end迭代器遍历map和set,当使用一个迭代器遍历一个map、multimap、set或multiset时,选代器按关键字升序遍历元素。通常不对关联容器使用泛型算法,关键字是const这一特性意味着不能将关联容器传递给修改或重排容器元素的算法,因为这类算法需要向元素写入值,而set类型中的元素是const的,map中的元素是pair,其第一个成员是const的。关联容器可用于只读取元素的算法(P383)。
33.关联容器的insert成员用于向容器中添加元素,如下图:
关联容器的删除操作如下图:
map和unordered_map容器提供了下标运算符和一个对应的at函数,如下图:
set类型容器、multimap和unordered_multimap都不支持下标操作。map下标运算符接受一个索引(即一个关键字),获取与此关键字相关联的值。但是与其他下标运算符不同的是,如果关键字并不在map中,会为它创建一个元素并插入到map中,关联值将进行值初始化,由于下标运算符可能插入一个新元素,所以只可以对非const的map使用下标操作。通常情况下,解引用一个迭代器所返回的类型与下标运算符返回的类型是一样的,但对map则不然,当对一个map进行下标操作时,会获得一个mapped_type对象,但当解引用一个map迭代器时,会得到一个value_type对象(P388)。如果一个multimap或multiset 中有多个元素具有给定关键字,则这些元素在容器中会相邻存储(因为它们是有序的)。在一个关联容器中的查找操作如下图:
如果没有元素与给定关键字匹配,则lower_bound和upper_bound 会返回相等的迭代器:都指向给定关键字的插入点,能保持容器中元素顺序的插入位置(P391)。
34.四个无序关联容器不是使用比较运算符来组织元素,而是使用一个哈希函数和关键字类型的==运算符。无序容器提供了一组管理桶的函数:
无序容器在存储上组织为一组桶,每个桶保存零个或多个元素,无序容器使用一个哈希函数将元素映射到桶。为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶,容器将具有一个特定哈希值的所有元素都保存在相同的桶中。如果容器允许重复关键字,所有具有相同关键字的元素也都会在同一个桶中。因此,无序容器的性能依赖于哈希函数的质量和桶的数量和大小。对于相同的参数,哈希函数必须总是产生相同的结果。理想情况下,哈希函数还能将每个特定的值映射到唯一的桶,但将不同关键字的元素映射到相同的桶也是允许的。默认情况下,无序容器使用关键字类型的==运算符来比较元素,还使一个hash<key_type>类型的对象来生成每个元素的哈希值。标准库为内置类型(包括指针)提供了hash模板。还为一些标准库类型,包括string 和智能指针类型定义了hash。因此可以直接定义关键字是内置类型(包括指针类型)、string还有智能指针类型的无序容器。但是不能直接定义关键字类型为自定义类类型的无序容器,与容器不同,不能直接使用哈希模板,而必须提供自己的hash模板版本,参考P396。无论在有序容器中还是在无序容器中,具有相同关键字的元素都是相邻存储的。
35.动态内存的使用很容易出问题,因为确保在正确的时间释放内存是极其困难的。有时会忘记释放内存,在这种情况下就会产生内存泄漏;有时在尚有指针引用内存的情况下就释放了它,在这种情况下就会产生引用非法内存的指针。为了更容易(同时也更安全)地使用动态内存,新的标准库提供了两种智能指针类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准库提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr 允许多个指针指向同一个对象;unique_ptr 则“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中(P400)。
36.类似 vector,智能指针也是模板,因此当创建一个智能指针时,必须提供额外的信息指明指针可以指向的类型。如:shared_ptr<string> p1定义了一个可以指向string的shared_ptr指针p1。智能指针的使用方式与普通指针类似,解引用一个智能指针返回它指向的对象,如果在一个条件判断中使用智能指针,效果就是检测它是否为空。shared_ptr和unique_ptr支持的操作如下图所示:
类似顺序容器的emplace成员,make_shared 用其参数来构造给定类型的对象。例如,调用make_shared <string>时传递的参数必须与string的某个构造函数相匹配,调用make_shared <int>时传递的参数必须能用来初始化一个int,依此类推。如果不传递任何参数,对象就会进行值初始化。
37.可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数。在内存中shared_ptr包含了一个指向对象的指针和一个指向控制块的指针,每一个由shared_ptr管理的对象都有一个控制块,该控制块包含引用计数等信息,无论何时拷贝一个shared_ptr,计数器都会递增。例如,当用一个shared_ptr初始化另一个shared_ptr或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增。当给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减。一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象。类似于构造函数,每个类都有一个析构函数。就像构造函数控制初始化一样,析构函数控制此类型的对象销毁时做什么操作,shared_ptr就是通过析构函数来完成销毁工作的(P402)。对于一块内存,shared_ptr类保证只要有任何shared_ptr对象引用它,它就不会被释放掉。
38.程序使用动态内存出于以下三种原因之一:程序不知道自己需要使用多少对象;程序不知道所需对象的准确类型;程序需要在多个对象间共享数据。
39.C++语言定义了两个运算符来分配和释放动态内存。运算符new分配内存,delete释放new分配的内存。相对于智能指针,使用这两个运算符管理内存非常容易出错。在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针:int *pi=new int;则pi指向一个动态分配的、未初始化的无名对象。默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化。也可以对动态分配的对象进行值初始化,只需在类型名之后跟一对空括号即可:string *ps=new string();,此时string会被值初始化为空。如果提供了一个括号包围的初始化器,就可以使用auto从此初始化器来推断我们想要分配的对象的类型。但是,由于编译器要用初始化器的类型来推断要分配的类型,只有当括号中仅有单一初始化器时才可以使用auto:auto pl=new auto(obj);是正确而auto p2=new auto{a,b,c};是错误的。类似其他任何const对象,一个动态分配的const对象必须进行初始化(P408)。默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常,也可以改变new的方式来阻止它抛出异常,如:int *p2 =new(nothrow) int; 传递给new一个由标准库定义的名为nothrow的对象,以此来告诉它不能抛出异常,如果这种形式的new不能分配所需内存,它会返回一个空指针。可以通过delete p的形式来将动态分配的内存还给系统,传递给delete的p指针必须是n动态分配的内存或者是一个空指针。释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的(P409)。由内置指针(而不是智能指针)管理的动态内存在被显式释放前一直都会存在。当delete 一个指针后,指针值就变为无效了,虽然指针值已经无效,但是在很多机器上指针仍然保存着(已经释放了的)动态内存的地址,所以在delete指针后,最好将其置为nullptr。但可能还存在指向这个(已经释放了的)动态内存的地址的其他指针,在delete之后,这些指针就变成了空悬指针,即指向一块曾经保存数据对象但现在已经无效的内存的指针。
40.可以用new返回的指针来初始化智能指针,接受指针参数的智能指针构造函数是explicit的(被explicit修饰的构造函数只能用于直接初始化(使用()初始化,如Class foo(parameter)),而不能用于拷贝初始化(用=初始化,如Class foo=parameter))。因此不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:shared_ptr<int> p1=new int(1024)是错误的,必须使用直接初始化形式shared_ptr <int> p2(new int(1024));。默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用 delete释放它所关联的对象。但也可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来替代delete。如下图:
与赋值类似,reset会更新引用计数,如果需要的话,会释放p指向的对象。shared_ptr可以协调对象的析构,但这仅限于其自身的拷贝(也是shared_ptr)之间。所以推荐使用make_shared 而不是new,这样就能在分配对象的同时就将shared_ptr与之绑定,从而避免了无意中将同一块内存绑定到多个独立创建的shared_ptr上(注意这里的独立创建的含义,说明他们在内存中指向不同的控制块,拥有不同的计数器,但实际却指向同一个对象)。如果多个独立的shared_ptr指向同一块内存,则这几个shared_ptr是独立计数的。当将一个 shared_ptr绑定到一个普通指针时,就将内存的管理责任交给了这个shared_ptr,一旦这样做了,就不应该再使用内置指针来访问shared_ptr所指向的内存了。也不要使用 get初始化另一个智能指针或为智能指针赋值:智能指针类型定义了一个名为get的函数,它返回一个内置指针,指向智能指针管理的对象。此函数是为了这样一种情况而设计的:当需要向不能使用智能指针的代码传递一个内置指针时。get用来将指针的访问权限传递给代码,只有在确定代码不会delete指针的情况下,才能使用get。特别是,永远不要用get初始化另一个智能指针或者为另一个智能指针赋值(P414)。
41.可以将智能指针用于异常处理,来确保资源的正确释放,具体参考P415。智能指针可以提供对动态分配的内存安全而又方便的管理,但这建立在正确使用的前提下。为了正确使用智能指针,必须坚持一些基本规范:
- 不使用相同的内置指针值初始化(或reset)多个智能指针。
- 不delete get()返回的指针。
- 不使用get()初始化或reset另一个智能指针。
- 如果使用get()返回的指针,当最后一个对应的智能指针销毁后,指针就变为无效了。
- 如果使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器(即自定义的delete操作)。
42.一个unique_ptr“拥有”它所指向的对象。与shared_ptr不同,某个时刻只能有一个 unique_ptr指向一个给定对象。当unique_ptr 被销毁时,它所指向的对象也被销毁。由于一个 unique_ptr拥有它指向的对象,因此 unique_ptr不支持普通的拷贝或赋值操作。unique_ptr的相关操作如下图:
虽然不能拷贝或赋值 unique_ptr,但可以通过调用release(并没有进行资源的释放)或reset 将指针的所有权从一个(非const)unique_ptr转移给另一个unique_ptr,具体例子见P418。调用release 会切断unique_ptr和它原来管理的对象间的联系。release 返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。但是如果不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放。不能拷贝unique_ptr的规则有一个例外,可以拷贝或赋值一个将要被销毁的unique_ptr,最常见的例子是从函数返回一个unique_ptr。类似 shared_ptr,unique_ptr默认情况下用 delete 释放它指向的对象,与shared_ptr一样,可以重载一个unique_ptr中默认的删除器(即delete操作),但是unique_ptr管理删除器的方式与shared_ptr不同。重载一个 unique_ptr中的删除器会影响到unique_ptr类型以及如何构造(或reset)该类型的对象,必须在尖括号中unique_ptr指向类型之后提供删除器类型。在创建或reset 一个这种 unique_ptr类型的对象时,必须提供一个指定类型的可调用对象(删除器):unique_ptr<objT,delT> p(newobjT,fcn);(P419)。
43.weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr 不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放。weak_ptr支持的操作如下图:
由于对象可能不存在,不能使用weak_ptr直接访问对象,而必须调用1ock。此函数检查weak_ptr指向的对象是否仍存在。如果存在,lock返回一个指向共享对象的 shared_ptr。与任何其他shared_ptr类似,只要此shared_ptr存在,它所指向的底层对象也就会一直存在(P420)。
44.大多数应用应该使用标准库容器而不是动态分配的数组。使用容器更为简单、更不容易出现内存管理错误并且可能有更好的性能。使用容器的类可以使用默认版本的拷贝、赋值和析构操作,分配动态数组的类则必须定义自己版本的操作,在拷贝、复制以及销毁对象时管理所关联的内存。为了让new分配一个对象数组,需要在类型名之后跟一对方括号,在其中指明要分配的对象的数目,方括号中的大小必须是整型,但不必是常量。例如:int *pia=new int[get size()];//pia指向第一个int。也可以用一个表示数组类型的类型别名来分配一个数组,这样new表达式中就不需要方括号了:typedef int art[42]; int *p=new art;(P423)。当用new分配一个数组时,并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin或end。出于相同的原因,也不能用范围for语句来处理(所谓的)动态数组中的元素。默认情况下,new分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。可以对数组中的元素进行值初始化,方法是在大小之后跟一对空括号,int *pia2 =new int[10]();。不能使用auto分配动态数组。分配一个大小为0的动态数组是合法的,当用new分配一个大小为0的数组时,new返回一个合法的非空指针。此指针保证与new返回的其他任何指针都不相同。对于零长度的数组来说,此指针就像尾后指针一样,可以像使用尾后迭代器一样使用这个指针。可以用此指针进行比较操作,可以向此指针加上(或从此指针减去)0,也可以从此指针减去自身从而得到0。但此指针不能解引用,毕竟它不指向任何元素。可以使用delete [] p的形式释放动态数组,数组中的元素按逆序销毁,即最后一个元素首先被销毁,然后是倒数第二个,依此类推。当释放一个指向数组的指针时,空方括号对是必需的,它指示编译器此指针指向一个对象数组的第一个元素。如果在delete一个指向数组的指针时忽略了方括号(或者在 delete 一个指向单一对象的指针时使用了方括号),其行为是未定义的(P425)。标准库提供了一个可以管理new分配的数组的unique_ptr版本。为了用一个unique_ptr管理动态数组,必须在对象类型后面跟一对空方括号:unique_ptr<int []> up(new int[10]);。当一个unique_ptr指向一个数组时,不能使用点和箭头成员运算符。毕竟unique_ptr指向的是一个数组而不是单个对象,因此这些运算符是无意义的。另一方面,当一个unique_ptr指向一个数组时,我们可以使用下标运算符来访问数组中的元素:
与unique_ptr不同,shared_ptr不直接支持管理动态数组。如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器(P426)。shared_ptr未定义下标运算符,而且智能指针类型不支持指针算术运算,因此为了访问数组中的元素,必须用get获取一个内置指针,然后用它来访问数组元素。
45.new有一些灵活性上的局限,其中一方面表现在它将内存分配和对象构造组合在了一起。类似的,delete将对象析构和内存释放组合在了一起。标准库 allocator 类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的(P427)。allocator是一个模版,它支持的操作如下图:
allocator分配的内存是未构造的,可以按需要在此内存中构造对象,可以多次调用 a.allocate 来获取不同的内存块,这些内存块通常是不连续的。在新标准库中,construct成员函数接受一个指针和零个或多个额外参数,在给定位置构造一个元素,额外参数用来初始化构造的对象,这些额外参数必须是与构造的对象的类型相匹配的合法的初始化器。为了使用allocate返回的内存,必须用construct构造对象,使用未构造的内存,其行为是未定义的。当用完对象后,必须对每个构造的元素调用destroy来销毁它们,函数destroy接受一个指针,对指向的对象执行析构函数。一旦元素被销毁后,就可以重新使用这部分内存来保存其他string,也可以将其归还给系统,释放内存通过调用deallocate来完成(P429)。标准库还为 allocator类定义了几个伴随算法,可以在未初始化内存中创建对象。如下图:
allocator类的使用方法可以参考P468的例子。