【问题标题】:Why does C need arrays if it has pointers?如果 C 有指针,为什么需要数组?
【发布时间】:2010-12-11 18:55:29
【问题描述】:

如果我们可以使用指针和malloc来创建和使用数组,为什么C语言中存在数组类型?如果我们可以使用指针来代替,不是没有必要吗?

【问题讨论】:

  • “数组类型”是什么意思? int x[6];?
  • 我不知道 C 中有一个“数组”类型。
  • 我更赞成这个问题,因为答案中有一些有趣的讨论。
  • 这是出于深思熟虑的好奇心而提出的一个完全有效的问题。投票赞成 +1。

标签: c arrays types pointers


【解决方案1】:

数组比动态内存分配更快。

数组在“编译时”“分配”,而 malloc 在运行时分配。分配需要时间。

此外,C 不要求 malloc() 和朋友在独立实现中可用。


编辑

数组示例

#define DECK_SIZE 52
int main(void) {
    int deck[DECK_SIZE];
    play(deck, DECK_SIZE);
    return 0;
}

malloc() 的示例

int main(void) {
    size_t len = 52;
    int *deck = malloc(len * sizeof *deck);
    if (deck) {
        play(deck, len);
    }
    free(deck);
    return 0;
}

在数组版本中,deck数组的空间在程序创建时由编译器保留(当然,内存只在程序运行时保留/占用),在@ 987654326@ 版本,每次运行程序时都必须请求deck 数组的空间。

数组永远不会改变大小,malloc 内存可以在需要时增长。

如果您只需要固定数量的元素,请使用数组(在您的实现范围内)。 如果您需要在程序运行过程中可以增长或缩小的内存,请使用malloc() 和朋友。

【讨论】:

  • 两者的分配发生在运行时。不同之处在于 C 允许您在运行时为其中一个提供大小,但不能为另一个提供大小。
  • 你能给我更多的细节,当一个数组被定位时,编译时和运行时会发生什么?即使是一个例子也会有所帮助。谢谢。
  • @AraK,我认为在运行时分配意味着软件必须从操作系统请求内存(调用malloc),而不是仅仅调整堆栈-指针(因此在“编译时”“分配”)。
【解决方案2】:

这不是一个坏问题。事实上,早期的 C 语言没有数组类型。

全局和静态数组在编译时分配(非常快)。其他数组在运行时(快速)在堆栈上分配。使用 malloc 分配内存(用于数组或其他)要慢得多。在解除分配中可以看到类似的情况:动态分配的内存解除分配速度较慢。

速度不是唯一的问题。数组类型在超出范围时会自动释放,因此它们不会被错误地“泄漏”。您不必担心意外释放某些东西两次,等等。它们还使静态分析工具更容易检测错误。

您可能会争辩说,有一个函数_alloca() 可以让您从堆栈中分配内存。是的,在_alloca() 上需要数组并没有技术 的原因。但是,我认为数组使用起来更方便。此外,编译器优化数组的使用比使用带有_alloca() 返回值的指针更容易,因为很明显堆栈分配的数组与堆栈指针的偏移量是多少,而如果_alloca() 是像黑盒函数调用一样处理,编译器无法提前告诉这个值。

编辑,因为 tsubasa 已要求提供有关此分配如何发生的更多详细信息:

在 x86 架构上,ebp 寄存器通常引用当前函数的堆栈帧,并用于引用堆栈分配的变量。例如,您可能有一个位于[ebp - 8]int 和一个从[ebp - 24] 延伸到[ebp - 9]char 数组。堆栈上可能还有更多变量和数组。 (编译器在编译时决定如何使用栈帧。C99 编译器允许对可变大小的数组进行栈分配,这只是在运行时做一点点工作的问题。)

在 x86 代码中,指针偏移量(例如 [ebp - 16])可以在单个指令中表示。相当有效率。

现在,重要的一点是当前上下文中所有堆栈分配的变量和数组都是通过单个寄存器的偏移量检索的。如果您调用 malloc ,那么(正如我所说的)在实际为您找到一些内存时会产生一些处理开销。而且,malloc 给你一个新的内存地址。假设它存储在ebx 寄存器中。您不能再使用来自ebp 的偏移量,因为在编译时您无法判断该偏移量是多少。所以你基本上是在“浪费”一个额外的寄存器,如果你使用一个普通的数组,你就不需要了。如果你 malloc 更多的数组,你有更多的“不可预测”的指针值放大这个问题。

【讨论】:

  • “担心意外释放某些东西两次” - 删除空指针是标准定义的无操作,即它是安全的。现在,您在空指针上调用 delete 的事实可能表明另一个问题,但调用本身是安全的。
  • @Ed Swangren:虽然一种常见的编码实践是在释放指针变量后将它们设置为 NULL:A)不是每个人都这样做,并且 B)在可以有多个指针指向同一个的数据结构中东西,这不能防止问题
  • 多维数组似乎很难甚至不可能用像_alloca 这样的东西来处理,但没有一些 C 显然还没有准备好承担的开销。此外,sizeof 也不会那么容易实现以仍然产生编译时结果(编译器基本上必须回溯指针以查看用于该指针的初始化程序,然后决定是否产生 sizeof指针,或“数组”)
  • @litb:多维数组并不难做,程序员会承担一些额外的工作(即自己跟踪数组维度),但不会降低性能。请参阅我对您的回答的评论。
【解决方案3】:

数组有其用途,应尽可能使用,因为静态分配有助于使程序更稳定,并且有时由于需要确保不会发生内存泄漏而成为必需品。

它们的存在是因为某些要求需要它们。

在诸如 BASIC 之类的语言中,由于语言结构,您有某些允许的命令,这是已知的。那么,使用 malloc 创建数组,然后从字符串中填充它们有什么好处呢?

如果我必须定义操作的名称,为什么不将它们放入一个数组中?

C 是作为通用语言编写的,这意味着它在任何情况下都应该有用,因此他们必须确保它具有对编写操作系统和嵌入式系统有用的构造。

例如,数组是指定指向 malloc 开头的简写方式。

但是,想象一下尝试使用指针操作而不是 vec[x] * vec[y] 来进行矩阵数学运算。很容易出错。

【讨论】:

    【解决方案4】:

    请参阅this question discussing space hardening 和 C。有时动态内存分配只是一个坏主意,我曾使用过完全没有 malloc() 和朋友的 C 库。

    您不希望卫星解除对 NULL 指针的引用,就像您希望空中交通管制软件忘记将堆块归零一样。

    了解什么是 C 的一部分以及什么将其扩展到各种统一标准(即 POSIX)也很重要(正如其他人所指出的)。

    【讨论】:

      【解决方案5】:

      与处理指针相比,数组是一个很好的语法改进。在处理指针时,您可能会在不知不觉中犯各种错误。如果因为使用了错误的字节大小而在内存中移动了太多空间怎么办?

      【讨论】:

      • 在 C 中,数组和指针都使用相同的方法来计算要移动的字节数。
      • @Artelius,一点也不。数组使用它们的类型来知道在索引时使用什么偏移量,并使用它们自己的地址来知道要开始的基地址,而指针使用存储在其中的地址来知道基地址,它们不知道偏移量(你必须手动告诉他们)。 +1 :)
      • @litb:你在说什么?数组和指针使用它们的类型签名来知道要使用的基地址的偏移量(尽管你是对的,这个基地址的来源是不同的)。
      • 嗯,是的,但是当你有一个多维数组int[2][2],那么当你做a[1]时,前进sizeof(int)是错误的,你必须前进sizeof(int) * 2:)
      • IMO,“使用错误的字节大小”这句话暗示 omouse 认为指针运算总是以字节为单位(许多 C 初学者的误解——我曾一度认为自己就是这种情况) .多暗阵列是一个问题,但我认为它们不是 omouse 想到的问题。如果他有其他想法,欢迎他说清楚。
      【解决方案6】:

      Explanation by Dennis Ritchie关于C历史:

      胚胎C

      NB 存在的时间太短,以至于没有完整的描述。它提供了 int 和 char 类型、它们的数组以及指向它们的指针,以

      为代表的样式声明
      int i, j;
      char c, d;
      int iarray[10];
      int ipointer[];
      char carray[10];
      char cpointer[];
      

      数组的语义与 B 和 BCPL 中的完全一样:iarray 和 carray 的声明创建了动态初始化的单元格,其值分别指向 10 个整数和字符序列中的第一个。 ipointer 和 cpointer 的声明省略了大小,以断言不应自动分配存储空间。在过程中,语言对指针的解释与数组变量的解释相同:指针声明创建的单元格与数组声明的不同之处仅在于程序员应该分配一个引用对象,而不是让编译器分配空间和初始化单元格。

      存储在绑定到数组和指针名称的单元格中的值是相应存储区域的机器地址,以字节为单位。因此,通过指针间接意味着没有运行时开销来将指针从字偏移量缩放到字节偏移量。另一方面,数组下标和指针运算的机器代码现在取决于数组或指针的类型:计算 iarray[i] 或 ipointer+i 意味着将加数 i 按所引用对象的大小缩放。

      这些语义代表了从 B 的简单过渡,我用它们做了几个月的试验。当我尝试扩展类型表示法,尤其是添加结构化(记录)类型时,问题变得很明显。结构似乎应该以直观的方式映射到机器的内存中,但是在包含数组的结构中,没有好地方来存放包含数组基数的指针,也没有任何方便的方法来安排它初始化。例如,早期 Unix 系统的目录条目在 C 中可能被描述为

      struct {
          int   inumber;
          char  name[14];
      };
      

      我希望该结构不仅可以表征抽象对象,还可以描述可能从目录中读取的位集合。编译器可以在哪里隐藏语义要求的名称指针?即使结构被认为更抽象,并且指针的空间可以以某种方式隐藏,我如何处理在分配复杂对象时正确初始化这些指针的技术问题,也许是一个指定结构包含包含任意深度结构的数组的结构?

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

      用我自己的话总结一下——如果上面的name 只是一个指针,那么该结构中的任何一个都将包含一个额外的指针,从而破坏了它与外部对象(如目录条目)的完美映射。

      【讨论】:

        猜你喜欢
        • 2014-01-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-06-02
        • 1970-01-01
        • 1970-01-01
        • 2013-04-09
        • 1970-01-01
        相关资源
        最近更新 更多