第5章 数组与记录
我在第二章介绍数据类型时,提到了Object Pascal中既有内置数据类型又有自定义数据类型。自定义数据类型的一个简单示例是在该章节中介绍的枚举类型。
自定义类型的真正强大体现在更高级的机制中,比如数组、记录和类。在本章中,我将讲述前两个机制,基本上它们的存在可以追溯到 Pascal 的早期定义,但经过多年的改变(现在变得更加强大),它们与早期同名的定义类型几乎没有相似之处。
在本章末尾,我还将简要介绍一些高级的Object Pascal数据类型,比如指针。然而,自定义数据类型的真正威力将在第7章中揭示,届时我们将开始研究类和面向对象编程。
5.1 数组数据类型
数组类型定义了具有特定类型元素的列表。这些列表可以有固定数量的元素(静态数组),也可以有数量可变的元素(动态数组)。通常使用方括号内的索引来访问数组中的一个元素。方括号还可用于定义固定大小数组的数值个数。
Object Pascal
语言支持不同的数组类型,从传统的静态数组到动态数组。建议使用动态数组,特别是在使用移动版本的编译器时。我将首先介绍静态数组,然后重点介绍动态数组。
5.1.1 静态数组
在传统的Pascal语言中,数组是静态的,且大小固定。下面的代码片段就是一个例子,它定义了一个由 24 个整数组成的列表,代表一天 24 小时内的温度:
type
TDayTemperatures = array[1..24] of Integer;
在这个经典的数组定义中,您可以在方括号内使用子界类型,实际上是用两个序数类型的常量定义了一个新的特定子范围类型。这个子界表示数组的有效索引。由于您同时指定了数组索引的上限和下限,所以数组索引不需要以零为基础,这与C、C++、Java和大多数其他语言的情况不同(尽管在Object Pascal中使用基于零的数组也很常见)。此外,Object Pascal中的静态数组索引既可以是数字,也可以是其他序数类型,如字符、枚举类型等。不过,非整数索引相当少见。
注解:有一些语言,比如JavaScript,大量使用关联数组。Object Pascal数组仅限于序数索引,因此不能直接使用字符串作为索引。RTL 中有现成可用的数据结构可以实现字典和其他类似的数据结构提供此类功能。我将在本书第三部分关于泛型的章节中介绍它们。
由于数组索引基于子界,编译器可以检查其范围。无效的常量子界会导致编译时错误;而在运行时使用的超出范围的索引会导致运行时错误,但前提是必须启用相应的编译器选项。
注解:这是IDE的Project Options对话框的Compiling页面中Runtime errors组的Range checking选项。我在第二章的“子范围类型”一节中已经提到过这个选项。
使用上述数组定义,您可以将TDayTemperatures类型的DayTemp1变量的值设置为以下方式(就像我在ArraysTest示例中所做的那样,以下代码片段从中提取):
type
TDayTemperatures = array[1..24] of Integer;
var
DayTemp1: TDayTemperatures;
begin
DayTemp1[1] := 54;
DayTemp1[2] := 52;
...
DayTemp1[24] := 66;
// The following line causes:
// E1012 Constant expression violates subrange bounds
// DayTemp1[25] := 67;
end;
鉴于数组的性质,对数组进行操作的标准方法是使用 for 循环。下面是一个循环示例,用于显示一天中的所有温度:
var
I: Integer;
begin
for I := 1 to 24 do
Show(I.ToString + ': ' + DayTemp1[I].ToString);
end;
虽然这段代码可以运行,但硬编码数组边界(1和24)并不是一种理想的方式,因为数组定义本身可能会随时间变化,而您可能希望转而使用动态数组。
5.1.2 数组大小和边界
在使用数组时,可以使用标准的 Low 和 High 函数来测试边界,这两个函数会返回下届和上届。在对数组进行操作时,强烈建议使用 Low 和 High 函数,尤其是在循环中,因为它可以使代码独立于数组的当前范围(可能是从 0 到数组长度减 1,也可能是从 1 开始并达到数组长度,还可能是其他子界定义)。如果以后更改了数组索引的声明范围,使用 Low 和 High 的代码将自动生效,因为它们的值来自数组定义。相反,如果你编写一个硬编码数组范围的循环,那么当数组范围发生变化时,你就必须更新循环的代码。因此,使用 Low 和 High 不仅可以使代码更易于维护,而且更加可靠。
注解:顺便提一下,在静态数组中使用 Low 和 High 不会产生运行时开销。它们在编译时被解析为常量表达式,而不是实际的函数调用。这种表达式和函数调用的编译时解析也适用于许多其他系统函数。
另一个相关函数是 Length,它返回数组的元素个数。在下面的代码中,我将这三个函数组合在一起,计算并显示当天的平均温度:
var
I: Integer;
Total: Integer;
begin
Total := 0;
for I := Low(DayTemp1) to High(DayTemp1) do
Inc(Total, DayTemp1[I]);
Show((Total / Length(DayTemp1)).ToString);
end;
这段代码也是ArraysTest示例的一部分。
5.1.3 多维静态数组
数组可以有多个维度,表示矩阵或立方体,而不仅仅是一个列表。以下是两个示例定义:
type
TAllMonthTemps = array[1..24, 1..31] of Integer;
TAllYearTemps = array[1..24, 1..31, 1..12] of Integer;
您可以这样访问一个元素:
var
AllMonth1: TAllMonthTemps;
AllYear1: TAllYearTemps;
begin
AllMonth1[13, 30] := 55; // 小时,日期
AllYear1[13, 30, 8] := 55; // 小时,日期,月份
end;
注解:静态数组随即占用了大量内存(上面的情况是在栈上),因此应避免这样做。
AllYear1
变量需要8,928个整数,每个整数占用4个字节,总共近35KB。在全局内存或栈中分配如此大的内存块(如演示代码中)确实是个错误。相比之下,动态数组使用堆内存,在内存分配和管理方面更具灵活性。
由于这两种数组类型基于相同的核心类型,所以最好使用前面的数据类型来声明它们,如下所示:
type
TMonthTemps = array[1..31] of TDayTemperatures;
TYearTemps = array[1..12] of TMonthTemps;
这个声明颠倒了上面介绍的索引顺序,但它还允许在变量之间进行整块的赋值。让我们看看如何给单个元素赋值:
Month1[30][14] := 44; // 天,小时
Month1[30, 13] := 55; // 天,小时
Year1[8, 30, 13] := 55; // 月份,天,小时
使用中间类型的重要性在于,只有当数组引用了相同的类型名称(即完全相同的类型定义)时,它们才是类型兼容的,而不是它们的类型定义碰巧引用相同的实现。这种类型兼容性规则对Object Pascal中的所有类型都是一样的,只有一些特定的例外。
例如,以下语句将一个月的温度复制到年的第三个月:
Year1[3] := Month1;
相比之下,基于独立的数组定义的类似语句(它们不是类型兼容的):
AllYear1[3] := AllMonth1;
将导致错误:
Error: Incompatible types: 'array[1..31] of array[1..12] of Integer' and 'TAllMonthTemps'
正如我所提到的,静态数组存在内存管理问题,特别是当你想将其作为参数传递或只分配大型数组的一部分时。此外,在数组变量的生命周期内,无法调整它们的大小。这就是为什么我们更倾向于使用动态数组的原因,即使动态数组需要一些额外的管理,比如需要分配内存。