4

我只写了几个星期的 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 如何分配内存?默认是什么?

4

8 回答 8

26

简短的回答:它不是为您分配的。

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

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

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

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

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

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;
}

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

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

于 2009-03-02T21:42:33.447 回答
17

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

newCell.subcells[i] = ...

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

*(newCell.subcells + i)

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

newCell.subcells = malloc(bytecount)

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

  • malloc返回一个指向没有类型的对象的指针。您可以使指针指向该内存区域,并且对象的类型将有效地成为指向对象类型的类型。内存未初始化为任何值,访问通常较慢。如此获得的对象称为allocated objects
  • 您可以全局放置对象。他们的记忆将被初始化为零。对于点,您将获得 NULL 指针,对于浮点数,您也将获得适当的零。您可以依赖适当的初始值。
  • 如果您有局部变量但使用static存储类说明符,那么您将具有与全局对象相同的初始值规则。内存的分配方式通常与全局对象相同,但这绝不是必要的。
  • 如果你有没有任何存储类说明符或 with 的局部变量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把它写下来作为练习留给你:)

于 2009-03-02T21:42:10.327 回答
4

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

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

于 2009-03-02T21:41:50.203 回答
3

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

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

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

于 2009-03-02T21:43:30.590 回答
0

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

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

于 2009-03-02T21:43:26.440 回答
0

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

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

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

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

默认是什么?

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

于 2009-03-02T21:45:34.047 回答
0

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

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

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

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

于 2009-03-02T21:47:05.823 回答
0

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

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 选项有助于显示指针,作为一种粗略的调试形式。调试器程序也可以很好地显示它们。)

于 2009-03-03T01:00:20.880 回答