为什么C数组不跟踪其长度?


77

不使用数组in显式存储数组长度的背后原因是什么C

从我的角度来看,这样做的原因很多,但并没有太多支持标准(C89)的理由。例如:

  1. 在缓冲区中具有可用长度可以防止缓冲区溢出。
  2. Java风格arr.length既清晰又避免了程序员int在处理多个数组时必须在堆栈上维护多个s的情况。
  3. 功能参数变得更有说服力。

但我认为,最有动力的原因是通常情况下,不保留长度就不会节省空间。我敢说,数组的大多数使用都涉及动态分配。没错,在某些情况下,人们会使用在堆栈上分配的数组,但这只是一个函数调用*-堆栈可以额外处理4或8个字节。

由于堆管理器无论如何都必须跟踪动态分配的阵列所用的空闲块大小,所以为什么不使该信息可用(并添加在编译时检查的附加规则,除非有人愿意,否则不能显式地操纵长度)喜欢在脚上开枪)。

我能想到的对对方的唯一的事情是没有长度跟踪可能已经做出简单的编译器,但不是要简单得多。

*从技术上讲,可以用自动存储的数组编写某种递归函数,在这种(非常复杂的)情况下,存储长度确实可以有效地占用更多空间。


6
我猜想可能会引起争议,当C包括使用struct作为参数和返回值类型时,它应该已经为“ vectors”(或任何名称)包括了语法糖,该语法糖将在具有长度的结构,数组或指向数组的指针的下面。对这种通用结构的语言级别支持(当作为单独的参数而不是单个结构传递时)也将节省无数的错误并简化标准库。
海德2014年

3
您可能还会发现为什么Pascal不是我最喜欢的编程语言第2.1节,很有见地。

34
尽管所有其他答案都有一些有趣的观点,但我认为最重要的是编写C语言,以便汇编语言程序员能够更轻松地编写代码并具有可移植性。考虑到这一点,将数组长度自动存储在数组中将是一件麻烦事,而不是缺点(就像其他一些很好的涂糖的愿望一样)。如今,这些功能看起来不错,但那时候确实很难将程序或数据的另一个字节压缩到系统中。内存的浪费使用将严重限制C的采用。
Dunk 2014年

6
您答案的真实部分已经用我本来已经回答过很多次,但是我可以提取一个不同的观点:“为什么不能以malloc()可移植的方式请求编辑区域的大小?” 那件事让我想知道好几次了。
glglgl 2014年

5
投票重新开放。即使某处只是“ K&R没想到”,也存在某些原因。
Telastyn

Answers:


106

C数组确实会跟踪其长度,因为数组长度是静态属性:

int xs[42];  /* a 42-element array */

通常,您不能查询该长度,但是因为它是静态的,所以您不需要这样做-只需XS_LENGTH为该长度声明一个宏就可以了。

更重要的问题是C数组隐式降级为指针,例如,当传递给函数时。这确实是有道理的,并且允许使用一些不错的低级技巧,但是会丢失有关数组长度的信息。因此,一个更好的问题是,为什么在设计C时会隐式地降低指针的质量。

另一件事是,指针不需要存储器地址本身的任何存储。C允许我们将整数转换为指针,将指针转换为其他指针,并将指针视为数组。在这样做的时候,C不足以疯狂地制造某种数组长度,但似乎相信Spiderman的座右铭:凭借强大的功能,程序员有望担负起跟踪长度和溢出的重大责任。


13
我想您是想说,如果我没记错的话,C编译器会跟踪静态数组的长度。但这对仅获得指针的函数没有好处。
VF1 2014年

25
@ VF1是的。但是重要的是,数组和指针在C中是不同的东西。假设您没有使用任何编译器扩展,通常就不能将数组本身传递给函数,但是可以传递指针,并像建立数组一样对指针进行索引。您实际上在抱怨指针没有附加长度。您应该抱怨数组不能作为函数参数传递,或者数组隐式降级为指针。
2014年

37
“您通常不能查询此长度”-实际上您可以,它是sizeof运算符-如果int的长度为4个字节,则sizeof(xs)将返回168。要获得42,请执行以下操作:sizeof(xs)/ sizeof(int)
tcrosley14年

15
@tcrosley这仅在数组声明的范围内有效-尝试将xs作为参数传递给另一个函数,然后查看sizeof(xs)给您带来什么...
Gwyn Evans

26
再次@GwynEvans:指针不是数组。因此,如果“将数组作为参数传递给另一个函数”,则不是传递数组而是传递指针。声称sizeof(xs)这里xs是一个数组将东西在另一个不同的范围是公然假的,因为C的设计不允许阵列离开其范围。如果sizeof(xs)这里xs是一个数组是不同的sizeof(xs)地方xs是一个指针,这并不令人吃惊,因为你是用橘子在比较苹果
2014年

38

其中很大一部分与当时可用的计算机有关。不仅已编译的程序必须在有限资源的计算机上运行,​​而且,更重要的是,编译器本身也必须在这些计算机上运行。汤普森(Thompson)开发C时,他使用的是PDP-7,带有8k RAM。在实际的机器代码中没有直接模拟的复杂语言功能根本没有包含在该语言中。

仔细阅读C历史可以对上面的内容有更多的了解,但这并不完全是由于它们所具有的机器限制:

此外,语言(C)具有强大的能力来描述重要概念,例如,向量的长度在运行时会有所变化,仅带有一些基本规则和约定。...将C的方法与两种几乎同时代的语言Algol 68和Pascal [Jensen 74]进行比较很有趣。Algol 68中的数组具有固定范围,或者是“灵活的”:在语言定义和编译器中都需要相当大的机制来容纳灵活的数组(并非所有编译器都完全实现它们。)原始的Pascal仅具有固定大小数组和字符串,这被证明是局限的[Kernighan 81]。

C数组本质上更强大。给它们添加边界会限制程序员可以使用它们。这样的限制对于程序员可能有用,但也必然是限制。


4
这几乎解决了最初的问题。那就是事实,在检查程序员在做什么时,故意使C保持“轻触”,这是使其对编写操作系统有吸引力的一部分。
ClickRick

5
很棒的链接,他们还显式更改了存储字符串的长度以使用定界符to avoid the limitation on the length of a string caused by holding the count in an 8- or 9-bit slot, and partly because maintaining the count seemed, in our experience, less convenient than using a terminator-对此非常有用:-)
Voo

5
无终止数组也适用于C的裸机方法。请记住,K&R C书不到300页,其中包含语言教程,参考资料和标准调用列表。我的正则表达式O'Reilly的书几乎两倍长是K&R C.
迈克尔Shopsin

22

早在创建C的那一天,无论多么短,每个字符串都需要额外的4个字节的空间,这真是浪费!

还有另一个问题-请记住,C不是面向对象的,因此,如果对所有字符串进行长度前缀,则必须将其定义为编译器固有类型,而不是a char*。如果是特殊类型,则您将无法将字符串与常量字符串进行比较,即:

String x = "hello";
if (strcmp(x, "hello") == 0) 
  exit;

必须具有特殊的编译器详细信息才能将该静态字符串转换为String,或者具有不同的字符串函数以考虑长度前缀。

我认为最终,他们只是不像Pascal那样选择长度前缀方式。


10
边界检查也需要时间。以今天的术语来说,这是微不足道的,但是当人们关心大约4个字节时,人们就注意到了这一点。
Steven Burnap 2014年

18
@StevenBurnap:即使在今天,即使您处于一个遍历200 MB图像的每个像素的内循环中,也不是那么简单。通常,如果您正在编写C,那么您想走得更快,并且您不想在每次for循环都已经设置好以遵守边界的情况下在每次迭代中浪费时间进行无用的边界检查。
Matteo Italia

4
@ VF1“回到过去”很可能是两个字节(DEC PDP / 11有人吗?)
ClickRick

7
它不只是“回到过去”。C作为一种“便携式汇编语言”针对的软件,例如OS内核,设备驱动程序,嵌入式实时软件等。在边界检查上浪费六条指令确实很重要,而且在许多情况下,您需要“越界”(如果不能随机访问其他程序存储,如何编写调试器?)。
James Anderson

3
考虑到BCPL具有长度计数的论点,这实际上是一个相当弱的论点。就像Pascal一样,尽管它仅限于1个字,所以通常只有8或9位,这是一个位限制(它也排除了共享部分字符串的可能性,尽管该优化可能在当时还太先进了)。并宣布一个字符串,长度其次阵列真是一个结构就不需要特殊的编译器的支持..
VOO

11

在C语言中,数组的任何连续子集也是数组,因此可以对其进行操作。这适用于读取和写入操作。如果大小是显式存储的,则此属性将不成立。


6
“设计会有所不同”并不是反对设计有所不同的原因。
VF1 2014年

7
@ VF1:您曾经用标准Pascal编程吗?C对数组具有合理灵活性的能力是对汇编(绝对没有安全性)和第一代类型安全语言(过分类型安全性,包括确切的数组范围)的巨大改进
MSalters 2014年

5
切片阵列的能力确实是C89设计的一个重要论据。

老式的Fortran黑客也充分利用了此属性(尽管它需要将切片传递给Fortran中的数组)。编程或调试时令人困惑和痛苦,但工作时又快速又优雅。
dmckee 2014年

3
有一种有趣的设计方法可以切片:不要将长度存储在数组旁边。对于任何指向数组的指针,请将其长度与指针一起存储。(当您只有一个实际的C数组时,大小是一个编译时间常数,可供编译器使用。)虽然占用更多空间,但可以在保持长度的同时进行切片。&[T]例如,Rust对类型进行此操作。

8

用长度标记数组的最大问题不是存储该长度所需的空间,也不是如何存储长度的问题(对于短数组使用一个额外的字节通常不会令人反感,也不会使用四个长数组需要额外的字节,但即使是短数组也可能需要使用四个字节)。一个更大的问题是给定的代码如下:

void ClearTwoElements(int *ptr)
{
  ptr[-2] = 0;
  ptr[2] = 0;
}
void blah(void)
{
  static int foo[10] = {1,2,3,4,5,6,7,8,9,10};
  ClearTwoElements(foo+2);
  ClearTwoElements(foo+7);
  ClearTwoElements(foo+1);
  ClearTwoElements(foo+8);
}

代码能够接受第一个调用ClearTwoElements而拒绝第二个调用的唯一方法是使该ClearTwoElements方法接收足以知道每种情况下foo除知道哪一部分之外还接收到对数组部分的引用的信息。通常,这将使传递指针参数的成本增加一倍。此外,如果每个数组前面都有一个指向末尾的地址的指针(最有效的验证格式),则优化后的代码ClearTwoElements可能会变成:

void ClearTwoElements(int *ptr)
{
  int* array_end = ARRAY_END(ptr);
  if ((array_end - ARRAY_BASE(ptr)) < 10 ||
      (ARRAY_BASE(ptr)+4) <= ADDRESS(ptr) ||          
      (array_end - 4) < ADDRESS(ptr)))
    trap();
  *(ADDRESS(ptr) - 4) = 0;
  *(ADDRESS(ptr) + 4) = 0;
}

注意,方法调用者通常可以完全合法地将指针传递给数组的开头,或者将最后一个元素传递给方法。仅当该方法尝试访问传入数组之外的元素时,此类指针才会引起任何麻烦。因此,被调用方法必须首先确保数组足够大,以使用于验证其参数的指针算术本身不会超出范围,然后进行一些指针计算以验证参数。这种验证所花费的时间可能会超过进行任何实际工作所花费的成本。此外,如果编写并调用该方法,则可能会更有效:

void ClearTwoElements(int arr[], int index)
{
  arr[index-2] = 0;
  arr[index+2] = 0;
}
void blah(void)
{
  static int foo[10] = {1,2,3,4,5,6,7,8,9,10};
  ClearTwoElements(foo,2);
  ClearTwoElements(foo,7);
  ClearTwoElements(foo,1);
  ClearTwoElements(foo,8);
}

一种类型的概念很不错,它结合了一些东西来识别一个对象和一个东西来识别一个对象。但是,如果不需要执行验证,则C样式的指针会更快。


如果数组具有运行时大小,则指向数组的指针与指向数组元素的指针将根本不同。后者可能根本无法直接转换为前者(无需创建新数组)。[]指针的语法可能仍然存在,但是与这些假设的“真实”数组有所不同,并且您描述的问题可能不存在。
海德2014年

@hyde:问题是对象基地址未知的指针是否应允许算术运算。另外,我忘记了另一个困难:结构中的数组。考虑一下,我不确定是否有任何一种指针类型可以指向存储在结构中的数组,而不需要每个指针不仅包括指针本身的地址,还包括上下限的合法性。它可以访问的范围。
2014年

插入点。我认为,这仍然减少了阿蒙的答案。
VF1

该问题询问数组。指针是内存地址,不会随问题的前提而变化,只要了解目的即可。数组将获得长度,指针将保持不变(除了指向数组的指针需要为新的,独特的,唯一的类型外,非常类似于指向结构的指针)。
海德2014年

@hyde:如果人们充分改变了语言的语义,尽管存储在结构中的数组会带来一些困难,但可能会使数组包含相关的长度。就其语义而言,数组边界检查仅在将相同检查应用于数组元素的指针时才有用。
2014年

7

C语言与大多数其他第三代语言以及我所知道的所有最新语言之间的根本区别之一是,C语言的设计并非旨在使程序员的生活更轻松或更安全。设计该程序的初衷是希望程序员知道他们在做什么,并且只想做到这一点。它在“幕后”不做任何事情,因此您不会感到惊讶。甚至编译器级别的优化也是可选的(除非您使用Microsoft编译器)。

如果程序员想在他们的代码中编写边界检查,则C使其非常简单,但是程序员必须选择在空间,复杂性和性能方面付出相应的代价。即使多年来我一直没有激怒过它,但在教编程以突破基于约束的决策概念时,我仍然会使用它。基本上,这意味着您可以选择做任何您想做的事情,但是您做出的每个决定都需要您付出一定的代价。当您开始告诉他人您希望他们的程序做什么时,这一点就变得尤为重要。


3
随着C的发展,它并不是那么“设计”的。最初,这样的声明int f[5];不会创建f为五个项目的数组。相反,它等效于int CANT_ACCESS_BY_NAME[5]; int *f = CANT_ACCESS_BY_NAME;。可以处理前一个声明,而无需编译器真正“理解”数组时间。它只需要输出一个汇编程序指令来分配空间,然后就可以忘记f与数组有任何关系。数组类型的不一致行为源于此。
2014年

1
事实证明,没有程序员知道C所要求的程度。
CodesInChaos

7

简短答案:

因为C是一种低层次的编程语言,它希望你把这些问题自己照顾,但是这增加了更大的灵活性,究竟如何你实现它。

C具有数组的编译时概念,该数组以长度进行初始化,但在运行时,整个过程仅作为指向数据开头的单个指针存储。如果要将数组长度与数组一起传递给函数,则可以自己执行:

retval = my_func(my_array, my_array_length);

或者,您可以使用带有指针和长度的结构,或任何其他解决方案。

较高级别的语言将作为数组类型的一部分为您完成此操作。在C语言中,您有责任自己执行此操作,还可以选择执行方法的灵活性。 而且,如果您正在编写的所有代码都已经知道数组的长度,则根本不需要将长度作为变量传递。

明显的缺点是,无需检查作为指针传递的数组的固有范围,就可以创建一些危险的代码,但这是低级/系统语言的性质及其折衷方案。


1
+1“如果您正在编写的所有代码都已经知道数组的长度,则根本不需要将长度作为变量传递。”
林果皞2015年

如果只有指针+长度结构被烘焙到语言和标准库中。如此多的安全漏洞本可以避免。
CodesInChaos

那就不是C了。还有其他语言可以做到这一点。C使您处于低水平。
thomasrutter

C是作为一种低级编程语言而发明的,许多方言仍然支持低级编程,但是许多编译器编写者都喜欢不能真正称为低级语言的方言。它们允许甚至需要低级语法,但是然后尝试推断其行为可能与该语法所隐含的语义不匹配的较高级结构。
超级猫

5

额外存储的问题是一个问题,但我认为这是一个小问题。毕竟,尽管amon指出经常可以静态地跟踪它,但大多数时候还是需要跟踪它的长度。

更大的问题是长度存储在哪里以及长度如何。没有一个地方可以在所有情况下正常工作。您可能会说只是将长度存储在数据之前的内存中。如果阵列不是指向内存,而是指向UART缓冲区,该怎么办?

留出足够的长度允许程序员为适当的情况创建自己的抽象,并且有大量现成的库可用于通用情况。真正的问题是,为什么那些抽象不用于安全敏感的应用程序?


1
You might say just store the length in the memory just before the data. What if the array isn't pointing to memory, but something like a UART buffer?您能再解释一下吗?难道有些事情可能会发生得太频繁或只是一种罕见的情况?
Mahdi 2014年

如果我设计了它,则编写为的函数参数T[]将不等于,T*而是将指针和大小的元组传递给函数。固定大小的数组可能会衰减到这样的数组切片,而不是像在C语言中那样衰减到指针。这种方法的主要优点不是它本身是安全的,而是一个约定,包括标准库在内的所有内容都可以建立。
CodesInChaos

1

C语言的发展

看来,结构应该以直观的方式映射到机器的内存中,但是在包含数组的结构中,没有好地方可以存放包含数组基础的指针,也没有任何方便的方式来安排它初始化。例如,早期的Unix系统的目录条目可能用C描述为
struct {
    int inumber;
    char    name[14];
};
我想要的结构不仅要表征抽象对象,还要描述可能从目录中读取的位的集合。编译器在哪里可以隐藏指向name语义要求的指针?即使更抽象地考虑结构,并且可以以某种方式隐藏指针的空间,当分配一个复杂的对象时,我可能会如何处理正确初始化这些指针的技术问题?

该解决方案构成了无类型BCPL和类型C之间的进化链中的关键跳跃。它消除了指针在存储中的实现,当在表达式中提及数组名称时,导致了指针的创建。在今天的C语言中仍然存在的规则是,当数组类型的值出现在表达式中时,它们将转换为指向组成数组的第一个对象的指针。

这段话讨论了为什么数组表达式在大多数情况下会衰减为指针,但是同样的道理也适用于为什么数组长度不与数组本身存储在一起的原因。如果要在类型定义与其在内存中的表示形式之间进行一对一映射(如Ritchie所做的那样),那么没有合适的位置存储该元数据。

另外,考虑多维数组;您将在哪里存储每个维度的长度元数据,以便仍然可以像这样遍历数组

T *p = &a[0][0];

for ( size_t i = 0; i < rows; i++ )
  for ( size_t j = 0; j < cols; j++ )
    do_something_with( *p++ );

-2

该问题假定C中存在数组。称为数组的事物只是用于对数据和指针算术连续序列进行操作的语法糖。

以下代码将int大小的数据块中的某些数据从src复制到dst,而不知道它实际上是字符串。

char src[] = "Hello, world";
char dst[1024];
int *my_array = src; /* What? Compiler warning, but the code is valid. */
int *other_array = dst;
int i;
for (i = 0; i <= sizeof(src)/sizeof(int); i++)
    other_array[i] = my_array[i]; /* Oh well, we've copied some extra bytes */
printf("%s\n", dst);

为什么C如此简化,它没有适当的数组?我不知道这个新问题的正确答案。但是有人经常说C只是(某种程度上)更具可读性和可移植性。


2
我认为您尚未回答问题。
罗伯特·哈维

2
你说的是真的,但问的人想知道为什么会这样。

9
请记住,C的绰号之一是“便携式程序集”。虽然该标准的更新版本增加了更高级别的概念,但其核心是由简单的低级结构和指令组成,这些结构和指令在大多数非平凡的机器中都是常见的。这驱动了使用该语言做出的大多数设计决策。运行时唯一存在的变量是整数,浮点数和指针。指令包括算术,比较和跳转。几乎所有其他内容都是基于此的薄层构建。

8
考虑到您实际上如何无法与其他构造生成相同的二进制数,那么说C没有数组是错误的(嗯,至少,如果考虑使用#defines确定数组大小,至少不会这样)。C语言中的数组 “连续数据序列”,对此没有什么困扰。在这里使用指针就像数组是指针(而不是显式指针算术),而不是数组本身。
海德2014年

2
是的,请考虑以下代码:struct Foo { int arr[10]; }arr是一个数组,而不是一个指针。
史蒂文·本纳普
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.