第13章 动态对象创建
有时我们能知道程序中对象的确切数量、类型和生命期。但情况并不总是这样。
空中交通指挥系统将会需要处理多少架飞机?一个CAD系统将会需要多少个形体?在一个网络中将会有多少个节点?
为了解决这个普通的编程问题,在运行时可以创建和销毁对象是最基本的要求。当然,C早就提供了动态内存分配(dynamic memory allocation)函数malloc()和free()(以及malloc()的变体),这些函数在运行时从堆(也称自由内存)中分配存储单元。
然而,在C++中这些函数将不能很好地运行。因为构造函数不允许我们向它传递内存地址来进行初始化。如果那么做了,我们可能:
1)忘记了。则在C++中有保证的对象初始化将会难以保证。
2)期望发生正确的事,但在对对象进行初始化之前意外地对对象进行了某种操作。
3)把错误规模的对象传递给它。
当然,即使我们正确地完成了每件事,修改我们程序的人也容易犯同样的错误。不正确的初始化要对大部分编程问题承担责任,所以在堆上创建对象时确保构造函数调用是特别重要的。
C++是如何保证正确的初始化和清理,又允许我们在堆上动态创建对象呢?
答案是,使动态对象创建成为语言的核心。malloc()和free()是库函数,因此不在编译器控制范围之内。然而,如果我们有一个完成动态内存分配及初始化组合动作的运算符和另一个完成清理及释放内存组合动作的运算符,编译器仍可以保证所有对象的构造函数和析构函数都会被调用。
在本章中,我们将明白C++的new和delete是如何通过在堆上安全地创建对象来出色地解决这个问题。
13.1 对象创建
当创建一个C++对象时,会发生两件事:
1)为对象分配内存。
2)调用构造函数来初始化那个内存。
到目前为止,应该确保步骤2一定发生。C++强迫这样做是因为未初始化的对象是程序出错的主要原因。对象在哪里和如何被创建无关紧要—构造函数总是需要被调用。
然而,步骤1可以用几种方式或在可选择的时间发生:
1)在静态存储区域,存储空间在程序开始之前就可以分配。这个存储空间在程序的整个运行期间都存在。
2)无论何时到达一个特殊的执行点(左大括号)时,存储单元都可以在栈上被创建。出了执行点(右大括号),这个存储单元自动被释放。这些栈分配运算内置于处理器的指令集中,非常有效。然而,在写程序的时候,必须知道需要多少个存储单元,以便编译器生成正确的指令。
3)存储单元也可以从一块称为堆(也被称为自由存储单元)的地方分配。这被称为动态内存分配。在运行时调用程序分配这些内存。这意味着可以在任何时候决定分配内存及分配多少内存。当然也需负责决定何时释放内存。这块内存的生存期由我们选择决定—而不受范围决定。
这三个区域经常被放在一块连续的物理存储单元里:静态内存、栈和堆(由编译器的开发者决定它们的顺序),但没有一定的规则。堆栈可以在特定的地方,堆的实现可以通过调用由运算系统分配的一块存储单元。这三件事无需程序设计者来完成。当申请内存的时候,只要知道它们在哪里就行了。
13.1.1 C从堆中获取存储单元的方法
为了在运行时动态分配内存,C在它的标准库函数中提供了一些函数:从堆中申请内存的函数malloc()以及它的变种calloc()和realloc()、释放内存返回给堆的函数free()。这些函数是有效的但较原始的,需要编程人员理解和小心使用。为了使用C的动态内存分配函数在堆上创建一个类的实例,我们必须这样做:
在下面这行代码中,使用了malloc()为对象分配内存:
这里用户必须决定对象的长度(这也是程序出错原因之一)。由于malloc()只是分配了一块内存而不是生成一个对象,所以它返回了一个void类型指针。而C++不允许将一个void类型指针赋予任何其他指针,所以必须做类型转换。
因为malloc()可能找不到可分配的内存(在这种情况下它返回0),所以必须检查返回的指针以确保内存分配成功。
但这一行最易出现问题:
用户在使用对象之前必须记住对它初始化。注意构造函数没有被使用,这是因为构造函数不能被显式地调用—它是在对象创建时由编译器调用[1]。问题是现在用户可能在使用对象时忘记执行初始化,因此这又是一个程序出错的主要来源。
许多程序设计者发现C的动态内存分配函数太复杂,容易令人混淆。所以,C程序设计者常常在静态内存区域使用虚拟内存机制分配很大的变量数组以避免使用动态内存分配。为了在C++中使得一般的程序员可以安全使用库函数而不费力,所以C的动态内存方法是不可接受的。
[1]这里,称为定位new(placement new)的特殊语法可用来在一块预先分配好的内存上调用构造函数。这将在后面的章节中加以介绍。