《C++面向对象高效编程(第2版)》——4.2 无用单元收集问题

简介:

本节书摘来自异步社区出版社《C++面向对象高效编程(第2版)》一书中的第4章,第4.2节,作者: 【美】Kayshav Dattatri,更多章节内容可以访问云栖社区“异步社区”公众号查看。

4.2 无用单元收集问题

C++面向对象高效编程(第2版)
在我们讨论无用单元收集1(garbage collection)之前,先了解一下何为无用单元(garbage),何为悬挂引用(dangling reference)。

4.2.1 无用单元

所谓无用单元(garbage),是一块存储区(或资源),该存储区虽然是程序(或进程)的一部分,但是在程序中却不可再对其引用。按照C++的规定,我们可以说,无用单元是程序中没有指针指向的某些资源。以下是一个示例:

main()
{
   char* p = new char[1000]; // 分配一个包含1000个字符的动态数组
   char* q = new char[1000]; // 另一块动态内存
   // 使用p和q进行一些操作的代码
   p = q;   // 将q赋值给p,覆盖p中的地址
      /* p所指向的1000个字符的存储区会发生什么?此时,p和q指向相同的区域,
没有指针指向之前p指向的旧存储区!该储存区还在,仍然占用着空间,
但程序却不可访问(使用)该区域。这样的区域则称为无用单元。*/
}```
现在,在main()中为p分配的内存便是无用单元,因为它仍然是正在运行程序的一部分,但是,所有对它的引用都被销毁了。

无用单元不会立即对程序造成损害,但它将逐渐消耗内存,最终耗尽内存导致系统中止运行。在某些情况下,由于若干原因2,还可能导致无法停止程序。随着越来越多的无用单元被创建,系统运行得越来越慢。定期进行无用单元收集是资源回收的有效途径。当然,无用单元收集并不是毫无代价的,因为必须定期地运行(自动或手动地)一个收集所有无用单元、并将其返回自由池(free pool)中的程序。而且,不一定能收集完所有的无用单元。

###4.2.2 悬挂引用
当指针所指向的内存被删除,但程序员认为被删除内存的地址仍有效时,就会产生悬挂引用(dangling reference)。例如:

main()
{
   char *p;
   char *q;
   p = new char[1024]; // 分配1k字符的动态数组
   // ... 使用它
   q = p;  // 指针别名(pointer aliasing)
   delete [] p;
   p = 0;
   // 现在q是一个悬挂引用,如果试图 *q = ‘A’,将导致程序崩溃。
}`
如果试图访问q所指向的内存,将引发严重的问题。在该例中,指针q称为悬挂引用。指针别名(即多个指针持有相同的地址)通常会导致悬挂引用。与无用单元相比,悬挂引用对于程序而言是致命的,因为它必定导致严重破坏(大多数可能是运行时崩溃)。

4.2.3 无用单元收集和悬挂引用的补救

这两个问题(无用单元和悬挂引用)都是操纵指针和指针别名直接导致的结果。由于程序员复制了地址,但尚未理解复制地址的语义(和后果),才引发了这些问题。这不是OOP才有的新问题,但OOP让这些问题的影响更加严重。

SMALLTALK:

一些语言提供自动的无用单元收集。在Smalltalk环境下工作的程序员根本无需担心无用单元,因为无用单元收集在Smalltalk中是自动进行的。语言会跟踪对内存的引用,当不再引用某块内存时,语言便自动释放它们。

EIFFEL:

Eiffel以辅助程序的形式提供自动的无用单元收集,该程序定期在后台运行,用于收集所有不可再访问的单元。

C++:

C++不提供自动的无用单元收集机制。它支持所有类型的指针变量。这就把无用单元收集的重任留给了程序员。一般而言,这是存储区管理的问题。在C++中,无用单元收集是一个研究课题。也许在不久的将来,C++也会有自动的无用单元收集。

由此可见,只要不让程序员创建持有内存区域地址的指针类型,几乎就可以避免悬挂引用的问题。在Eiffel、Smalltalk和Java中就是这种的情况。

你可能觉得奇怪,无用单元收集和悬挂引用在其他类型的编程中也会出现,为何要将这两个问题作为OOP中的特殊问题?请继续往下读。在面向过程编程系统中,没有对象的概念,也不会频繁地进行内存分配(和释放)。然而,在OOP中,一切皆为对象,而且绝大多数大型对象都要分配资源。在我们感兴趣的面向对象系统中,时刻都有成百上千的对象,对象不断地被创建、复制和销毁。而且,可以按不同的方式,甚至动态地创建对象。因此,作为类的实现者,不仅要充分理解无用单元收集问题,还要额外注意存储区的管理。

4.2.4 无用单元收集和语言设计

语言(自动或程序员实现)支持的无用单元收集类型,和语言本身的设计原理有较大的关系。提供自动无用单元收集的语言(如Eiffel和Smalltalk),实际上是基于引用的语言。在基于引用的语言中,每个对象只是一个引用。当创建对象时,事实上是创建了一个引用,该引用持有真正对象的地址,此地址被保存在别处。这使得复制和共享对象非常容易和迅速。但是,另一方面,这也导致安全性较低。因为通过使用对象的引用,可能会意外地修改该对象。

然而,C++是一种基于值的语言(C也是)。在该语言中,一切(对象和基本类型)皆为值。每个对象都是一个真正的对象,不是一个指向储存在别处的对象的指针。C++对待类和基本类型一样,这是该语言中的统一模型。

Eiffel:

Eiffel使用双重方案。在Eiffel中,所有对象都基于引用,但所有基本类型都基于值。新对象获得自己所有基本实例变量的副本,但是,在新对象中只能包含对对象的引用。在其他地方也提到,引用要么是void,要么是一个对有效对象的引用。

Smalltalk:

另外,Smalltalk对待对象和基本类型一致。在该语言中,一切皆为对象,所有的基本类型也是对象。这使得语言易于理解,无需区分对象和基本类型的不同。

以下的示例说明了多种语言间的不同。回顾TCar类的例子:

class TCar {
     private:
         unsigned   _weight;  // 汽车的重量,英镑
         short   _driveType; // 四轮驱动或两轮驱动
         TPerson  _owner;   // 谁拥有这辆车?
         // 略去其他不相关的细节
      public:
         TCar(const char name[], const char address[],
             unsigned long ssn, unsigned long ownerBirthDate,
             unsigned weight = 900, short driveType = 2 );
};```
为什么无用单元控制非常重要

无用单元控制在软件设计中是一个非常重要的问题,因为它会影响应用程序的整体性能。即使是不太重要的应用程序,在持续运行很长一段时间(数周甚至数月)未停止,内存泄漏也会导致严重的问题。随着越来越多的内存成为无用单元,应用程序(和整个系统)的性能逐渐降低,导致越来越多的虚拟内存分页(paging)活动。最终,由于分页文件被占满,整个系统只能暂停。

并非所有的程序都能随意暂停。暂停某些重要的应用程序任务(例如,核电站控制程序),将导致灾难性的后果。如果监视和控制反应堆的软件持续地泄漏内存,该系统最终将停止运行,这会给周围的居民带来毁灭性的灾难。

在嵌入式系统中,虽然不像大型的操作系统需要进行虚拟内存管理,但内存泄漏也会引发事故(如控制汽车燃料供给系统的电脑)。如果用于驱动燃料喷嘴的软件,由于发生内存泄漏,在汽车行驶数百公里后,停止运行会怎样?汽车会突然停在路中间,但是之前不会发出任何警告。

自动的无用单元收集方案,看上去像是解决大多数类似问题的良方,但事实并非如此。在无用单元收集程序运行时,应用程序必须暂停运行。暂停的时间可能短至几微秒,或长达数百毫秒。想象一下,照相机已对焦,准备捕捉精彩的瞬间。正当按下快门时,无用单元收集程序运行,用于拍照的软件暂停。在无用单元收集完成后,精彩动作的瞬间已经过去了,未拍到合适的图像。

编写绝不泄漏任何资源(内存、文件句柄(file handle)、网络套接字(network socket)等)的软件,应成为每个软件设计者和开发者的共同目标。
虽然我们在C++中声明了TCar类,但了解一下Eiffel、Smalltalk和C++中的对象结构也非常有趣。假设有一个TCar类的对象,对象名为Jeep,driveType = 4(即四轮驱动),重量为800磅(见图4-3)。

如图4-3所示,3种语言的模型大不相同。在C++中创建的对象,是真正的对象。因此,以下声明:

`TCar jeep(“Einstein”, “Princeton, NJ”, 618552272, “12-11-1879”, 800,4);`
将创建一个真正的对象,如图4-3所示。

Eiffel中的声明:

`jeep : TCar;  - Eiffel声明`
不会创建任何新对象,它只创建一个引用。下面的语句:

`jeep.!!make;  - 仍然是Eiffel代码`
![image](https://yqfile.alicdn.com/7fbf43c1ed9b09ef07712a948b3753b2e527a823.png)
图4-3

将创建一个对象,并在运行时初始化它。现在,jeep必定是对内存中某个对象的引用。

Eiffel:

在Eiffel中,通常由make操作来创建对象。一个类可以在creation关键字下声明很多这样的创建例程。和任何其他例程一样,访问控制也可应用于该例程上。!!前缀意味着创建一个新对象。如果在无!!前缀的情况下调用make,它会重置现有对象中的值。

在Smalltalk中,以下声明:
`
jeep new TCar “Smalltalk code”`
将创建一个TCar类的新对象,且所有实例变量都设置为nil。

从以上这些示例中不难发现,每种语言创建对象的方式都不同。而且,对象的表示也有所不同。

###4.2.5 在C++中何时产生无用单元
本节将详细介绍哪些操作会产生无用单元。当对象所分配的资源未释放,但该资源不可再用、不可再访问时,便产生了无用单元。很多情况下都会导致资源不可访问(或不能使用),即资源在程序的作用域内不可再用。例如:

(1)从函数退出时,在函数内部创建的所有局部变量(包括对象)以及按值传递的所有参数都不可访问。

(2)从块退出时,在块内部声明的所有局部变量(包括对象)都不可访问3。

(3)任何复杂表达式包含的临时变量,在不需要时必须全部予以销毁,否则它们将成为无用单元。

(4)任何动态分配的对象,在不需要时必须由程序员显式地销毁。

我们已经在面向过程编程中熟悉了这些情况。但是,在面向对象编程中,情况会更加复杂。因为对象不是简单的变量,其中甚至还包含其他的对象。被分配的资源作为对象的一部分,当该对象不可再用时,释放已分配的资源(包括其他对象)非常重要。这是一个递归过程,因为对象可能包含其他对象,而其他对象也可能包含另外的对象,如此以至无限多的对象。对象内部的所有其他对象所分配的资源,在不需要时必须及时予以释放。

还有一种情况也会产生无用单元,即在复制对象和给对象赋值时。我们稍后将详细讨论这个问题。

###4.2.6 对象何时获得资源
对象被创建或被客户传递时,可通过动态分配获得资源。进一步而言,在对象的生存期内,可通过该对象调用不同的方法获得资源。例如,在TPerson类中,为了储存_name中的字符,要在构造函数内分配内存。类似地,第3章中提到的TIntStack类对象,要为储存元素而分配内存。特别是提供储存机制的类(如List或Queue),在对象的生存期内,有可能根据需要随时分配资源。

对象还可能因为采用语义(第3章中讨论过)而获得资源。在这种情况下,某对象分配了资源,但是将这些资源的所有权转移给另外的对象,后者负责在不需要这些资源(或资源不可再访问)时释放它们。

1译者注:根据GB/T 11457-2006 软件工程术语国家标准,garbage collection译为“无用单元收集”,也称为“垃圾回收”。
2想象一款运行生命支持系统的软件。
3在C++中,块就是一对花括号{ }内的一系列指令,它类似一个微型函数。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。
相关文章
|
7天前
|
C++
面向对象的C++题目以及解法2
面向对象的C++题目以及解法2
13 1
|
17天前
|
存储 人工智能 机器人
【C++面向对象】C++图书管理系统 (源码)【独一无二】
【C++面向对象】C++图书管理系统 (源码)【独一无二】
|
22天前
|
存储 人工智能 BI
【C++面向对象】C++银行卡管理系统(源码+论文)【独一无二】
【C++面向对象】C++银行卡管理系统(源码+论文)【独一无二】
|
1月前
|
算法 IDE Java
【软件设计师备考 专题 】面向对象程序设计语言:C++、Java、Visual Basic和Visual C++
【软件设计师备考 专题 】面向对象程序设计语言:C++、Java、Visual Basic和Visual C++
41 0
|
1月前
|
设计模式 负载均衡 算法
C/C++发布-订阅者模式世界:揭秘高效编程的秘诀
C/C++发布-订阅者模式世界:揭秘高效编程的秘诀
70 1
|
1月前
|
存储 安全 编译器
C++ std::move以及右值引用全面解析:从基础到实战,掌握现代C++高效编程
C++ std::move以及右值引用全面解析:从基础到实战,掌握现代C++高效编程
79 0
|
2月前
|
C语言 C++
【c++】什么是面向对象
【c++】什么是面向对象
【c++】什么是面向对象
|
2月前
|
存储 数据安全/隐私保护 C++
基于C++的面向对象程序设计:类与对象的深入剖析
基于C++的面向对象程序设计:类与对象的深入剖析
41 1
|
3天前
|
存储 编译器 C语言
c++的学习之路:5、类和对象(1)
c++的学习之路:5、类和对象(1)
19 0
|
3天前
|
C++
c++的学习之路:7、类和对象(3)
c++的学习之路:7、类和对象(3)
19 0

热门文章

最新文章