本节书摘来自异步社区出版社《c++面向对象高效编程(第2版)》一书中的第4章,第4.2节,作者: 【美】kayshav dattatri,更多章节内容可以访问云栖社区“异步社区”公众号查看。
c++面向对象高效编程(第2版)
在我们讨论无用单元收集1(garbage collection)之前,先了解一下何为无用单元(garbage),何为悬挂引用(dangling reference)。
所谓无用单元(garbage),是一块存储区(或资源),该存储区虽然是程序(或进程)的一部分,但是在程序中却不可再对其引用。按照c++的规定,我们可以说,无用单元是程序中没有指针指向的某些资源。以下是一个示例:
main()
{
char *p;
char *q;
p = new char[1024]; // 分配1k字符的动态数组
// ... 使用它
q = p; // 指针别名(pointer aliasing)
delete [] p;
p = 0;
// 现在q是一个悬挂引用,如果试图 *q = ‘a’,将导致程序崩溃。
}<code>`</code>
如果试图访问q所指向的内存,将引发严重的问题。在该例中,指针q称为悬挂引用。指针别名(即多个指针持有相同的地址)通常会导致悬挂引用。与无用单元相比,悬挂引用对于程序而言是致命的,因为它必定导致严重破坏(大多数可能是运行时崩溃)。
这两个问题(无用单元和悬挂引用)都是操纵指针和指针别名直接导致的结果。由于程序员复制了地址,但尚未理解复制地址的语义(和后果),才引发了这些问题。这不是oop才有的新问题,但oop让这些问题的影响更加严重。
smalltalk:
一些语言提供自动的无用单元收集。在smalltalk环境下工作的程序员根本无需担心无用单元,因为无用单元收集在smalltalk中是自动进行的。语言会跟踪对内存的引用,当不再引用某块内存时,语言便自动释放它们。
eiffel:
eiffel以辅助程序的形式提供自动的无用单元收集,该程序定期在后台运行,用于收集所有不可再访问的单元。
c++:
c++不提供自动的无用单元收集机制。它支持所有类型的指针变量。这就把无用单元收集的重任留给了程序员。一般而言,这是存储区管理的问题。在c++中,无用单元收集是一个研究课题。也许在不久的将来,c++也会有自动的无用单元收集。
由此可见,只要不让程序员创建持有内存区域地址的指针类型,几乎就可以避免悬挂引用的问题。在eiffel、smalltalk和java中就是这种的情况。
你可能觉得奇怪,无用单元收集和悬挂引用在其他类型的编程中也会出现,为何要将这两个问题作为oop中的特殊问题?请继续往下读。在面向过程编程系统中,没有对象的概念,也不会频繁地进行内存分配(和释放)。然而,在oop中,一切皆为对象,而且绝大多数大型对象都要分配资源。在我们感兴趣的面向对象系统中,时刻都有成百上千的对象,对象不断地被创建、复制和销毁。而且,可以按不同的方式,甚至动态地创建对象。因此,作为类的实现者,不仅要充分理解无用单元收集问题,还要额外注意存储区的管理。
语言(自动或程序员实现)支持的无用单元收集类型,和语言本身的设计原理有较大的关系。提供自动无用单元收集的语言(如eiffel和smalltalk),实际上是基于引用的语言。在基于引用的语言中,每个对象只是一个引用。当创建对象时,事实上是创建了一个引用,该引用持有真正对象的地址,此地址被保存在别处。这使得复制和共享对象非常容易和迅速。但是,另一方面,这也导致安全性较低。因为通过使用对象的引用,可能会意外地修改该对象。
然而,c++是一种基于值的语言(c也是)。在该语言中,一切(对象和基本类型)皆为值。每个对象都是一个真正的对象,不是一个指向储存在别处的对象的指针。c++对待类和基本类型一样,这是该语言中的统一模型。
eiffel使用双重方案。在eiffel中,所有对象都基于引用,但所有基本类型都基于值。新对象获得自己所有基本实例变量的副本,但是,在新对象中只能包含对对象的引用。在其他地方也提到,引用要么是void,要么是一个对有效对象的引用。
另外,smalltalk对待对象和基本类型一致。在该语言中,一切皆为对象,所有的基本类型也是对象。这使得语言易于理解,无需区分对象和基本类型的不同。
以下的示例说明了多种语言间的不同。回顾tcar类的例子: