第十二章 动态内存

版权声明:转载请注明出处 https://blog.csdn.net/weixin_39918693/article/details/86569986


除了自动和static对象外,C++还支持动态分配对象。动态分配对象的正确释放被证明是编程过程中极其容易出错的地方。标准库中的智能指针类型可以有效的解决这个问题

静态内存用来保存局部static对象,类static数据成员以及定义在任何函数之外的变量。栈内存用来保存定义在函数内的非static对象。分配在静态内存或栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在其定义的程序块运行时才存在, static对象在使用之前分配,在程序结束时销毁

除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称为自由空间或堆。程序用堆来存储动态分配的对象


一、动态内存和智能指针

在C++中,动态内存的管理是通过一对运算符来完成的

  • 1、new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化
  • 2、delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存

新标准库提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_ptr则独占所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中

智能指针也是模板,当我们创建一个智能指针时,必须提供额外的信息——指针可以指向的类型

默认初始化的智能指针中保存着一个空指针

智能指针的使用方式于普通指针类似。解引用一个智能指针返回它所指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空

p.get():返回p中保存的指针
shared_ptr和unique_ptr都支持的操作p401

shared_ptr独有的操作p401:

  • 1、make_shared (args):返回一个shared_ptr,用args初始化该对象
  • 2、shared_ptr p(q):注意计数器的递增,内部指针是否能够完成转换
  • 3、p = q:注意计数器的变换,内部指针必须能够相互转换
  • 4、p.unique():
  • 5、p.use_count():速度很慢,主要用于调试

make_shared ():将执行值初始化

当用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增。当我们给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部shared_ptr离开其作用域)时,计数器就会递减。一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象

shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁其所管理的对象(通过调用该对象的析构函数),并释放它所占用的内存

由于在最后一个shared_ptr销毁前内存都不会释放,保证shared_ptr在无用之后不再保留是非常重要的

程序使用动态内存出于以下三种原因之一:程序不知道自己需要使用多少对象、程序不知道所需对象的准确类型、程序需要在多个对象间共享数据

自己直接管理内存的类与使用智能指针的类不同,他们不能依赖类对象的拷贝、赋值和销毁操作的任何默认定义。因此,使用智能指针的程序更容易编写和调试

在堆中分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针

默认情况下,动态分配的对象是默认初始化的(没有括号),这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化

我们也可以用空的圆括号来指定进行值初始化

类的默认初始化和值初始化都是通过类默认构造函数来完成的

对于类中那些依赖于编译器合成的默认构造函数的内置类型成员,如果他们未在类内被初始化(没有为他们提供类内初始值),那么他们的值也是未定义的

对于提供了单一初始化器的动态对象,我们可以使用auto关键字来推断所要分配的类型

一个动态分配的const对象必须进行初始化。对于一个定义了默认构造函数的类类型来说,其const对象可以隐式初始化。否则,就必须显式初始化。动态分配的const对象返回的是指向const的指针

默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。我们可以使用nothrow对象来使得new分配内存失败,也不会抛出异常。bad_alloc和nothrow都定义在头文件new中

传递给delete的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的

动态对象的生存期直到被释放时为止,由内置指针管理的动态内存在被显式释放前一直都会存在

在delete之后,建议将指针置空

shared_ptr和new结合使用,接受指针参数的智能指针的构造函数是explicit的,因此无法完成从普通指针向智能指针的隐式转换,而且必须使用直接初始化形式

默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。我们也可以将智能指针绑定到一个指向其他类型资源的指针上,但是为了这样做,必须提供自己的操作来替代delete

定义和改变shared_ptr的其他方法p412

shared_ptr可以从unique_ptr那里接管对象的所有权,然后将unique_ptr置为空

不要混合使用普通指针和智能指针,shared_ptr可以协调对象的析构,但这仅限于其自身的拷贝。相互独立的智能指针不具有协调对象析构的能力

使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁

将另一个智能指针也绑定到get返回的指针上是错误的

get用来将指针的访问权限传递给代码,你只有在确定代码不会delete指针的情况下,才能使用get。特别是,永远不要用get初始化另一个智能指针或者为另一个智能指针赋值

程序要确保在异常发生后资源能被正确地释放

函数的退出有两种可能,正常处理结束或者发生了异常,无论哪种情况,局部对象都会被销毁

没有定义析构函数的类为哑类,一般是那些为C和C++两种语言设计的类。如果是哑类,就要求用户显式的释放所使用的任何资源(可以用智能指针来解决这个问题)

当我们创建一个shared_ptr时,可以传递一个(可选的)指向删除器函数的参数(自定义释放操作)shared_ptr p(&c, end_connection);

为了正确使用智能指针,我们必须坚持以下基本规范:

  • 1、不使用相同的内置指针值初始化(或reset)多个智能指针
  • 2、不delete get()返回的指针
  • 3、不使用get()初始化或reset另一个智能指针
  • 4、如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效的
  • 5、如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器

某个时刻只能有一个unique_ptr指向一个给定对象。当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。其没有自己构造动态对象的能力。类似shared_ptr,初始化unique_ptr必须采用直接初始化形式

由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作

unique_ptr的操作p418

我们可以拷贝或赋值一个将要被销毁的unique_ptr(是一种特殊操作)。例:从函数返回一个unique_ptr

unique_ptr管理删除器的方式与shared_ptr不同。重载一个unique_ptr中的删除器会影响到unique_ptr类型以及如何构造(或reset)该类型的对象。需要在尖括号内提供删除器的类型(函数指针),与重载关联容器的比较操作类似。而shared_ptr不用这么麻烦

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。weak_ptr不会该变shared_ptr的引用计数,也不会阻止对象被销毁

weak_ptr操作p420
以后用到再详细看


二、动态数组

vector和string都是在连续内存中保存他们的元素,既一次分配一个对象数组

C++语言定义了另一种new表达式语法,可以分配并初始化一个对象数组
标准库中包含一个名为allocator的类,允许我们将分配和初始化分离(更高效、更灵活)

使用容器类可以使用默认版本的拷贝、赋值和析构操作。分配动态数组的类则必须定义自己版本的操作,在拷贝、复制以及销毁对象时管理所关联的内存(建议使用标准库容器类)

格式:int *pia = new int[get_size()];
方括号中的大小必须是整型,但不必是常量,与数组的维度要求不一样

分配一个动态数组,其实并没有得到一个数组对象,只得到了一个元素类型的指针。由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin和end,也不能用范围for循环来处理动态数组中的元素(动态数组并不是数组类型)

在分配动态数组的时候,默认执行默认初始化,我们可以在最后加()来显式执行值初始化。我们还可以用初始化器来完成初始化(花括号列表)。初始化器会用来初始化动态数组中开始部分的元素。如果初始化器数目小于元素数目,剩余元素将进行值初始化。如果初始化器大于元素数目,则new表达式失败,不会分配任何内存,并且还会抛出bad_array_new_length的异常,其定义在new头文件中

虽然我们用空括号对数组中的元素进行值初始化,但不能在括号中给出初始化器,所以不能用auto分配数组

动态分配一个空数组是合法的

释放动态数组的格式:delete [] pa; pa必须指向一个动态数组或为空

标准库提供了一个可以管理new分配的数组的unique_ptr版本p426

与unique_ptr不同,shared_ptr不直接支持管理动态数组。如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器

由于shared_ptr不直接支持管理动态数组,所以用shared_ptr来访问动态数组有诸多不便

智能指针类型不支持指针算术运算,递增递减也不支持

标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的

allocator是一个模板,为了定义一个allocator对象,我们必须指明这个allocator可以分配的对象类型。当一个allocator对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置

  • 1、allocator a:
  • 2、a.allocate(n):
  • 3、a.deallocate(p, n):
  • 4、a.construct(p, args):
  • 5、a.destroy§:意思显而易见,注意事项用时再看

使用未构造的内存,其行为是未定义的

allocator算法:

  • 1、uninitialized_copy(b, e, b2)
  • 2、uninitialized_copy_n(b, n, b2)
  • 3、uninitialized_fill(b, e, t)
  • 4、uninitialized_fill_n(b, n, t)一看就会

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

猜你喜欢

转载自blog.csdn.net/weixin_39918693/article/details/86569986