第8章 IO库
- 由于不能拷贝IO对象,因此不能将形参和返回值类型设置为流类型。进行IO操作的函数通常以引用方式传递和返回流。读写一个IO对象会改变其状态,因此传递和返回的引用不能是const的。
-
管理流的状态:
流的rdstate成员返回一个iostate值,对应流的当前状态。
setstate操作将给定条件位置位,表示发生了对应错误。
clear操作不接受参数的版本清除(复位)所有错误标志位。执行clear()后,调用good会返回true。
clear的带参数版本接受一个iostate值,表示流的新状态。
为了复位单一的条件状态位,可以先用rdstate读出当前条件状态,再用位操作将所需位复位来生成新状态,例如:
//复位failbit和badbit,其他标志位不变
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);
- 刷新输出缓冲区相关的操纵符:
cout << "hi!" << endl; //输出"hi!"和换行,刷新缓冲区
cout << "hi!" << flush; //输出"hi!",刷新缓冲区,不附加额外字符
cout << "hi!" << ends; //输出"hi!"和一个空字符,刷新缓冲区
cout << unitbuf; //设为所有输出操作后都会立即刷新缓冲区
cout << nounitbuf; //回到正常的缓冲方式
交互式系统通常应该关联输入流和输出流。这意味着所有输出,包括用户提示信息,都会在读操作之前被打印出来。
tie有两个重载的版本:一个版本不带参数,返回指向输出流的指针。
x.tie(); //返回指向x关联到的输出流的指针;若x未关联到流,则返回空指针
tie的第二个版本接受一个指向ostream的指针,将自己关联到此ostream。
x.tie(&o); //将流x关联到输出流o
每个流最多关联到一个流,但多个流可以同时关联到同一 ostream 。
当一个 fstream 对象被销毁时,close 会自动被调用。
以 out 模式打开文件会丢弃已有数据。
保留被 ofstream 打开的文件中已有数据的唯一方法是显式指定 app 或 in 模式。- 当我们的某些工作是对整行文本进行处理,而其他一些工作是处理行内的每个单词时,通常可以使用 istringstream 。
当我们逐步构造输出,希望最后一起打印时,可以使用 ostringstream 。
ostringstream 对象调用 str() 返回其中保存的数据,调用 str("") 将流中保存的数据清空。
第9章 顺序容器
- vector——可变大小数组
deque——双端队列
forward_list——单向链表
list——双向链表
array——固定大小数组
string——与vector相似的容器,专门用于保存字符 以下是一些选择容器的基本原则:
● 除非你有很好的理由选择其他容器,否则应使用 vector 。
● 如果程序有很多小的元素,且空间的额外开销很重要,则不要使用 list 或 forward_list 。
● 如果程序要求随机访问元素,应使用 vector 或 deque 。
● 如果程序要求在容器的中间插入或删除元素,应使用 list 或 forward_list 。
● 如果程序需要在头尾位置插入或删除元素,但不会在中间位置进行插入或删除操作,则使用 deque 。
● 如果程序只有在读取输入时才需要在容器中间位置插入元素,随后需要随机访问元素,则
—— 首先,确定是否真的需要在容器中间位置添加元素。当处理输入数据时,通常可以很容易地向 vector 追加数据,然后再调用标准库的 sort 函数来重排容器中的元素,从而避免在中间位置添加元素。
—— 如果必须在中间位置插入元素,考虑在输入阶段使用 list ,一旦输入完成,将 list 中的内容拷贝到一个 vector 中。
如果你不确定应该使用哪种容器,那么可以在程序中只使用 vector 和 list 的公共操作:使用迭代器,不使用下标操作,避免随机访问。这样,在必要时选择使用 vector 或 list 都很方便。
当不需要写访问时,应使用cbegin和cend。
当将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同。
除array外,swap不对任何元素进行拷贝、删除或插入操作,因此可以保证在常数时间内完成。
emplace函数在容器中直接构造元素。传递给emplace的参数必须与元素类型的构造函数相匹配。
- 除了顺序容器外,标准库还定义了三个顺序容器适配器:stack、queue 和 priority_queue 。
这些容器适配器通过成员函数 push() 和 pop() 添加和删除元素(而非 push_back() ),栈为后进先出(LIFO),队列为先进先出(FIFO),priority_queue 为优先级高的先出队,详见原书368页或中文版329页。
第10章 泛型算法
- 泛型算法永远不会执行容器的操作,它们只会运行于迭代器之上,执行迭代器的操作。算法不能(直接)添加或删除元素。
对于只读取而不改变元素值的算法,通常最好使用cbegin()和cend()。但是,如果你计划使用算法返回的迭代器来改变元素的值,就需要使用begin()和end()的结果作为参数。
- 那些只接受单一迭代器来表示第二序列的算法,都假定第二序列至少和第一序列一样长。向目的位置迭代器写入数据的算法假定目的位置足够大,能容纳要写入的元素。
- 算法命名规范:
- "
_copy
"版本。这些算法不会改变原序列,会在新序列保存结果。 - "
_if
"版本。接受一个谓词替代原本(查找)的元素值。
- "
- 一个lambda表达式具有如下形式:
[capture list] (parameter list) -> return type { function body }
可以忽略 参数列表 和 返回类型 ,但必须永远包含 捕获列表 和 函数体 。
——忽略括号和 参数列表 等价于指定了一个空参数列表。
——忽略 返回类型 :如果函数体只是一个return语句,会自动推断返回类型;如果包含任何单一return语句之外的内容,则返回void。
捕获列表 只适用于局部非static变量,lambda可以直接使用局部static变量和在它所在函数之外声明的名字。 - 当定义一个lambda时,编译器生成一个新的与lambda对应的类类型。默认情况下,从lambda生成的类包含一个对应该lambda所捕获变量的数据成员,此数据成员在lambda对象创建时被初始化(而非调用时)。
因此,需要保证lambda每次执行时这些数据都还有预期的意义。
值捕获、引用捕获:与参数传递类似。
隐式捕获:"&
"表示默认引用捕获,"=
"表示默认值捕获,写在第一位。
ostream &os = cout;
char c = '.';
auto f1 = [&, c](const string &s) { os << s << c; }; //os隐式捕获,引用捕获;c显式捕获,值捕获
auto f2 = [=, &os](const string &s) { os << s << c; }; //os显式捕获,引用捕获;c隐式捕获,值捕获
可变lambda:对于值被拷贝的变量,lambda不会改变其值,若想改变,则加关键字mutable。(引用捕获则看是否指向const)
int v = 42;
auto f = [v]() { return ++v; }; //错误!!表达式必须是可修改的左值
auto f = [v]() mutable { return ++v; }; //正确!!
- 参数绑定:
//#include <functional>
using namespace std::placeholders;
//假定f为可调用对象,它有5个参数
auto g = bind(f, a, b, _2, c, _1);
g(X, Y); //相当于调用下一条语句
f(a, b, Y, c, X);
//若要传引用,则使用 ref() 或 cref()
- inserter 插入器插入的元素是正序排列的(与 back_inserter 相同,与 front_inserter 相反)。
- 反向迭代器调用base成员函数可以转换为普通迭代器,指向原迭代器的相邻位置。
对于 list 和 forward_list ,应该优先使用成员函数版本的算法而不是通用算法。
第11章 关联容器
-
对于有序容器——map、multimap、set 以及 multiset,关键字类型必须定义元素比较的方法。默认情况下,标准库类型使用关键字类型的<运算符来比较两个关键字。
使用自定义的比较函数compare:
set<TypeName, decltype(compare)*> obj(&compare); //其中&符号可以去掉,因为在使用函数名时,当需要时它会自动转化为指针
pair 通过成员访问运算符来分别访问两个名为 first 和 second 的成员。
关联容器还定义了以下类型:
key_type —— 此容器的关键字类型
mapped_type —— map中与关键字关联的类型,只适用于map
value_type —— 对于set,为关键字类型;
对于map,为pair<const key_type, mapped_type>当解引用一个关联容器的迭代器时,我们会得到一个类型为容器的 value_type 的值的引用。
必须记住,一个 map 的 value_type 是一个 pair,我们可以改变 pair 的值,但不能改变关键字成员的值。
set 的迭代器是 const 的。添加元素:
对于不包含重复关键字的容器,添加单一元素的 insert 和 emplace 版本返回一个 pair:pair 的 first 成员是一个迭代器,指向容器中具有给定关键字的元素;pair 的 second 成员是一个 bool 值,指示插入操作是否成功。
对于允许重复关键字的容器,添加单一元素的 insert 操作返回一个指向新元素的迭代器,无须返回 bool 值。删除元素:
关联容器定义了一个额外的接受 key_type 参数的 erase 版本,删除所有匹配给定关键字的元素,返回实际删除元素的数量。map 和 unordered_map 容器提供了下标运算符和一个对应的 at 函数。与其他下标运算符不同,如果关键字不在 map 中,会为它创建一个元素添加到 map 中,关联值进行值初始化(at 函数则会抛出 out_of_range 异常)。
访问元素:
c.find(k):返回指向第一个关键字为 k 的元素的迭代器或尾后迭代器。
c.count(k):返回关键字等于 k 的元素的数量。
c.equal_range(k):返回一个迭代器 pair,表示关键字等于 k 的元素的范围。若 k 不存在,pair 的两个成员均等于 c.end()。
c.lower_bound(k)、c.upper_bound(k):仅适用于有序容器。若存在关键字为 k 的元素,两个函数分别返回两个迭代器可组成一个迭代器范围,表示所有关键字为 k 的元素的范围;若不存在关键字为 k 的元素,则两个函数会返回关键字的第一个安全插入点——不影响容器中元素顺序的插入位置。-
无序容器使用一个哈希函数和关键字类型的==运算符来组织元素。
如果关键字类型固有就是无序的,或者性能测试发现问题可以用哈希技术解决,就可以使用无序容器。
关于无序容器的更多内容,请见原书章节 11.4
第12章 动态内存
- shared_ptr 的初始化
//#include <memory>
//可以使用make_shared或new返回的指针来初始化智能指针
//指针指向一个值为"9999999999"的string
shared_ptr<string> p = make_shared<string>(10, '9');
shared_ptr<string> p2(new string(10, '9'));
/*接受指针参数的智能指针构造函数是explicit的,因此如下写法是错误的,必须使用直接初始化!!
*shared_ptr<string> p3 = new string(10, '9');*/
如果你将 shared_ptr 存放于一个容器中,以后不再需要全部元素,而只使用其中一部分,要记得用 erase 删除不再需要的那些元素。
使用动态内存的一个常见原因是允许多个对象共享相同的状态。
- new 和 delete
//new操作
int *p1 = new int; //默认初始化,*p1的值未定义
int *p2 = new int(); //值初始化,*p2为0
auto p3 = new auto(obj); //p3指向与obj类型相同的对象,该对象用obj进行初始化
//内存耗尽,new失败的情况
int *p1 = new int; //如果分配失败,new抛出std::bad_alloc异常
int *p2 = new (nothrow) int; //如果分配失败,new返回空指针
//delete操作
delete p; //p必须指向一个动态分配的对象或是空指针
p = nullptr; //指出p不再绑定任何对象
为了正确使用智能指针,我们必须坚持一些基本规范:
● 不使用相同的内置指针值初始化(或 reset )多个智能指针。
● 不 delete get() 返回的指针。
● 不使用 get() 初始化或 reset 另一个智能指针。
● 如果你使用 get() 返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。
● 如果你使用智能指针管理的资源不是 new 分配的内存,记住传递给它一个删除器。(见12.1.4末尾 “使用我们自己的释放操作 ”和12.1.5末尾 “向 unique_ptr 传递删除器 ”)- 一个 unique_ptr “拥有”它所指向的对象,在某个时刻只能有一个 unique_ptr 指向一个给定对象。因此,unique_ptr 不支持普通的拷贝或赋值操作。
但可以调用 release 和 reset 将指针所有权从一个非 const unique_ptr 转移到另一个:
unique_ptr<string> p1(new string("this is p1")), p2(new string("this is p2"));
unique_ptr<string> p3(p1.release()); //release将p1置为空,p3指向"this is p1"的string
p3.reset(p2.release()); //reset释放p3原来指向的内存,p3指向"this is p2"的string
另外,不能拷贝 unique_ptr 的规则有一个例外:可以拷贝或赋值一个将要被销毁的 unique_ptr。因此可以从函数返回一个 unique_ptr。
- 动态数组
要记住我们所说的动态数组并不是数组类型,这是很重要的。
//动态数组的new和delete
int *pi1 = new int[10]; //10个未初始化的int
int *pi2 = new int[10](); //10个值初始化为0的int
int *pi3 = new int[10]{ 1,2,3,4,5 }; //前5个用给定的初始化器初始化,剩余的进行值初始化
delete[] p; //p必须指向一个动态分配的数组或为空
//标准库提供了可以管理new分配的数组的unique_ptr版本
unique_ptr<int[]> up(new int[10]); //此指针可以使用下标运算符
up.release(); //自动调用delete[]销毁
- allocator 类
//#include <memory>
int n = 5;
allocator<string> alloc; //定义allocator对象,它可以为尖括号<>中所写类型的对象分配内存
auto p = alloc.allocate(n); //分配n个原始的未构造的内存,用来保存n个string
alloc.construct(p, "hello"); //在p所指的地址构造一个string对象
alloc.destroy(p); //对p指向的对象执行析构函数
alloc.deallocate(p, n); //释放p中地址开始的内存,其中n必须与调用allocate时填写的参数具有一样的值
/*allocator算法*/
auto q = alloc.allocate(n);
vector<string> vec{ "one","two","three" };
//将vec中的元素拷贝到q开始的地址,返回拷贝完成后的下一个地址
auto r = uninitialized_copy(vec.cbegin(), vec.cend(), q);
//从r开始的地址构造n-vec.size()个对象
uninitialized_fill_n(r, n - vec.size(), "rest");
【注】整理的是个人认为的书中值得注意的“Note”、“Best practices”等条目及部分知识点、代码,仅供参考。