《C++ Primer》第II部分:C++标准库
前言
把《C++ Primer》读薄系列笔记。本篇为第II部分C++标准库,包含全书第8~12章重难点:
- IO库
- 顺序容器
- 范型算法
- 关联容器
- 动态内存
修订版课后题解见GitHub仓库cpp-primer-workbook。
IO库
- IO类继承机制:ifstream和istringstream继承自istream,ofstream和ostringstream都继承自ostream
- 宽字符IO类:在函数和类型前加前缀w,如wcin、wistream
- IO对象无拷贝赋值:IO操作的函数通常以引用方式传递和返回流;由于读写会改变状态,IO对象的引用不能是const
- 条件状态:iostate表示流状态的类型,其包含4种constexpr值,badbit(流崩溃)、failbit(可恢复错误)、goodbit、eofbit;对应4个函数bad()、fail()、good()、eof()
- 管理条件状态:rdstate()获取状态,clear()清除所有错误标志位,clear(flags)和setstate(flags)将状态置为flags
- 刷新输出缓冲区:可使用操纵符endl(换行)、flush、ends(空字符);开启unitbuf每次调用flush,nounitbuf解除
- 关联流:交互式系统通常关联输入和输出流,使所有输出在读操作前被打印;每个流同时最多关联到一个流,但多个流可以关联到同一个ostream;将其关联到空指针可彻底解开关联
- fstream特有:打开文件绑定流的open()、关闭绑定文件的close()、文件是否成功打开且尚未关闭的is_open()
- stringstream特有:将s拷贝到stringstream对象的str(s)、返回保存的string的拷贝的str()
顺序容器
- 选择容器:标准库容器性能通常优于同类数据结构;C++标准新增array数组和forward_list单向链表;很多小元素且额外空间开销大,不用list或forward_list
- 容器的额外操作:iterator表示迭代器类型,size_type无符号整型,value_type指元素类型,reference与value_type&等价
- size操作:返回容器内元素数量;forward_list没有size操作,其他容器的size操作是常量时间
- 反向容器:reverse_iterator按逆序寻址的迭代器,rbegin()是尾迭代器,crend()表示const首前迭代器,
- forward_list:不支持反向容器迭代器,且其迭代器不支持递减运算
- 容器初始化:必须是相同的容器类型,且保存相同类型的元素
- 用大小初始化:只有顺序容器的构造函数才接受大小参数,关联容器不支持
- array:具有固定大小,列表初始化的数目必须等于或小于array的大小;花括号列表只能初始化不能赋值
- assign:仅顺序容器支持,传递给assign的迭代器不能指向调用assign的容器;会导致指向容器的迭代器、引用、指针失效
- swap:除array外,swap不对任何元素进行拷贝、删除、插入,可保证常数时间完成;除string外,指向其他容器的迭代器、引用、指针在swap后都不会失效
- 关系运算符:两边的运算对象必须是相同的容器类型,且保存相同类型的元素
- 插入元素:返回新添加的第一个元素的迭代器;向vector、string、deque插入元素,会使指向的迭代器、引用、指针失效;插入迭代器表示的范围内的一段元素时,不能指向与目的位置相同的容器
- 元素是拷贝:用一个对象来初始化或插入容器时,实际上放入的是拷贝,容器中元素与提供值的对象无任何关联
- 访问元素:at和下标操作只支持string、vector、deque、array;访问成员函数返回的是引用
- 删除元素:clear和pop返回void,erase返回指向最后一个被删元素之后元素的迭代器
- 特殊的forward_list操作:添加或删除通过改变给定元素之后的元素来完成;定义了首前迭代器before_begin();insert_after()返回指向最后一个插入元素的迭代器;erase_after()返回指向被删元素之后的元素的迭代器
- 改变容器大小:resize()可用来增大或缩小容器,缩小容器则多余元素被删除,增大则填充指定值或默认初始值对象
-
迭代器失效:添加或删除元素可能使指向容器元素的指针、引用、迭代器失效
添加元素 删除元素 vector、string 存储空间未重新分配,则插入位置之后失效;否则全部失效 被删元素及之后失效 deque 在首尾添加只有迭代器失效;否则全部失效 删除首元素只首元素失效;删除尾元素则尾后迭代器也失效;删除首尾之间元素则全部失效 list、forward_list 不影响 只有被删元素受影响 - 不保存尾后迭代器:在循环中插入删除deque、string、vector中元素,应每次重新调用end()
- 管理容器大小:capacity()返回容器保留内存的大小,空容器返回0;reverse()只能增加容器保留内存大小;减少所占空间可用shrink_to_fit()申请将空间值缩小到元素数量,但不保证一定退回内存空间
- 重新分配内存:只有执行insert时size和capacity相等,或resize、reserve时给定大小超过capacity,vector才会分配新的内存空间,分配量取决于底层实现
- 修改string:replace和insert所允许的参数形式args依赖于范围range和位置pos是如何指定的
- string::npos:string搜索失败返回的static成员,类型是const string::size_type,被初始化为-1
- string搜索:find()第一次出现,rfind()最后一次出现,find_first_of()参数中任何一个字符第一次出现,find_last_not_of()非参数中的字符最后一次出现;找到返回下标,没找到返回npos
- string数值转换:
s=to_string(val); val = stoi(s, p, b);
val可以是任何算术类型,对应stoi函数替换为stol、stod等,p表示字符串开始转换的下标,b表示转换所用基数 - 容器适配器:容器适配器接受一个已有的容器类型,使其行为看起来像一种不同类型
- 底层容器:stack和queue默认基于deque,priority_queue默认基于vector;不能使用不能添加删除元素的array,或不能访问尾元素的forward_list作为底层容器;stack可用除array和forward_list外的任何容器构造,queue可使用list或deque,priority_queue可基于vector或deque
- 适配器操作:每个适配器都基于底层容器类型的操作定义了自己的特殊操作,只能使用适配器操作,不能使用底层容器操作
范型算法
- 范型算法:实现了一些经典算法的公共接口,可用于不同类型的元素、多种类型的容器、其他类型序列
- 迭代器与算法:算法工作于迭代器之上,迭代器令算法不依赖于容器,但依赖于元素类型的操作;算法永远不会执行容器操作,所以不会改变底层容器的大小
- 输入范围:标准库算法通常对一个范围内的元素进行操作,用两个迭代器参数表示左闭右开区间
- 算法分类:是否读取元素、改变元素、重排元素顺序
- 只读算法:只读取而不改变元素,最好用常量迭代器;若计划用算法返回的迭代器改变元素值,就要传递非常量参数
- 写算法:算法不检查写操作,都假定目的位置空间足够大;常见错误有在刚定义的空容器上进行fill_n或其他写操作;保证算法有足够元素空间来容纳输出可用插入迭代器
- 迭代器参数:一些算法读两个序列,不要求容器或元素类型严格匹配,只要求两个序列中的元素能够比较
- 拷贝算法:copy返回目的位置迭代器(递增后)的值;多个算法提供“拷贝”版本,接受一个额外的目的迭代器参数,将新序列拷贝到里面,而不改变原输入范围的序列
- 重排算法:去除容器重复元素的方法是,先调用重排算法sort和unique,再用容器操作erase
- 定制操作:算法默认使用<或==运算符完成,但允许我们传递任何类型的可调用对象,用自定义操作代替默认运算符
- 可调用对象:函数、函数指针、重载了函数调用符()的类、lambda表达式
- 谓词:返回可转换为bool型的函数,通常用于判定元素关系;根据接受参数个数分为一元谓词和二元谓词
- lambda表达式:[捕获列表] (参数列表) -> return 返回类型 {函数体};某些时候参数列表和返回类型可省略;不能有默认参数
- 捕获列表:只需捕获局部非静态变量,分为值捕获和引用捕获;值捕获的变量的值是lambda创建时的拷贝,若要改变需要在参数列表后加mutable关键字;引用捕获要保证lambda执行时变量仍存在,是否可变由是否是常量引用而定
- 隐式捕获:让编译器根据lambda体中的代码推断需要使用哪些变量,使用&或=参数表示引用捕获或值捕获;混合使用隐式和显式捕获时,两者捕获方式必须不同
- lambda返回:lambda体只含有一个return语句可省略返回类型;否则lambda默认将返回void,必须指定返回类型
- bind函数:auto newCallable = bind(callable, arg_list);传给newCallable的参数会按照arg_list中占位符参数的顺序传给callable
- 占位符:形如_n,n为整数;定义在std::placeholders的命名空间
- 绑定引用参数:bind的那些不是占位符的参数默认被拷贝到可调用对象中;希望传递给bind一个对象而不拷贝它,必须使用ref或cref;bind、placeholders、ref都定义在头文件functianal中
- 插入迭代器:back_inserter、inserter、front_inserter调用容器操作push_back、insert、push_front;*it、++it、it++都返回it
- 流迭代器:将对应流当作特定类型的元素序列处理;istream_iterator输入,ostream_iterator输出
- istream_iterator:空的istream_iterator可作为尾后迭代器;比较两个istream_iterator是否相等必须读取相同类型,如果都是尾后迭代器或绑定到相同的输入则相等
- ostream_iterator:it、++it、it++都返回it;推荐使用it,可与其他迭代器的使用保持一致,将来修改更容易
- 反向迭代器:rbegin指向尾元素、rend指向首前元素;递增操作移动到前一个元素;反向迭代器的base操作可以得到普通迭代器,但正反向迭代器互相转换时指向的是不同元素
- 算法要求的迭代器:输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器;C++标准指明了算法的每个迭代器参数的最小类别,但编译器通常不会报该类错
- 算法命名:接受谓词来替代默认运算符、或不接受额外参数的算法通常是重载函数;接受谓词来替代元素值的算法都有附加的_if,如find_if;拷贝到额外空间都有附加的_copy,如replace_copy;一些算法同时提供_copy和_if,如remove_copy_if
- 链表的算法:list和forward_list有自己的函数成员sort、merge、remove、reverse、unique,可改变底层容器,优先使用它们而不是通用算法
- splice成员:链表特有的算法,将一个链表的一截移动到另一个链表的指定位置,要保证移动的目的位置不在待移动范围内
关联容器
- 关联容器分类:set还是map、关键字是否重复、关键字是否有序
- 关联容器:可列表初始化;不支持位置相关操作,不支持参数为元素值和对应数量的构造或插入;迭代器是双向的
- 有序容器:关键字类型必须定义了比较方法,默认情况下使用<运算符,可自定义一个严格弱序
- 严格弱序:不能同时“小于等于”对方;“小于等于”具有传递性;双方都不“小于等于”对方称为“等价”,“等价”具有传递性
- pair类型:可用花括号包围的初始化器来返回pair类型的对象,老版本只允许显式构造
- 关联容器迭代器:map的value_type是pair<const key_type, mapped_type>,所以map迭代器只能改变关键字映射的值,不能修改关键字;set的value_type等于key_type,都是const关键字,不能修改
- 关联容器insert:只有当关键字不存在时才插入,函数返回一个pair,包含一个指向元素的迭代器,和一个指示是否插入成功的bool型
- 关联容器erase:若传入迭代器,必须保证指向真实元素;若传入关键字,则返回被删去的所有该关键字的元素的数量
- map的下标操作:map下标操作返回mapped_type,解引用返回value_type;若关键字还不存在,会创建一个对应关键字且值为0的新元素,已存在则返回最近一次赋的值;只能用于map和unorders_map
- 查找元素:find(第一个等于的迭代器)、count(数量)、lower_bound(第一个小于等于的迭代器)、upper_bound(第一个大于的迭代器)、equal_range(返回一个迭代器pair,表示所有相等的头及尾后位置)
- 无序关联容器:使用关键字的哈希函数和==运算符;不能直接定义关键字类型为自定义类类型的无序容器,必须提供自己的hash模板版本,或者用另一种方法,类似于有序容器重载关键字类型的默认比较操作
- 管理桶:无序容器在存储上组织为一组桶,每个桶保存哈希值相同的若干元素;无序容器的性能依赖于哈希函数的质量和桶的数量及大小;管理桶的函数包括桶接口、桶迭代、哈希策略
动态内存
- 动态内存:除了静态内存和栈内存,每个程序有自由空间(堆),用于存储动态分配的对象;自由空间中分配的对象直到显式释放或程序结束才被销毁
- 智能指针:自动释放指向的对象及对象所占内存;shared_ptr可多个指向一个对象,unique_ptr独占所指对象;智能指针不支持指针算术运算
- 引用计数:shared_ptr关联的计数器,拷贝shared_ptr时递增,赋值或销毁时递减;计数器为0则自动释放管理对象
- 使用动态内存的场景:不知道对象数量、不知道对象的准确类型、多个对象间共享数据
- 直接内存管理:new分配内存,delete释放;不能依赖类对象拷贝、赋值、销毁的默认定义;内置类型默认初始化为未定义,建议用圆括号直接初始化;只有当括号中仅有单一初始化器时才可以使用auto
- 动态分配的const:new分配const对象是合法的,它的值不能被改变,但本身可用delete销毁
- 空悬指针:指向一块曾经保存数据对象但现在无效的内存的指针;delete指针会变成空悬指针,此时建议用nullptr给它赋值
- 初始化智能指针:不能进行内置指针到智能指针的隐式转换,必须使用直接初始化形式;智能指针默认使用delete释放,所以用于初始化智能指针的普通指针必须指向动态内存,管理的资源不是new分配的内存,则必须传入一个删除器替代delete
- 混用智能指针和普通指针:shared_ptr绑定普通指针,不应再使用内置指针访问这块内存;只有确定代码不会delete指针时才能使用get将访问权限传递给代码,不要用get初始化或给另一个智能指针赋值
- 修改shared_ptr共享对象:当改变底层对象时调用unique判断自己是否是仅有用户,如果不是则用reset制作新拷贝
- unique_ptr:不支持普通的拷贝或赋值,但可拷贝或赋值一个即将销毁的unique_ptr,如函数返回值;调用release或reset将指针的所有权从一个非const的unique_ptr转移给另一个unique_ptr;调用reset()或用nullptr赋值可以释放对象,release只会切断管理关系
- unique_ptr的删除器:管理方式与share_ptr不同,删除器会影响到类型及构造过程;重载删除器必须在尖括号里指明删除器类型,并在参数列表里指定一个可调用对象(删除器)
- weak_ptr:是一种弱引用,指向shared_ptr的对象,但不改变引用计数;调用lock返回指向的shared_ptr对象,当对象计数器为0时返回空shared_ptr
- 分配动态数组:使用new T[]分配指定数量的内存,返回的是元素类型指针而不是数组;可用空括号或花括号对数组初始化,但不能在括号中出现初始化器,所以不能用auto;可分配0长度的数组,此时返回的指针类似尾后指针
- 释放动态数组:delete []释放,空的方括号是必须的,否则会没有警告但行为异常
- 动态数组的智能指针:unique_ptr管理new分配的数组,在尖括号里的对象类型后面跟一对空括号,不能使用点或箭头运算符,可使用下标运算符访问数组中的元素;shared_ptr不能直接管理动态数组,必须提供自己的删除器,并且只能通过get内置指针后进行指针算术运算来访问数组元素
- allocator类:allocate分配一定大小的原始未构造的内存;deallocate的参数必须是allocate返回的指针和allocate传入的大小;deallocate前调用destroy析构已创建的每个对象,allocate后调用cnostruct构造对象、或调用uninitialized_copy[_n]拷贝对象、或uninitialized_fill[_n]填充未初始化的内存
转载请注明出处