# 前言
本篇笔记从原书第二部分开始,到第三部分第十四章重载运算与类型转换结束,不过对于 第八章 (IO 库)、第九章 (顺序容器)、第十一章 (关联容器) 基本略过。
因为这几章个人也只是粗略翻过,考虑到就算 UE5 里边,容器算法这些估计也都是全套 UE 系列自定义的模板,所以对于标准库提供的感觉目前大概了解下、有个印象就行了。
同理,第十章泛型算法也是如此,不过还是记录了部分思想。
本来之前还计划国庆节把这本书看完,结果因为国庆身体中招了,加上研究了下其它 (UE5 C++ 及其动画、AI 等),国庆后又忙于工作交接,导致现在其实也还剩了三分之一,因此觉得还是分一下阶段比较好... 可能还是得分成三份了。
# 第九章 顺序容器
vector
:可变大小数组,具有 size 和 capacity,当添加元素导致 size 超出 capacity 则翻倍扩容。deque
:双端队列,支持快速随机访问,在 头 (尾) 插入 (删除) 元素很快。list
:双向链表forward_lis
t:单链表array
:固定大小数组,与内置数组相比更安全、更容易使用string
:与 vector 类似,保存字符容器
容器适配器
stack
:栈,默认基于 deque 实现。queue
:队列,默认基于 deque 实现。priority_queue
:优先级队列,默认基于 vector 实现。
# 第十章 泛型算法
标准库并未给每个容器都定义成员函数来实现各种操作,而是定义了一组 泛型算法。
- 这组算法实现了一些经典算法的公共接口 (如排序、搜索)
- 一般情况这些算法不直接操作容器,而是遍历由两个迭代器指定的元素范围
- 迭代器令算法不依赖于容器:泛型算法本身不执行容器的操作,而是运行于迭代器之上,执行迭代器操作
# lamda 表达式
对于一个对象或一个表达式,如果可以对其使用调用运算符,则称它为可调用的。
- 函数和函数指针
- 重载了函数调用运算符的类
- lambda 表达式
一个 lambda 表达式表示一个可调用的代码单元,可理解为一个未命名的内联函数。
[捕获列表](参数列表)->返回类型{函数体}
- 参数列表和返回类型可被忽略:
auto f=[]{return 42;}
- 注:捕获列表只用于局部非 static 变量,lambda 可直接使用局部 static 变量和它所在函数之外声明的名字。
lambda 捕获和返回
- 当定义一个 lambda 时,编译器生成一个与 lambda 对应的新的 (未命名的) 类类型。
- 默认情况下,从 lambda 生成的类都包含一个对应该 lambda 所捕获的变量的数据成员 (类似任何普通类的数据成员),lambda 数据成员也在 lambda 对象创建时被初始化。
值捕获
auto f=[v1]{return v1;}
- 与传值参数类似,采用值拷贝的前提是变量可以拷贝,与参数不同,被捕获的变量在 lambda 创建时被拷贝 (进 lambda 数据成员)。
引用捕获
auto f=[&v1]{return v1;}
- 以引用捕获方式与其它任何类型的引用的行为类似,当在 lambda 内使用时,实际操作的是引用所绑定对象。
- 引用捕获与返回引用有相同问题和限制:必须确保执行时被引用对象依然存在。
隐式捕获
- 让编译器根据 lambda 体中的代码来推断需要使用哪些变量
- 捕获列表:&(引用捕获) 或 =(值捕获)
- 可混合使用:
[&,c]xxxx、[=,&c]xxxx
,捕获列表中第一个元素 (& 或 =) 指定了默认捕获方式为引用或值。
可变 lambda
- 默认情况下,值捕获的被拷贝的变量,lambda 不会改变其值
- 若想改变需要加上 mutable 关键字
[v1]()mutable {return ++v1;}
# 标准库 bind 函数
auto newCallable=bind(callable,arg_list);
- arg_list:参数列表, 形如『
_n
』,n 表示参数占位符 - 可看作一个通用函数适配器
- 接受一个可调用对象,生成一个新的可调用对象来 适配 原对象的参数列表
- 可用 bind 修正参数的值、调整顺序等
绑定引用参数
- 引用:ref (val),ref 返回一个对象,包含给定的引用
- 常量引用:cref (val),cref 生成一个保存 const 引用的类
- 例:bind (print,ref (os),_1,' ')
# 再探迭代器
额外迭代器:
- 插入迭代器:绑定于容器,用于插入元素
- 流迭代器:绑定到输入或输出流,用于遍历关联 IO 流
- 反向迭代器:向后而不是向前移动,除 forward_list 均有
- 移动迭代器:移动元素
# 第十一章 关联容器
按关键字有序保存元素
map
:关联数组,保存关键字 - 值对- 如果关键字还未在 map 中,下标运算会添加一个新元素
set
:关键字即值multimap
:关键字可重复出现的 mapmultiset
:关键字可重复出现的 set
无序集合
unorderd_map
:哈希函数组织的 mapunorderd_set
:哈希函数组织的 setunorderd_multimap
unorderd_multiset
# pair 类型
一个 pair 保存两个数据成员
make_pair
# 第十二章 动态内存
动态分配的对象生存期与它们在哪里创建无关,只有显示释放才会销毁。
在 C++ 中,动态内存管理运算符 new 和 delete 完成。
- 在自由空间分配的内存是无名的,new 运算符返回指向该对象的指针
int *pi1=new int; //默认初始化,*pi1 的值未定义
int *pi2=new int() //值初始化为 0,*pi2 为 0
- 注:对于定义了自己构造函数的类类型来说,不管采用哪种方式都会通过默认构造函数来初始化。默认初始化和值初始化仅影响内置类型成员。
- const 对象使用 new 分配是合法的 (也可以 delete)
- 内存耗尽时,new 表达式就会失败
- 由内置指针 (而非智能指针) 管理的动态内存在被显式释放前一直都会存在。
空悬指针:指向一块曾经保存数据对象但现在已经无效的内存的指针。
# 智能指针
智能指针也是模板。
默认初始化的智能指针中保存着一个空指针,如果在一个条件判断语句中使用智能指针,效果就是检测其是否为空。
- 解引用一个智能指针返回它指向的对象
即使发生异常,(由于局部对象会被销毁) 智能指针也会释放其对象。
# 类型
shared_ptr
- 允许多个指针指向同一个对象。
- 通过引用计数器记录,当我们拷贝 shared_ptr 时计数递增,被拷贝对象离开作用域被销毁递减
- 计数器归零则自动释放所管理对象 (通过析构函数)
unique_ptr
- 『独占』所指向的对象
weak_ptr
- 一种弱引用,指向 shared_ptr 所管理的对象
# make_shared 函数
此函数在动态内存分配一个对象并初始化,返回指向对象的 shared_ptr
- 可传递匹配对应对象构造函数的参数
- 若不传递任何参数,对象会被进行值初始化
# shared_ptr 和 new 结合使用
这两者允许结合使用,使用 new 返回的指针初始化智能指针 (但必须使用直接初始化方式):
shared_ptr<int> p(new int(1024))
- 另外 shared_ptr 提供了 get 函数返回普通指针
- reset:
- 释放绑定的普通指针
- 或重新绑定智能指针维护的普通指针
- (会更新引用计数)
注 1:默认情况下初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用 delete 释放关联对象。可使用 shared_ptr
注 2:但是不要混合使用智能指针和普通指针
# 自定义 shared_ptr 释放操作
可以传递一个可选的指向删除器函数的参数
# 智能指针使用注意
- 不使用相同的内置指针初始化 (或 reset) 多个智能指针
- 不 delete get () 返回的内置指针
- 不使用 get () 返回的内置指针初始化或 reset 另外一个智能指针
- 使用 get () 返回的内置指针时,注意智能指针自动释放后,该内置指针指向内存也会无效
- 如果智能指针不是管理的 new 操作符分配的动态内存,注意需要传递一个删除器以使其正确释放对象
# unique_ptr
某个时刻只有一个 unique_ptr 指向一个给定对象
因此 unique_ptr 不支持普通的拷贝或赋值操作,只能采用直接初始化方式:
unique_ptr<int> p(new int(1024))
- 注:拷贝限制的例外之处在于即将销毁时,例如允许从函数返回一个 unique_ptr
需要注意的是 release 操作:
- 该操作会切断 unique_ptr 与其管理的对象间的联系,返回对象内置指针 (不会释放对象内存)
- 返回的内置指针需要我们自己维护 (释放),或转移至另外一个智能指针管理
- (会导致释放对象的操作与 shared_ptr 一样,是 reset)
# weak_ptr
一种不控制所指向对象生存期的智能指针,其指向一个 shared_ptr,即使有 weak_ptr 指向,当其它指向 shared_ptr 销毁时对象就会被 shared_ptr 销毁。
主要操作
- expired ():若 shared_ptr 数量为 0 (对象可能已释放),返回 true,否则 false
- lock ():若 expired () 为 true,返回空,否则返回 shared_ptr
- use_count ():shared_ptr 共享数量
使用: if(shared_ptr<int> p=wp.lock()){}
# 动态数组
- new T [],返回一个数组元素类型的指针
- delete [] array,释放
- 或使用智能指针管理:
unique_ptr<int[]> array(new int[10])
array.release()
- 对于 unique_ptr 指向的数组不能使用点或箭头成员运算符,但可以使用下标访问元素
- 注:shared_ptr 不直接支持动态数组,如果想使用 shared_ptr 管理动态数组,需要提供自定义的删除器且访问也更麻烦 (通过获取内置指针)
- 注:动态数组并不是数组类型,也不能使用 begin 或 end 获取迭代器
建议:大多数应用应该使用标准库容器而不是动态分配的数组。标准库容器更简单、安全,性能也更好。
# allocator 类
new 将内存分配和对象构造组合在了一起,灵活性更有局限。
allocator 类将内存分配和对象构造分离开来,其分配的内存是原始的、未构造的。
操作
allocator<T> a
:定义一个可以为类型 T 分配内存的 allocator 对象allocate(n)
:分配可以存储 n 个对象的原始内存deallocate(p,n)
:释放,调用之前必须对每个对象先调用过 destroyconstruct(p,args)
:实际构造对象destroy(p)
:对 p 指针指向对象执行析构函数
注:为使用 allocate 返回的内存,必须用 construct 构造对象。
另外还提供了可以拷贝和填充未初始化内存算法:
uninitialized_copy
uninitialized_copy_n
uninitialized_fill
uninitialized_fill_n
# 第十三章 拷贝控制
当定义一个类时,我们显式指定在此类型的对象拷贝、移动、赋值和销毁时做什么:
- 拷贝构造函数:当用同类型的另一个对象初始化本对象时做什么
- 拷贝赋值运算符:将一个对象赋予同类型另一个对象时做什么
- 移动构造函数:当用同类型的另一个对象初始化本对象时做什么
- 移动赋值运算符:将一个对象赋予同类型另一个对象时做什么
- 析构函数:销毁时做什么
- 注:这些操作若我们未显式定义,则编译器会自动生成
# 拷贝构造函数
一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值
- Foo (); // 默认构造函数
- Foo (const Foo&); // 拷贝构造函数
拷贝初始化
- 拷贝初始化通常使用拷贝构造函数完成,不过一个类有移动构造函数时,有时会使用移动构造函数完成
- string s ("1") // 直接初始化
- string s2=s; // 拷贝初始化
- 其它发生情况:
- 将一个对象作为实参传递给一个非引用类型的形参
- 从一个返回类型为非引用类型的函数返回一个对象
- 花括号列表初始化一个数组中的元素或一个聚合类中的成员
# 拷贝赋值运算符
重载赋值运算符
- 重载运算符本质上是函数,其名字由 operator 关键字后接表示要定义的运算符的符号组成
- 因此赋值运算符是一个名为 operator= 的函数,类似于任何其它函数
- 运算符函数也有一个返回类型和一个参数列表
拷贝赋值运算符表现:
Foo& operator=(const Foo&);
注:注意即使一个对象赋值给自身,也需要正确工作。因此处理拷贝控制时需要临时变量做缓存,即销毁左侧运算对象资源之前拷贝右侧运算对象。
# 析构函数
资源释放
注 1:析构函数体自身并不直接销毁成员,成员是在析构函数体之后隐含的析构阶段中被销毁的。析构函数体作为成员销毁步骤之外的另一部分而进行。
注 2:根据三 / 五法则,如果一个类需要定义析构函数,几乎可以肯定它也需要自定义拷贝构造函数和拷贝赋值运算符。
# 阻止拷贝
可以通过将拷贝构造函数和拷贝赋值运算符定义为 删除的函数 来阻止拷贝。
NoCopy()=default; //使用合成的默认构造函数
NoCopy(const NoCopy&)=delete; //阻止拷贝
NoCopy &operator=(const NoCopy&) =delete; //阻止赋值
注 1:不同于 =default,我们可以对任何函数指定 =delete
注 2:本质上,当不可能拷贝、赋值或销毁类的成员时,类的合成拷贝控制成员就被定义为删除的。
# 拷贝控制和资源管理
众所周知,若一个类对象被拷贝,若成员是指针,而我们定义了释放该指针的析构函数,每个发生释放的拷贝对象都会释放指针资源,导致错误问题。
因此需要同时重载拷贝构造函数。
# 行为像值的类
重载的拷贝构造函数或拷贝赋值运算符中,将指针对象重新分配一次,每个拷贝对象中指针都指向内容相同但实际不同的对象。
这样每次析构函数释放时都是释放自己指向的指针。
# 行为像指针的类
需要定义拷贝构造函数和拷贝赋值运算符,以拷贝成员本身而不是它指向的对象。
而析构函数释放关联对象内存的方式,必须是最后一个指向关联对象的该对象销毁才可以释放。
- 因此最好的方法是使用 shared_ptr 管理类中资源
- 或自定义引用计算进行管理
- 构造函数还需要创建一个引用计算,记录有多少对象与正在创建对象共享状态
- 拷贝构造函数不分配新的计数器,而是拷贝数据成员并递增共享的计数器
- 析构函数递减计数器,若计数器达到 0,则释放状态
- 拷贝赋值运算符递增右侧运算对象计数器,递减左侧运算对象计数器,同时判断左侧计数器是否为 0 以决定是否释放其资源
- 注:引用计数不能直接作为对象数据成员,可以使用动态内存分配指针
# 交换操作
swap 标准库函数,有时需要定义自己的 swap 覆盖标准库的函数,避免额外分配。
可以在赋值运算符中使用使用 swap,使用拷贝和交换的赋值运算符自动就是异常安全的,且能正确处理自赋值。
# 移动构造函数与 std:move
# 对象移动
IO 类和 unique_ptr 可以移动但不能拷贝
# 右值引用
左值持久,右值短暂
&&:只能绑定到临时对象,如对象将要销毁、对象没有其它用具
- 使用右值引用可以自由地接管所引用对象的资源
- 变量是左值:因此不能将右值引用直接绑定到一个变量上,即使这个变量是右值引用也不行
但是允许使用 move 函数显式将一个左值转换为对应右值引用类型
int &&rr3=std::move(rr1);
- 调用 move 意味着承诺:除了对 rr1 赋值或销毁它外,我们将不再使用它
- 注:使用 move 应使用 std:move
# 移动构造函数和移动赋值运算符
这两个成员类似对应拷贝操作,但它们从给定对象『窃取』资源而不是拷贝资源。
- 移动构造函数不分配任何新的内存
- 类名 (类名 &&s) noexcept:{}
noexcept
- 承诺一个函数不抛出异常
- 移动构造函数通常不应抛出异常,因此可以以此标识,通知标准库减少一些额外操作
移动赋值运算符执行与析构函数和移动构造函数相同工作。
- 移动赋值运算符必须销毁左侧运算对象的旧状态
注 1:在移动操作之后,移后源对象必须保持有效的、可析构的状态,但是用户不能对其值做任何假设。
注 2:编译器根本不会为某些类合成移动操作。特别是一个类已定义自己的 拷贝构造函数、拷贝赋值运算符或析构函数,就不会合成移动构造函数和移动赋值运算符。—— 只有当一个类没有定义任何自己版本的拷贝控制成员,且所有数据成员都能移动构造或移动赋值时,编译器才会为其合成移动操作。
注 3:如果一个类没有移动构造函数或移动赋值运算符,则类会使用对应的拷贝操作来代替移动操作。
# 右值引用的成员函数
成员函数也可以同时提供拷贝和移动版本。
# 引用限定符
通过 & 或 && 分别指出 this 可以指向一个左值或一个右值
Foo &operator=(const Foo&) &; //只能向可修改的左值赋值
Foo sorted() &&; //可用于可改变的右值
# 第十四章 重载运算与类型转换
赋值运算符
- 例如使类型可以像 vector 一样接受花括号初始化
xxx &operator=(std::initializer_list<std::string>);
下标运算符
operator[]
递增和递减运算符
- 区分前置和后置
- 前置返回对象引用,后置返回对象的原值
成员访问运算符
*
->
- 重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。
函数调用运算符
operator()
- 可以像使用函数一样使用该类对象
- 如果类定义了调用运算符,则该类对象称作 函数对象
lambda
- lambda 生成的匿名类型中含有一个重载函数调用运算符
stable_sort(words.begin(),words.end(),类名());
- 捕获变量的 lambda 则生成构造函数带有对应参数
# 可调用对象与 function
C++ 语言中有几种可调用对象:
- 函数
- 函数指针
- lambda 表达式
- bind 创建的对象
- 重载了函数调用运算符的类
可调用对象也有类型,但两个不同类型的可调用对象可能共享同一种 调用形式。
- 调用形式:指明了调用返回的类型及传递给调用的实参类型
# 标准库 function 类型
- function 是一个模板,定义时可用于指定该 function 类型能够表示的对象的调用形式。
- 只要可调用对象满足指定的调用形式,就可以存储于同一个 function 类型中。
- 例如:
function<int(int,int)>
- 表示接受两个 int,返回一个 int 的可调用对象
- 如此声明后,可用于表示任意一种类似可调用类型:函数指针、函数对象类的对象 (函数对象)、lambda
# 重载函数与 function
- 如果函数存在重载,那么直接通过函数名称存储会导致二义性
- 可通过 存储函数指针而非函数名字,或 lambda 表达式来消除二义性
- 定义函数指针:
int (*fp)(int,int)=add
- 定义 lambda:
[](int a,int b){return add(a,b);}
- 定义函数指针:
# 重载、类型转换与运算符
# 类型转换运算符
类型转换运算符是类的一种特殊的成员函数,负责将一个类类型转换成其它类型。
operator type() const
- 一个类型转换函数必须是类的成员函数,它不能声明返回类型,形参列表也必须为空 (类型转换运算符是隐式执行的)。类型转换函数通常应该是 const (不应修改待转换对象的内容)
# 显式的类型转换运算符
- 为防止隐式转换可能产生的异常情况
explicit operator type() const
- 显式的类型转换也可能会被隐式执行:
- if、while 及 do 语句条件部分
- for 语句头的条件表达式
- 逻辑非运算符 (!)、逻辑或运算符 (||)、逻辑与运算符 (&&) 的运算对象
- 条件运算符
(? :)
的条件表达式
- 注:向 bool 的类型转换通常用在条件部分,因此 operator bool 一般定义成 explicit 的
# 避免二义性转换
- 例如定义了多个类型转换规则,可以通过其它类型转换联系到一起。
- 重载构造函数 (默认隐式转换) 也可能导致
- 对同一个类既提供转换目标是算数类型的类型转换,也提供了重载的运算符,重载运算符与内置运算符将会有二义性问题