【问题标题】:Does C allocate memory automatically for me?C 会自动为我分配内存吗?
【发布时间】:2009-03-02 21:38:15
【问题描述】:

我只写了几个星期的 C 语言,并没有花时间为malloc() 担心太多。不过,最近,我的一个程序返回了一串幸福的面孔,而不是我预期的真/假值。

如果我创建一个这样的结构:

typedef struct Cell {
  struct Cell* subcells;
} 

然后像这样初始化它

Cell makeCell(int dim) {
  Cell newCell;

  for(int i = 0; i < dim; i++) {
    newCell.subcells[i] = makeCell(dim -1);
  }

  return newCell; //ha ha ha, this is here in my program don't worry!
}

我最终会访问存储在某个地方的内存中的快乐面孔,或者可能会覆盖以前存在的单元格,还是什么?我的问题是,当我实际上还没有 malloc() 分配适当的内存量时,C 如何分配内存?默认是什么?

【问题讨论】:

    标签: c malloc


    【解决方案1】:

    简答:不是为你分配的。

    答案稍长:subcells 指针未初始化,可能指向任何地方。这是一个错误,绝不应该允许它发生。

    更长的答案仍然是: 自动变量在堆栈上分配,全局变量由编译器分配并且通常占用特殊段或可能在堆中。全局变量默认初始化为零。自动变量没有默认值(它们只是获取在内存中找到的值),程序员负责确保它们具有良好的起始值(尽管许多编译器会在您忘记时尝试提示您)。

    你函数中的newCell 变量是自动的,并且没有被初始化。你应该解决这个问题。要么立即给newCell.subcells 一个有意义的值,要么将它指向NULL,直到你为它分配一些空间。这样,如果您在为其分配一些内存之前尝试取消引用它,则会引发分段冲突。

    更糟糕的是,您会按值返回 Cell,但在尝试填充 subcells 数组时会将其分配给 Cell *。要么返回指向堆分配对象的指针,要么将值分配给本地分配的对象。

    一个通常的习惯用法是这样的

    Cell* makeCell(dim){
      Cell *newCell = malloc(sizeof(Cell));
      // error checking here
      newCell->subcells = malloc(sizeof(Cell*)*dim); // what if dim=0?
      // more error checking
      for (int i=0; i<dim; ++i){
        newCell->subCells[i] = makeCell(dim-1);
        // what error checking do you need here? 
        // depends on your other error checking...
      }
      return newCell;
    }
    

    虽然我给你留下了一些问题要解决..

    请注意,您必须跟踪最终需要释放的所有内存位...

    【讨论】:

    • 全局变量被初始化 - 为零。您省略了提及文件静态和函数静态变量;出于练习的目的,它们更像是全局变量(初始化为零)而不是自动变量。
    • @Jonathon:在 K&R 中查看,您至少可以追溯到 1989 年。在文本中已修复。谢谢。
    • 我不明白您认为该函数如何返回指向自动变量的指针 - 该函数似乎按值返回单元格。 (当然,你其余的批评都是关于金钱的。)
    • @Chuck:结构总是通过引用返回,不是吗?还是这取决于对象的大小?
    • 我发现自 c90 以来就支持声明结构赋值(因此按值返回),并声称许多编译器在 1990 年代中期不支持,这可能是我记得的。也解决这个问题...
    【解决方案2】:

    您的指针没有默认值。您的指针将指向它当前存储的任何内容。由于您尚未初始化它,因此该行

    newCell.subcells[i] = ...
    

    有效地访问内存的某些不确定部分。请记住 subcells[i] 等价于

    *(newCell.subcells + i)
    

    如果左侧包含一些垃圾,您最终会将i 添加到垃圾值并访问该不确定位置的内存。正如您所说的,您必须初始化指针以指向某个有效的内存区域:

    newCell.subcells = malloc(bytecount)
    

    在哪一行之后你可以访问那么多字节。关于其他内存来源,有不同类型的存储都有它们的用途。您获得哪种类型取决于您拥有哪种对象以及您告诉编译器使用哪种存储类。

    • malloc 返回一个指向没有类型的对象的指针。您可以使指针指向该内存区域,并且对象的类型将有效地成为指向对象类型的类型。内存未初始化为任何值,访问通常较慢。如此获得的对象称为allocated objects
    • 您可以全局放置对象。他们的记忆将被初始化为零。对于点,您将获得 NULL 指针,对于浮点数,您也将获得适当的零。您可以依赖适当的初始值。
    • 如果您有局部变量但使用static 存储类说明符,那么您将拥有与全局对象相同的初始值规则。内存的分配方式通常与全局对象相同,但这绝不是必要的。
    • 如果您有没有任何存储类说明符或带有auto 的局部变量,那么您的变量将被分配到堆栈上(即使C 没有这样定义,这当然是编译器实际上所做的)。您可以获取其地址,在这种情况下,编译器将不得不省略优化,例如将其放入寄存器中。
    • 与存储类说明符register 一起使用的局部变量被标记为具有特殊存储。结果,您无法再获取它的地址。在最近的编译器中,通常不再需要使用register,因为它们具有复杂的优化器。如果您真的是专家,那么如果使用它,您可能会从中获得一些性能。

    对象具有相关的存储持续时间,可用于显示不同的初始化规则(正式地,它们仅定义对象至少存在多长时间)。用autoregister 声明的对象具有自动存储持续时间并且初始化。如果您希望它们包含某些值,则必须显式初始化它们。如果不这样做,它们将包含编译器在它们开始生命周期之前留在堆栈中的任何内容。由malloc(或该系列的另一个函数,如calloc)分配的对象已分配存储持续时间。它们的存储也没有初始化。一个例外是在使用calloc 时,在这种情况下,内存被初始化为零(“真正的”零。即所有字节 0x00,不考虑任何 NULL 指针表示)。使用static 和全局变量声明的对象具有静态存储持续时间。他们的存储初始化为零以适合他们各自的类型。请注意,对象不能有类型,但获得无类型对象的唯一方法是使用分配的存储空间。 (C 中的对象是“存储区域”)。

    那么什么是什么?这是固定代码。因为一旦你分配了一块内存,你就无法再找回你分配了多少项目,最好总是将这个计数存储在某个地方。我已经将变量 dim 引入到获取存储计数的结构中。

    Cell makeCell(int dim) {
      /* automatic storage duration => need to init manually */
      Cell newCell;
    
      /* note that in case dim is zero, we can either get NULL or a 
       * unique non-null value back from malloc. This depends on the
       * implementation. */
      newCell.subcells = malloc(dim * sizeof(*newCell.subcells));
      newCell.dim = dim;
    
      /* the following can be used as a check for an out-of-memory 
       * situation:
       * if(newCell.subcells == NULL && dim > 0) ... */
      for(int i = 0; i < dim; i++) {
        newCell.subcells[i] = makeCell(dim - 1);
      }
    
      return newCell;
    }
    

    现在,dim=2 的情况如下所示:

    Cell { 
      subcells => { 
        Cell { 
          subcells => { 
            Cell { subcells => {}, dim = 0 }
          }, 
          dim = 1
        },
        Cell { 
          subcells => { 
            Cell { subcells => {}, dim = 0 }
          }, 
          dim = 1
        }
      },
      dim = 2
    }
    

    请注意,在 C 中,函数的返回值不需要是对象。根本不需要存储。因此,您不能更改它。例如,以下是不可能的:

    makeCells(0).dim++
    

    您将需要一个“释放函数”来再次释放分配的内存。因为分配对象的存储不会自动释放。您必须调用 free 为树中的每个 subcells 指针释放该内存。把它写下来作为练习留给你:)

    【讨论】:

      【解决方案3】:

      任何未在堆上分配的东西(通过malloc 和类似的调用)都被分配在堆栈上。因此,在特定函数中创建的任何不是malloc'd 的东西都将在函数结束时被销毁。这包括返回的对象;当堆栈在函数调用后展开时,返回的对象被复制到调用者函数在堆栈上为其留出的空间。

      警告:如果要返回一个对象,其中包含指向其他对象的指针,请确保指向的对象是在堆上创建的,更好的是,在堆上创建该对象堆也是如此,除非它不打算在创建它的函数中继续存在。

      【讨论】:

        【解决方案4】:

        我的问题是,当我实际上没有 malloc() 分配适当的内存量时,C 如何分配内存?默认是什么?

        不分配内存。您必须明确地在堆栈上或动态地创建它。

        在您的示例中,子单元格指向 undefined 位置,这是一个错误。你的函数应该在某个时候返回一个指向 Cell 结构的指针。

        【讨论】:

          【解决方案5】:

          我最终会访问存储在某个地方的内存中的快乐面孔,或者可能会覆盖以前存在的单元格,还是什么?

          你很幸运,你有一张快乐的脸。在那些不幸的日子里,它可能已经把你的系统擦干净了;)

          我的问题是,当我实际上还没有 malloc() 分配适当的内存量时,C 如何分配内存?

          它没有。但是,当您定义 Cell newCell 时会发生什么,subCells 指针被初始化为垃圾值。这可能是一个 0(在这种情况下你会崩溃)或一些足够大的整数,使它看起来像一个实际的内存地址。在这种情况下,编译器会很高兴地获取那里存在的任何值并将其返回给您。

          什么是默认值?

          如果您不初始化变量,这是 行为。而且您的makeCell 功能看起来有点欠发达。

          【讨论】:

            【解决方案6】:

            实际上可以分配三个部分 - 数据、堆栈和堆。

            在您提到的情况下,它将在堆栈上分配。在堆栈上分配一些东西的问题是它只在函数的持续时间内有效。一旦您的函数返回,该内存就会被回收。所以,如果你返回一个指向堆栈上分配的东西的指针,那个指针将是无效的。如果您返回实际对象(不是指针),则会自动生成该对象的副本以供调用函数使用。

            如果您已将其声明为全局变量(例如,在头文件中或函数外部),它将在内存的数据部分中分配。本节中的内存在程序启动时自动分配,在程序结束时自动释放。

            如果您使用 malloc() 在堆上分配某些内容,那么只要您想使用它,该内存就可以使用 - 直到您调用 free() 时它被释放。这使您可以根据需要灵活地分配和取消分配内存(而不是使用全局变量,其中所有内容都预先分配并且仅在程序终止时才释放)。

            【讨论】:

              【解决方案7】:

              局部变量在堆栈上“分配”。堆栈是预先分配的内存量,用于保存这些局部变量。当函数退出时,变量不再有效,将被接下来的任何内容覆盖。

              在您的情况下,代码什么也不做,因为它不返回您的结果。此外,当范围退出时,指向堆栈上对象的指针也将不再有效,所以我猜在你的精确情况下(你似乎正在做一个链表),你将需要使用 malloc()。

              【讨论】:

                【解决方案8】:

                我要假装我是这里的电脑,正在阅读这段代码……

                typedef struct Cell {
                  struct Cell* subcells;
                }
                

                这告诉我:

                • 我们有一个称为 Cell 的结构类型
                • 它包含一个称为 subcells 的指针
                • 指针应该指向 struct Cell 类型的东西

                它没有告诉我指针是指向一个 Cell 还是一组 Cell。当一个新的 Cell 被创建时,该指针的值是不确定的,直到一个值被分配给它。在定义指针之前使用指针是个坏消息。

                Cell makeCell(int dim) {
                  Cell newCell;
                

                新的 Cell 结构,带有未定义的子单元指针。所有这一切都是保留一小块内存,称为 newCell,它是 Cell 结构的大小。它不会改变那个记忆中的值——它们可以是任何东西。

                  for(int i = 0; i < dim; i++) {
                    newCell.subcells[i] = makeCell(dim -1);
                

                为了得到newCell.subcells[i],计算从子单元格偏移i,然后取消引用。具体来说,这意味着该值是从该内存地址中提取的。举个例子,i==0...然后我们将取消引用子单元指针本身(无偏移)。由于子单元格是未定义的,它可以是任何东西。从字面上看!因此,这将要求内存中某个完全随机的值。无法保证任何结果。它可能会打印一些东西,它可能会崩溃。绝对不应该这样做。

                  }
                
                  return newCell;
                }
                

                任何时候使用指针,在取消引用它之前确保它被设置为一个值是很重要的。鼓励你的编译器尽可能地给你任何警告,许多现代编译器可以捕捉到这种事情。您还可以为指针提供可爱的默认值,例如 0xdeadbeef(是的!这是一个十六进制数字,它也是一个单词,所以看起来很有趣),以便它们脱颖而出。 (printf 的 %p 选项有助于显示指针,作为一种粗略的调试形式。调试器程序也可以很好地显示它们。)

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2010-11-08
                  • 2012-03-14
                  • 2013-05-20
                  • 2015-06-25
                  • 2020-04-06
                  相关资源
                  最近更新 更多