C++抽象编程——内存管理(1)——动态分配与内存管理

动态分配与内存管理

到目前为止,你已经看到了两种将内存分配给变量的机制。

  • 当你声明全局常量时,编译器将分配整个程序中已经存在的内存空间。这种分配风格称为静态分配(static allocation),因为变量分配给内存中在程序整个生命周期内保持不变的位置。
  • 当你在一个函数中声明一个局部变量时,该变量的空间被分配到栈中.调用函数将内存赋给变量,当函数返回时,该内存被释放。这种分配风格称为自动分配(automatic allocation)。

然而,第三种分配内存的方式允许你在程序运行时获取新的内存。 在程序运行时获取新内存的过程称为动态分配(dynamic allocation)。
在使自己能流利的使用C++之前,动态分配是我们需要学习的最重要的技术之一。 在一定程度上,重要性来自于动态分配使数据结构在程序运行时可以根据需要扩展的可能性。 例如,我们之前学习的STL就需要依赖这样的能力,Vector或Map的大小都是没有任何限制的。如果这些类需要更多的内存,他们只需从系统中请求它。
在C ++中,动态分配特别重要,因为与大多数现代语言相比,语言肩负的责任比程序员的责任要高出许多。在C ++中,只知道如何分配内存是不够的。你也必须学习如何在不再需要时释放内存。以正确有序的方式分配和释放内存的过程称为内存管理(memory management)The process of allocating and freeing memory in a disciplined way is called memory management

动态分配与堆

当程序加载到内存中时,它通常只占用可用存储空间的一小部分。像大多数编程语言一样,C ++允许你在应用程序需要更多内存时将一些未使用的存储空间分配给程序。例如,如果在程序运行时需要数组空间,则可以预留部分未分配的内存,其余的用于后续分配。可用于程序的未分配内存池称为堆(heap)。The pool of unallocated memory available to a program is called the heap.
在大多数现代架构中,存储器被布置成使得堆和栈朝着彼此相对的方向增长,如下图所示:

这种策略的优点在于任何一个区域都可以根据需要增长,直到所有可用的内存被填满。
在需要时从堆分配内存的能力证明是非常有用的技术,其广泛应用于编程。例如,所有STL类都使用堆来存储其元素,因为动态分配对于创建可根据需要进行扩展的数据结构至关重要。。 在本系列的后面,你将有机会构建其中一个集合类的简化版本。然而,在这样做之前,你要学习动态分配的基本机制以及过程的工作原理。

new 运算符

C ++使用new运算符从堆中分配内存。在最简单的形式中,new运算符为类型定义一个类型,并为堆中的该类型的变量分配空间。 例如,如果要在堆中分配一个整数,则可以调用:

int *ip = new int;

调用new操作符返回已经设置为保存整数的,堆中的,存储位置的地址。该地址已被设置为保存一个整数。如果堆中的第一个空位于地址1000,则当前栈中的变量ip将被分配该存储器字的地址,如下所示:

在概念上,栈上的局部变量ip指向堆上新分配的word。如果你用箭头而不是地址的方式表示,则该关系将变得更加清晰,如下所示:

一旦在堆内存中分配了整数值的空间,可以通过取消引用指针来引用该整数。例如,可以通过执行语句将整数42存储到该word中:

*ip = 42;

此时的内存状态看起来是这样的:

你还可以使用新的运算符在堆上分配对象。如果你只使用类名,就像:

Rational *rp = new Rational;

C ++在堆为Rational对象分配空间,并调用默认构造函数,在内存中创建以下状态:

如果在类型名称之后提供参数,C ++将调用构造函数的匹配版本。 声明

Rational *rp = new Rational(2, 3);

在内存中创建以下状态:

动态数组

new运算符还可以为堆中的数组分配空间,称为动态数组(dynamic array)。要为动态数组分配空间,我们使用包含在方括号中的所需数量的元素的类型名称。 因此,声明:

double *array = new double[3];

初始化数组,使其指向足够大的三维存储器的连续块,如下所示:

变量array现在是一个功能齐全的数组,其存储位于堆中而不是栈中。你可以为数组的元素分配值,然后将其存储在堆中的相应单元格中。

释放内存

虽然它们总是越来越大,但是计算机存储器系统的尺寸是有限的。结果,堆最终会耗尽空间。发生这种情况时,新的操作员将无法分配所请求大小的块。 不能完成分配请求是一个严重的错误,通常没有程序可以做的恢复。
确保你不会用尽内存的最佳策略是在完成使用后释放堆存储。为此,C ++包括操作符delete,它接受以前由new分配的指针,并释放与该指针相关联的内存。例如,如果你使用分配给ip的存储完全完成,则可以通过调用释放该存储空间:

delete ip;

如果堆内存是数组,则需要在delete关键字之后添加方括号。 因此,当您在前面的示例中完成使用数组时,可以通过执行该语句来释放其内存:

delete[] array;

知道什么时候释放一块内存不是一件容易的事情,特别是随着程序变大。如果一个程序的几个部分共享一些已经在堆中分配的数据结构,任何一个部分都可能无法识别出是否需要该内存。对于只有在产生所需结果之前运行的简单程序,通常是合理的策略来分配所需的内存,而无需再次释放它。然而,这样做可能会造成危险的习惯。如果一些其他程序员试图在一个长期运行的应用程序中使用你的代码,那么你使用内存的不细心的事实将成为一个严重的问题。 因此,良好的做法是确保您在某种程度上释放您分配的任何堆存储。当程序无法释放其未使用的堆存储时,该程序被认为包含内存泄漏(memory leak)。

一些语言(如Java)支持动态分配策略,主动通过内存释放任何不再主动使用的存储。 这个策略叫做垃圾收集(garbage collection)。垃圾收集使得内存管理对于程序员来说非常容易,尽管它也增加了一些成本。扫描整个堆,以确定其使用的部分需要一些时间。更糟糕的是,垃圾收集通常使得无法预测执行特定任务需要多长时间。如果特定的调用结束运行垃圾收集器,通常运行非常快的功能可能会突然花费大量的时间。如果该功能负责一些时间关键任务,应用程序可能无法及时响应。

越来越多的编程语言正在采用垃圾回收作为内存管理策略。 作为一般原则,值得牺牲一些处理时间来节省大量的程序员时间。 处理器毕竟是便宜的,程序员很贵。 但是,C ++可以从早期的时代开始。 无论情况更好还是更糟,C ++的设计人员都决定将堆内存释放在程序员手中,而不是将该任务委托给自动进程。
幸运的是,这些设计师通过为程序员提供不同的内存管理策略来弥补垃圾收集的难度,从而简化了问题。 在C ++中,允许每个类指定当该类的对象消失时会发生什么。在精心设计的C ++应用程序中,每个类都负责自己的堆存储,从而释放客户端几乎不可能的任务,即确切地记住当前活动的堆存储。学习如何有效地使用这一策略是你需要掌握的C ++程序员最重要的技术之一,因此值得特别注意接下来的部分,该部分将详细描述这一方法。

析构函数

从已经看到的示例中可以看出,C ++类通常定义一个或多个初始化对象的构造函数。这样其实每个类也可以定义一个析构函数(destructor),当该类的对象消失时,它将自动调用。这个析构函数可以执行各种清理操作。它可以例如关闭对象打开的任何文件。然而,析构函数最重要的作用是释放由该对象创建的任何堆内存。
在C ++中,析构函数与前面带有波浪符号的类名称相同。因此,如果您需要为名为MyClass的类定义析构函数,则接口中的原型如下所示:

~MyClass();

与构造函数一样,析构函数不包括结果类型。 与构造函数不同,析构函数不能重载。 每个类只有一个析构函数,它不需要参数。
在C ++中,对象以几种不同的方式消失。在大多数情况下,对象在某些函数中被声明为局部变量,这意味着它们被分配在栈上。当函数返回时,这些对象消失。这意味着它们的析构函数在同一时间被自动调用,这使得类定义有机会释放在该对象生命周期内分配的堆内存。在大多数C ++文档中,函数返回时消失的局部变量被称为超出了范围(go out of scope)。
对象也可以在表达式中创建为临时值,即使它们的值永远不会存储在局部变量中。它们的消失是由于析构函数的自动调用。例如:

Rational a(1, 2);
Rational b(1, 3);
Rational c(1, 6);
Rational sum = a + b + c;

当函数返回时,局部变量a,b,c和sum将消失。 然而,最后一行最终将计算Rational 的值5/6作为中间结果,然后再继续计算最终值。 对于生成它们的表达式,执行完成后,这种临时值将消失。如果Rational类有一个析构函数,那么它将被调用,以释放与临时对象关联的内存。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值