没有在C中显式存储数组长度的背后原因是什么? 。例如:


缓冲区中有可用的长度可以防止缓冲区溢出。如果处理多个数组,则堆栈会变得更复杂。

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

由于堆管理器必须跟踪动态分配的数组占用的空闲块大小,为什么不使该信息可用(并添加另一条规则,在编译时检查该规则,除非有人想用脚开枪,否则无法明确操纵该长度)。

另一方面,我唯一想到的是没有长度跟踪可能会使编译器更简单,但并没有那么简单。

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

评论

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

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

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

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

投票重新开放。即使某个地方只是“ K&R都没有想到”,也存在某些原因。

#1 楼

C数组确实会跟踪它们的长度,因为数组长度是一个静态属性:

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


通常无法查询该长度,但是不需要因为它仍然是静态的,所以只需声明一个宏XS_LENGTH即可,就可以了。

更重要的问题是C数组隐式降级为指针,例如传递给函数时。这确实有道理,并且允许一些不错的底层技巧,但是却丢失了有关数组长度的信息。因此,一个更好的问题是,为什么在设计C时会对指针进行这种隐式降级。 C允许我们将整数转换为指针,将指针转换为其他指针,并将指针视为数组。在这样做的同时,C还不足以疯狂地制造某种数组长度,但似乎相信Spiderman的座右铭:强大的程序员将有望担负起跟踪长度和溢出的重大责任。

评论


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

–VF1
2014年4月28日在15:59

@ VF1是的。但是重要的是,数组和指针在C语言中是不同的。假设您没有使用任何编译器扩展,通常就不能将数组本身传递给函数,但是可以传递指针,并以如果它是一个数组。您实际上在抱怨指针没有附加长度。您应该抱怨数组不能作为函数参数传递,或者数组隐式降级为指针。

–阿蒙
14年4月28日在16:03

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

–tcrosley
2014年4月28日19:23



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

– Gwyn Evans
2014年4月28日在20:23

再次@GwynEvans:指针不是数组。因此,如果“将数组作为参数传递给另一个函数”,则不是传递数组而是传递指针。声称其中的xs是数组的sizeof(xs)在另一个范围内会有所不同,这显然是错误的,因为C的设计不允许数组离开其范围。如果其中xs是数组的sizeof(xs)与其中xs是指针的sizeof(xs)不同,那么这就不足为奇了,因为您正在将苹果与橙子进行比较。

–阿蒙
2014年4月28日在20:47

#2 楼

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

仔细阅读C的历史可以对上面的内容有更多的了解,但事实并非如此。




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


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

评论


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

– ClickRick
2014年4月28日在21:37

很好的联系,他们还明确更改了存储字符串的长度,以使用定界符来避免由于将计数保存在8位或9位插槽中而导致的对字符串长度的限制,部分原因是维护计数似乎在我们的经验,不如使用终结器方便-太多了:-)

–Voo
2014年4月29日在12:46



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

– Michael Shopsin
2014年4月29日15:39

#3 楼

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

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

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


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

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

评论


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

–Gor机器人
2014年4月28日在19:44

@StevenBurnap:即使在今天,即使您处于一个遍历200 MB图像的每个像素的内循环中,也不是那么简单。通常,如果您正在编写C,则您想提高速度,并且您不想在已经设置了for循环以遵守边界的每次迭代中浪费时间进行无用的边界检查。

–意大利Matteo
2014年4月28日在20:59



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

– ClickRick
2014年4月28日在21:26

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

–詹姆斯·安德森(James Anderson)
2014年4月29日在9:57

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

–Voo
2014年4月29日在13:21

#4 楼

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

评论


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

–VF1
2014年4月28日在20:25

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

– MSalters
2014年4月28日在20:30

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

–user44761
2014年4月28日在20:37

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

– dmckee ---前主持人小猫
2014年4月28日在21:22

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

–user7043
2014年4月29日上午10:43

#5 楼

用长度标记数组的最大问题不是存储该长度所需的空间,也不是如何存储长度的问题(对于较短的数组使用一个额外的字节通常不会令人反感,也不会使用四个长数组需要额外的字节,但即使是短数组也可能需要使用四个字节)。一个更大的问题是给定的代码,例如: ClearTwoElements方法接收信息,足以知道在每种情况下除了知道哪一部分之外,还接收到对数组ClearTwoElements的一部分的引用。这通常会使传递指针参数的成本增加一倍。此外,如果每个数组前面都有一个指向末尾的地址的指针(最有效的验证格式),则针对foo的优化代码可能会变成类似以下内容:

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

这是一个很好的。但是,如果不需要执行验证,则C样式的指针会更快。

评论


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

–氢化物
2014年4月28日在19:49

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

–超级猫
2014年4月28日在20:18

有趣的一点。不过,我认为这仍然是amon的答案。

–VF1
2014年4月28日在20:30

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

–氢化物
2014年4月28日在20:36

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

–超级猫
2014年4月28日在20:40

#6 楼

C语言与大多数其他第三代语言以及我所知道的所有最新语言之间的根本区别之一是,C语言的设计并非旨在使程序员的生活更轻松或更安全。设计它的初衷是希望程序员知道他们在做什么,并且只想做到这一点。它在“幕后”不做任何事情,因此您不会感到惊讶。甚至编译器级别的优化也是可选的(除非您使用Microsoft编译器)。在空间,复杂性和性能方面付出相应的代价。即使多年来我一直没有激怒过它,但在教编程以突破基于约束的决策概念时,我仍然会使用它。基本上,这意味着您可以选择做任何您想做的事情,但是您做出的每个决定都需要您付出一定的代价。当您开始告诉他人您希望他们的程序做什么时,这一点就变得尤为重要。

评论


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

–超级猫
2014年4月29日在18:20

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

– CodesInChaos
16年4月8日在13:41

#7 楼

简短答案:

由于C是一种低级编程语言,它希望您自己解决这些问题,但这在实现它的方式上增加了更大的灵活性。 /> C具有数组的编译时概念,该数组以长度进行初始化,但是在运行时,整个过程都简单地存储为指向数据开头的单个指针。如果您想将数组长度与数组一起传递给函数,请自己做:

retval = my_func(my_array, my_array_length);


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

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

明显的缺点是没有固有的界限检查作为指针传递的数组可以创建一些危险的代码,但这是低级/系统语言的本质以及它们所给予的折衷。

评论


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

–水果
2015年9月10日下午16:40

如果仅将指针+长度结构放入语言和标准库中。如此多的安全漏洞本可以避免。

– CodesInChaos
16年4月8日在13:42

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

–胸腺
16年4月12日在0:38

C是作为一种低级编程语言而发明的,许多方言仍然支持低级编程,但是许多编译器作者都喜欢方言,而这些方言实际上不能称为低级语言。它们允许甚至要求使用低级语法,但是随后尝试推断其行为可能与该语法所隐含的语义不匹配的高级构造。

–超级猫
17年2月10日在18:20

#8 楼

额外存储的问题是一个问题,但我认为这是一个小问题。毕竟,尽管amon提出了一个很好的观点,即通常可以静态地跟踪它,但是在大多数情况下,无论如何,都需要跟踪长度。

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

不留任何长度,程序员可以根据自己的情况创建自己的抽象,并且已经做好了很多准备通用情况下可用的库。真正的问题是,为什么那些抽象不用于安全敏感的应用程序?

评论


您可能会说只将长度存储在数据之前的内存中。如果阵列不是指向内存,而是指向UART缓冲区,该怎么办?您能再解释一下吗?另外,这种情况可能会发生得太频繁或只是一种罕见的情况?

–马赫迪
2014年5月2日在10:45

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

– CodesInChaos
16年4月8日在13:50

#9 楼

来自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++ );

那样遍历数组

#10 楼

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

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只是(某种程度上)更具可读性和可移植性。

评论


我认为您尚未回答问题。

–罗伯特·哈维(Robert Harvey)
2014年4月28日在15:47

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

–user22815
2014年4月28日在15:53

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

–user22815
2014年4月28日在16:47



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

–氢化物
2014年4月28日在19:28

是的,请考虑以下代码:struct Foo {int arr [10]; }。 arr是一个数组,而不是一个指针。

–Gor机器人
2014年4月28日在21:31