C автоматически выделяет память для меня? - PullRequest
4 голосов
/ 03 марта 2009

Я писал 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!
}

Собираюсь ли я в конечном итоге получить доступ к счастливым лицам, хранящимся где-то в памяти, или, возможно, написать поверх ранее существующих ячеек, или как? Мой вопрос: как C выделяет память, когда я фактически не использовал malloc () нужное количество памяти? Что по умолчанию?

Ответы [ 8 ]

26 голосов
/ 03 марта 2009

Краткий ответ: Он не выделен для вас.

Немного более длинный ответ: Указатель subcells не инициализирован и может указывать в любом месте . Это ошибка, и вы никогда не должны допустить этого.

Более длинный ответ: Автоматические переменные размещаются в стеке, глобальные переменные выделяются компилятором и часто занимают специальный сегмент или могут находиться в куче. Глобальные переменные по умолчанию обнуляются. Автоматические переменные не имеют значения по умолчанию (они просто получают значение, найденное в памяти), и программист несет ответственность за обеспечение того, чтобы они имели хорошие начальные значения (хотя многие компиляторы будут пытаться подсказать вам, когда вы забудете).

Переменная newCell в вашей функции является автоматической и не инициализируется. Вы должны исправить это быстро. Либо введите newCell.subcells значимое значение незамедлительно, либо укажите его на NULL, пока вы не выделите для него место. Таким образом, вы сгенерируете нарушение сегментации, если попытаетесь разыменовать его, прежде чем выделять для него память.

Хуже того, вы возвращаете Cell по значению, но присваиваете его 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;
}

хотя я оставил вам несколько проблем, чтобы выбить ..

И обратите внимание, что вы должны отслеживать все биты памяти, которые в конечном итоге должны быть освобождены ...

17 голосов
/ 03 марта 2009

Нет значения по умолчанию для вашего указателя. Ваш указатель будет указывать на то, что он хранит в настоящее время. Как вы еще не инициализировали, строка

newCell.subcells[i] = ...

Эффективно обращается к некоторой неопределенной части памяти. Помните, что подэлементы [i] эквивалентны

*(newCell.subcells + i)

Если левая сторона содержит мусор, вы добавите i к значению мусора и получите доступ к памяти в этом неопределенном месте. Как вы правильно сказали, вам придется инициализировать указатель так, чтобы он указывал на некоторую допустимую область памяти:

newCell.subcells = malloc(bytecount)

После какой строки вы можете получить доступ к такому количеству байтов. Что касается других источников памяти, существуют разные виды хранилищ, которые все используют. Какой тип вы получите, зависит от того, какой у вас объект и какой класс хранилища вы сообщаете компилятору.

  • malloc возвращает указатель на объект без типа. Вы можете указать указатель на эту область памяти, и тип объекта фактически станет типом указателя на тип объекта. Память не инициализируется каким-либо значением, и доступ обычно медленнее. Полученные таким образом объекты называются allocated objects.
  • Вы можете размещать объекты по всему миру. Их память будет обнулена. За очки вы получите NULL-указатели, а для поплавков вы также получите правильный ноль. Вы можете положиться на правильное начальное значение.
  • Если у вас есть локальные переменные, но используется спецификатор класса хранения static, то у вас будет то же правило начальных значений, что и для глобальных объектов. Память обычно распределяется так же, как глобальные объекты, но это ни в коем случае не является необходимостью.
  • Если у вас есть локальные переменные без какого-либо спецификатора класса хранения или с auto, тогда ваша переменная будет размещена в стеке (даже если это не определено в C, это то, что компиляторы делают практически). Вы можете взять его адрес, и в этом случае компилятору придется пропустить оптимизацию, например, поместить ее в регистры.
  • Локальные переменные, используемые со спецификатором класса хранилища register, помечаются как имеющие специальное хранилище. В результате вы не можете больше узнать его адрес. В последних компиляторах обычно нет необходимости использовать register из-за их сложных оптимизаторов. Если вы действительно эксперт, то вы можете получить некоторую производительность, если будете его использовать.

У объектов есть связанные сроки хранения, которые можно использовать для отображения различных правил инициализации (формально они определяют только как долго, по крайней мере, объекты живут). Объекты, объявленные с auto и register, имеют автоматическую продолжительность хранения и инициализируются , а не . Вы должны явно инициализировать их, если хотите, чтобы они содержали какое-то значение. Если вы этого не сделаете, они будут содержать то, что компилятор оставил в стеке до того, как они начали жизнь. Объектам, которые выделены 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 указателя в вашем дереве. Это оставлено для вас, чтобы написать это:)

4 голосов
/ 03 марта 2009

Все, что не выделено в куче (через malloc и аналогичные вызовы), размещается в стеке. Из-за этого все, что создано в конкретной функции без malloc 'd, будет уничтожено, когда функция завершится. Это включает возвращенные объекты; когда стек разматывается после вызова функции, возвращаемый объект копируется в пространство, выделенное для него в стеке функцией вызывающей стороны.

Предупреждение: Если вы хотите вернуть объект, на который есть указатели на другие объекты, убедитесь, что объекты, на которые он указывает, созданы в куче, а еще лучше, создайте этот объект в куче Кроме того, если только он не предназначен для того, чтобы выжить после функции, в которой он создан.

3 голосов
/ 03 марта 2009

Мой вопрос: как C выделяет память, когда я фактически не выполнял malloc () достаточный объем памяти? Что по умолчанию?

Чтобы не выделять память. Вы должны просто создать его в стеке или динамически.

В вашем примере подэлементы указывают на неопределенное местоположение, что является ошибкой. Ваша функция должна вернуть указатель на структуру Cell в какой-то момент.

0 голосов
/ 03 марта 2009

Я собираюсь притвориться, что я компьютер, и читаю этот код ...

typedef struct Cell {
  struct Cell* subcells;
}

Это говорит мне:

  • У нас есть тип структуры с именем Cell
  • Содержит указатель с именем subcells
  • Указатель должен быть на что-то типа struct Cell

Это не говорит мне, идет ли указатель на одну ячейку или массив ячеек. Когда создается новая ячейка, значение этого указателя не определено до тех пор, пока ему не будет присвоено значение. Плохие новости - использовать указатели перед их определением.

Cell makeCell(int dim) {
  Cell newCell;

Новая структура ячейки с неопределенным указателем подэлементов. Все, что это делает, это зарезервирует небольшой кусок памяти для вызова newCell, который имеет размер структуры Cell. Это не меняет значения, которые были в этой памяти - они могут быть чем угодно.

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

Чтобы получить newCell.subcells [i], выполняется вычисление для смещения от подэлементов на i, то есть разыменовано . В частности, это означает, что значение извлекается из этого адреса памяти. Возьмем, к примеру, i == 0 ... Тогда мы будем разыменовывать указатель на субэлементы (без смещения). Поскольку подэлементы не определены, это может быть что угодно. Буквально что угодно! Таким образом, это будет запрашивать значение откуда-то совершенно случайно в памяти. Там нет никакой гарантии ничего с результатом. Это может что-то напечатать, это может привести к сбою. Это определенно не должно быть сделано.

  }

  return newCell;
}

Каждый раз, когда вы работаете с указателем, важно убедиться, что он установлен в значение, прежде чем разыменовывать его. Поощряйте, чтобы ваш компилятор предупреждал вас, многие современные компиляторы могут уловить подобные вещи. Вы также можете задать указатели cutesy значения по умолчанию, например 0xdeadbeef (да! Это число в шестнадцатеричном, это просто слово, так что это выглядит забавно), чтобы они выделялись. (Параметр% p для printf полезен для отображения указателей в виде грубой формы отладки. Программы отладчика также могут показать их довольно хорошо.)

0 голосов
/ 03 марта 2009

На самом деле можно выделить три раздела: данные, стек и куча.

В случае, если вы упомянули, он будет размещен в стеке. Проблема с выделением чего-либо в стеке состоит в том, что это допустимо только на время действия функции. Как только ваша функция возвращается, эта память восстанавливается. Таким образом, если вы вернете указатель на что-то, размещенное в стеке, этот указатель будет недействительным. Если вы вернете реальный объект (но не указатель), копия объекта будет автоматически сделана для использования вызывающей функцией.

Если бы вы объявили ее как глобальную переменную (например, в файле заголовка или вне функции), она была бы размещена в разделе данных памяти. Память в этом разделе распределяется автоматически при запуске вашей программы и автоматически освобождается при ее завершении.

Если вы выделяете что-то в куче с помощью malloc (), эта память хороша до тех пор, пока вы хотите ее использовать - до тех пор, пока вы не вызовете free (), и в этот момент она освобождается. Это дает вам возможность распределять и освобождать память по мере необходимости (в отличие от использования глобальных переменных, где все выделяется заранее и освобождается только после завершения вашей программы).

0 голосов
/ 03 марта 2009

Собираюсь ли я в конечном итоге получить доступ к счастливым лицам, хранящимся где-то в памяти, или, возможно, переписать ранее существующие ячейки, или как?

Тебе повезло, что у тебя счастливое лицо. В один из тех неудачных дней он мог бы полностью стереть вашу систему;)

Мой вопрос: как C выделяет память, когда я фактически не выполнял malloc () достаточный объем памяти?

Это не так. Тем не менее, когда вы определяете ячейку newCell, указатель subCells инициализируется значением мусора. Это может быть 0 (в этом случае вы получите сбой) или какое-то целое число, достаточно большое, чтобы он выглядел как фактический адрес памяти. В таких случаях компилятор с радостью извлечет любую находящуюся там ценность и вернет ее вам.

Что по умолчанию?

Это поведение , если вы не инициализируете свои переменные. А ваша makeCell функция выглядит немного недостаточно развитой.

0 голосов
/ 03 марта 2009

Локальные переменные «размещены» в стеке. Стек - это заранее выделенный объем памяти для хранения этих локальных переменных. Переменные перестают быть действительными, когда функция завершается, и будут перезаписаны тем, что будет дальше.

В вашем случае код ничего не делает, поскольку он не возвращает ваш результат. Кроме того, указатель на объект в стеке также перестанет быть действительным при выходе из области, так что я предполагаю, что в вашем точном случае (кажется, вы делаете связанный список), вам нужно будет использовать malloc (). *

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...