数组类型究竟是如何存储在 C 中的?

How exactly array type is stored in C?

所以我一直在阅读 Brian W. Kernighan 和 Dennis M. Ritchie 的"The C Programming Language",在我读到数组到指针部分之前一切都很清楚。我们可以读到的第一件事是,根据定义,a[i] 被 C 转换为 *(a+i)。好的,这很清楚也合乎逻辑。接下来的事情是,当我们将数组作为函数参数传递时,您实际上将指针传递给该数组中的第一个元素。然后我们发现我们可以将整数添加到这样的指针,甚至指向数组后第一个元素的指针也是有效的。但是后来写到我们只能在同一个数组中减去指针。
那么如果这两个指针指向同一个数组,C'know'怎么办呢?是否有一些与数组相关的元信息?还是仅仅意味着这是未定义的行为,编译器甚至不会生成警告?数组是作为数组类型大小的普通值存储在内存中的,一个接一个,还是有其他东西?

... it's written that we can subtract pointers only in the same array.
So how does C 'know' if these two pointers point to the same array?

C 不知道。确定限制是程序员的责任。

int arr[100];
int *p1 = arr + 30;
int *p2 = arr + 50;
//both p1 and p2 point into arr
p2 - p1; //ok
p1 - p2; //ok
int *p3 = &((int)42); // ignore the C99 compound literal
//p3 does not point into arr
p3 - p1; //nope!

C 标准只为同一个数组中的两个指针定义减法的一个原因是某些(大多数是旧的)C 实现使用一种寻址形式,其中地址由 base 组成address加上一个offset,不同的数组可能有不同的基址。

在某些机器中,内存中的完整地址可能有一个基数,它是一些段或其他某种类型的块,以及一个偏移量,它是页内的多个字节。这样做是因为,例如,一些早期的硬件可以处理 16 位的数据,并且被设计为使用 16 位地址,但是扩展相同体系结构的后来版本的硬件将具有更大的地址,但仍将使用 16-位数据,以便与以前的软件保持一定的兼容性。所以较新的硬件可能有一个 22 位地址 space。仅使用 16 位地址的旧软件仍然表现相同,但较新的软件可以使用额外的数据来指定不同的基地址,从而访问 22 位地址中的所有内存 space.

在这样的系统中,基址 b 和偏移量 o 的组合可能指的是内存地址 64•b + o。这可以访问地址 space 的完整 22 位 — b=65535 和 o=63,我们有 64•b + o = 64•65535 + 63 = 4,194,303 = 222−1.

观察表单中的许多位置可以通过多个地址访问。例如b=17,o=40指的是与b=16相同的位置, o=104 和 b=15,o=168。尽管生成 22 位地址的公式可以设计为 65536•b + o,并且这会给出每个内存位置一个唯一的地址,使用重叠公式是因为它使程序员可以灵活地选择他们的基地。回想一下,这些机器主要是围绕使用 16 位数据而设计的。使用非重叠地址方案,无论何时进行地址运算,您都必须计算基数和偏移量。使用重叠地址方案,您可以为您正在使用的数组选择一个基数,然后进行任何地址运算只需要计算偏移量部分。

通过为数组设置一个基地址,然后仅对偏移量部分进行运算,此架构的C 实现可以轻松支持多达65536 个数组的数组。例如,如果我们有一个包含 1000 int 的数组 A,并且它是从内存位置 78,976(等于 1234•64)开始分配的,我们可以设置 b 到 1234 并用从 0 到 1998 的偏移量索引数组(999•2,因为每个 int 在这个 C 实现中是两个字节)。

然后,如果我们有一个指针 p 指向 A[125],它用 (1234, 250) 表示,指向以 1234 为基数的偏移量 250。如果 q指向A[55],用(1234, 110)表示。为了减去这些指针,我们忽略基数,减去偏移量,然后除以一个元素的大小,所以结果是 (250-110)/2 = 70.

现在,如果你有一个指针 r 指向其他数组 B 中的元素 13,它将有一个不同的基数,比如 2345。所以 r 会用 (2345, 26) 表示。然后,要从 p 中减去 r,我们需要从 (1234, 250) 中减去 (2345, 26)。在这种情况下,您不能忽略碱基;简单地使用偏移量会得到 (250−26)/2 = 112,但这些项目不是 112 个元素(或 224 字节)分开的。

可以更改编译器以通过减去基数、乘以 64 并将其添加到偏移量的差值来进行数学运算。但是它正在做数学减去指针运算的预期用途中完全不需要的指针。所以 C 标准委员会决定不应该要求编译器支持这个,指定的方式是说当你减去指向不同数组中的元素的指针时,行为没有定义。