📔 C++ Primer 0x0C 学习笔记

​更好的阅读体验(实时更新与修正)​

12.1 动态内存与智能指针

  • 运算符​​new​​,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象初始化
  • 运算符​​delete​​,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存
  • 智能指针负责自动释放所指向的对象,定义在​​memory​​头文件中
  • ​shared_ptr​​允许多个指针指向同一个对象
  • ​unique_ptr​​独占所指向的对象
  • ​weak_ptr​​​是一种弱引用,指向​​shared_ptr​​所管理的对象

12.1.1 shared_ptr 类

  • 智能指针也是模板
  • 最安全的分配和使用动态内存的方法是调用一个名为​​make_shared​​的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的​​shared_ptr​
  • 当进行拷贝或赋值操作时,每个​​shared_ptr​​都会记录有多少个其他​​shared_ptr​​指向相同的对象,可以认为关联了一个计数器(通常称引用计数,具体用什么数据结构实现,完全由标准库的具体实现来决定)
  • 当我们给​​shared_ptr​​赋予一个新值或是​​shared_ptr​​被销毁,计数器就会递减。一旦一个​​shared_ptr​​的计数器变为0,它就会自动释放自己所管理的对象
  • 当指向一个对象的最后一个​​shared_ptr​​被销毁时,​​shared_ptr​​类会自动通过析构函数销毁此对象
  • ​shared_ptr​​还会自动释放相关联的内存
  • 如果你将​​shared_ptr​​存放于一个容器中,而后不再需要全部元素,只用部分,记得要用​​erase​​删除不再需要的元素
  • 程序使用动态内存出于三种原因之一
  • 程序不知道自己需要使用多少对象
  • 程序不知道所需对象的准确类型
  • 程序不知道需要在多少个对象之间共享数据,使用动态内存的一个常见原因是允许多个对象共享相同状态
  • 如果两个对象共享底层数据,当某个对象被销毁是,我们不能单方面地销毁底层数据

12.1.2 直接管理内存

  • 在自由空间分配的内存是无名的,因此​​new​​无法为其分配的对象明明,而是非那会一个指向该对象的指针
  • 默认情况下,动态分配的对象是默认初始化的,我们也可以进行值初始化
  • 用​​new​​分配​​const​​对象是合法的,返回的指针是一个指向​​const​​的指针,一个动态分配的​​const​​对象必须初始化
  • 默认情况下,​​new​​不能分配所要求内存空间,会跑出一个类型为​​bad_alloc​​的异常。我们也可以改变使用​​new​​的方式阻止跑出异常(定位​​new​​)
int *p2 = new (nothrow) int;//如果分配失败,返回空指针
  • ​delete​​执行两个动作:销毁给定指针指向的对象,释放对应的内存
  • 我们传递给​​delete​​的指针必须指向动态分配的内存或是一个空指针,释放一个并非​​new​​分配的内存,或将相同的指针值释放多次的行为是未定义的
  • 动态对象的生存期直到被释放时为止
  • 与类类型不同,内置类型的对象被销毁时什么也不会发生。特别是当一个指针离开其作用域时,它所指向的对象什么也不会发生,如果指向的是动态内存,那么内存不会被自动释放
  • 由内置指针管理的动态内存被显式释放前一直都会存在
  • 使用​​new​​和​​delete​​管理动态内存存在三个常见问题,坚持只用智能指针可以避免这些问题
  • 忘记​​delete​​内存
  • 使用已经释放掉的对象
  • 对同一块内存释放两次
  • ​delete​​​一个指针后,指针值就无效了。虽然指针已经无效,但是很多机器上指针仍然保存着已经释放了动态内存的地址。在​​delete​​之后,指针变成空悬指针,指向一块曾经保存对象但现在已经无效的内存的指针。所以要在​​delete​​后将​​nullptr​​赋予指针。(但这只是有限的保护了这一个指针,不能影响其他指向该内存的指针)

12.1.3 shared_ptr 和 new 结合使用

  • 如果我们不初始化一个智能指针,它就会被初始化为一个空指针
  • 可以用​​new​​返回的指针来初始化智能指针,接受指针参数的智能指针构造函数是​​explicit​​的,因此不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针(但不建议使用,最好用​​make_shared​​)
  • 默认情况下一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认用​​delete​​释放对象
  • 不要混合使用普通指针和智能指针,​​shared_ptr​​可以协调对象的析构,但这仅限其自身的拷贝(也是​​shared_ptr​​)之间。所以推荐使用​​make_shared​​,而不是​​new​​。这样,我们就能在分配对象的同时就将​​shared_ptr​​与之绑定,从而避免了无意中将同一块内存绑定到多个独立创建的​​shared_ptr​​上
  • 智能指针类型定义了一个名为​​get​​的函数,返回一个内置指针,指向智能指针管理的对象。用来解决这样的情况:我们需要向不能使用智能指针的代码传递一个内置指针
  • ​get​​​返回的指针的代码不能​​delete​​此指针
  • 不要使用​​get​​初始化另一个智能指针或为智能指针赋值
  • ​reset​​​将一个新的指针赋予一个​​shared_ptr​​,会更新引用计数
  • ​unique​​检查自己是否是当前对象的仅有用户

12.1.4 智能指针和异常

  • 当发生异常时,我们直接管理的内存是不会自动释放的。如果使用内置指针管理内存,且在​​new​​​之后对应的​​delete​​之前发生了异常,则内存不会被释放掉
  • 如果用​​shared_ptr​​​来管理不是动态内存的资源,我们需要定义一个函数来代替​​delete​
  • 智能指针使用规范
  • 不使用相同的内置指针初始化(或​​reset​​)多个智能指针
  • 不​​delete​​​通过​​get()​​返回的指针
  • 不使用​​get()​​​初始化或​​reset​​另一个智能指针
  • 如果你使用​​get()​​返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就无效了
  • 如果你使用智能指针管理的资源不是​​new​​分配的内存,记住传递给它一个删除器

12.1.5 unique_ptr

  • 一个​​unique_ptr​​​拥有它所指向的对象,某个时刻只能有一个​​unique_ptr​​​指向一个给定对象,当​​unique_ptr​​被销毁时,它所指向的对象也被销毁
  • ​unique_ptr​​​没有类似​​make_shared​​​的标准库函数,当我们定义一个​​unique_ptr​​​时,需要绑定到一个​​new​​返回的指针上且必须采用直接初始化的方式进行初始化
  • **我们不能拷贝或赋值​​unique_ptr​​​**但是可以通过调用​​release​​​或​​reset​​​将指针的所有权从一个(非​​const​​​)​​unique_ptr​​转移给另一个
  • 较早标准库包含了一个名为​​auto_ptr​​​的类,具有​​unique_ptr​​​的部分特性但不是全部。我们不能在容器中保存​​auto_ptr​​​,也不能从函数中返回​​auto_ptr​
  • ​u.release()​​​:​​u​​​放弃对指针的控制权,返回指针,并将​​u​​置为空
  • ​u.reset(q)​​​:接受一个可选的指针参数,令​​unique_ptr​​​重新指向给定的指针,如果​​unique_ptr​​原来不为空,它原来指向的对象被释放
  • 不能拷贝​​unique_ptr​​​规则有个例外,我们可以拷贝或赋值一个将要被销毁的​​unique_ptr​​​,最常见的就是从函数返回一个​​unique_ptr​
  • ​unique_ptr​​​默认情况下使用​​delete​​​释放它指向的对象,我们可以传递一个删除器。但是​​unique_ptr​​​管理删除器的方式和​​shared_ptr​​不一样。
  • 重载一个​​unique_ptr​​​中的删除器会影响到​​unique_ptr​​​类型以及如何构造(或​​reset​​)该类型的对象

12.1.6 weak_ptr

  • ​weak_ptr​​​是一种不控制所指向对象生存期的智能指针,它指向一个​​shared_ptr​​管理的对象
  • 将一个​​weak_ptr​​​绑定到一个​​shared_ptr​​​不会改变​​shared_ptr​​​的引用计数。如果最后一个​​shared_ptr​​​被释放销毁了,即使有​​weak_ptr​​​,对象也会被释放。这就是​​weak​​,弱共享对象。
  • 由于对象可能不存在,我们不能使用​​weak_ptr​​​直接访问对象,而必须调用​​lock​​​。​​lock​​​检查对象是否存在,如果存在,返回一个指向共享对象的​​shared_ptr​
  • 通过使用​​weak_ptr​​,不会影响一个给定的对象的生存期,但是可以阻止用户访问一个不再存在的对象的企图

12.2 动态数组

  • 标准库包含一个名为​​allocator​​​的类,允许我们将分配和初始化分离,使用​​allocator​​通常会提供更好的性能和更灵活的内存管理能力
  • 实际上大多数应用没有直接访问动态数组的需求,当一个应用需要可变数量对象时,使用​​vector​​会更方便,使用容器的类可以使用默认版本的拷贝、赋值和析构操作
  • 分配动态数组的类必须定义自己版本的操作,在拷贝、复制以及销毁对象时管理所关联的内存

12.2.1 new 和数组

  • 通常称​​new T[]​​分配的内存为动态数组,但要记住它并不是数组类型,我们得到一个数组元素类型的指针
  • ​new int[10]​​​10个还没初始化的​​int​​​,​​new int[10]()​​​10个值初始化为0的​​int​
  • ​new T[]​​分配的内存也不是数组类型,这意味着
  • 不能对动态数组调用​​begin​​​或​​end​
  • 不能用范围​​for​​语句来处理其中的元素
  • 可以采用​​int* pia2 = new int[10]();​​​10个值初始化为0的int 。这样空括号对的方式进行值初始化,但不能在括号中给出初始化器,这意味着不能用​​auto​​分配数组
  • 动态分配一个空数组是合法的,返回一个合法的非空指针。此指针保证与​​new​​返回的其他任何指针不同,可以在此指针上加减0,也可以减去自身得到0,但不能解引用
  • 使用​​delete []pa;​​释放动态数组
  • 可以使用​​unique_ptr​​​来管理​​new​​​分配的数组,当​​unique_ptr​​​销毁它管理的指针时,会自动使用​​delete[]​​​;​​shared_ptr​​​不直接支持管理动态数组,需要自定义删除器,而且也没支持下标和指针算术运算,需要通过​​get()​​获取一个内置指针来访问
  • 当一个​​unique_ptr​​指向一个数组时
  • 不能用点和箭头运算符,因为是数组而不是单个对象
  • 可以用下标运算符来访问数组中的元素

12.2.2 allocator 类

  • ​new​​​将内存分配和对象构造组合到一起,​​delete​​将对象析构和内存释放组合到一起。这是灵活性上的一些局限,可能会导致一些不必要的浪费
  • ​allocator​​类帮助我们将内存分配和对象构造分离开来。提供一种类型感知的内存分配方法,它分配的内存是原始的,未构造的。我们按需在此内存中构造对象。
  • ​allocator​​是一个模板
  • 为了使用​​allocate​​​返回的内存,我们必须用​​construct​​构造对象。使用未构造的内存是未定义行为
  • 只能对真正构造了的元素进行​​destroy​​操作,来销毁元素
  • 元素被销毁后,可以重新使用这些内存保存其他东西,也可以将其归还给系统。释放内存通过调用​​deallocate​​来完成
  • 通过​​uninitialized_copy​​​和​​uninitialized_fill​​函数可以在给定的目的位置穿件元素,而不是系统分配内存给它们

12.3 使用标准库:文本查询程序

  • 如果两个类概念上共享了数据,可以使用​​shared_ptr​​来反映数据结构中的这种共享关系
  • 当我们设计一个类时,真正实现成员之前先编写程序使用这个类,是一种非常有用的方法