C Управление памятью

Я всегда слышал, что в Си вы должны действительно наблюдать, как вы управляете памятью. И я все еще начинаю изучать C, но до сих пор мне вообще не приходилось делать какие-либо операции по управлению памятью. Я всегда представлял, что мне нужно выпускать переменные и делать разные уродливые вещи. Но, похоже, это не так.

Может кто-нибудь показать мне (с примерами кода) пример того, когда вам нужно было бы заняться «управлением памятью»?

24.08.2008 06:50:56
Хорошее место для изучения G4G
EsmaeelE 18.12.2017 22:46:20
12 ОТВЕТОВ
РЕШЕНИЕ

Есть два места, где переменные могут быть помещены в память. Когда вы создаете переменную, как это:

int  a;
char c;
char d[16];

Переменные создаются в « стеке ». Переменные стека автоматически освобождаются, когда они выходят из области видимости (то есть, когда код больше не может их достичь). Вы можете услышать, что они называются «автоматическими» переменными, но это вышло из моды.

Многие примеры для начинающих будут использовать только переменные стека.

Стек хорош тем, что он автоматический, но у него также есть два недостатка: (1) компилятор должен заранее знать, насколько велики переменные, и (b) пространство стека несколько ограничено. Например: в Windows при настройках по умолчанию для компоновщика Microsoft стек имеет значение 1 МБ, и не все из них доступны для ваших переменных.

Если вы не знаете во время компиляции, насколько велик ваш массив, или если вам нужен большой массив или структура, вам нужен «план Б».

План Б называется « куча ». Обычно вы можете создавать переменные настолько большие, насколько позволяет операционная система, но вы должны сделать это сами. Более ранние публикации показали вам, как вы можете это сделать, хотя есть и другие способы:

int size;
// ...
// Set size to some value, based on information available at run-time. Then:
// ...
char *p = (char *)malloc(size);

(Обратите внимание, что переменные в куче обрабатываются не напрямую, а через указатели)

Когда вы создаете переменную кучи, проблема заключается в том, что компилятор не может определить, когда вы покончили с этим, поэтому вы теряете автоматическое освобождение. Вот тут и начинается «ручное освобождение», на которое вы ссылались. Теперь ваш код отвечает за решение, когда переменная больше не нужна, и освобождает ее, чтобы можно было использовать память для других целей. Для случая выше, с:

free(p);

Что делает этот второй вариант «неприятным делом», так это то, что не всегда легко узнать, когда переменная больше не нужна. Если вы забудете освободить переменную, когда она вам не нужна, ваша программа будет использовать больше памяти, чем нужно. Эта ситуация называется «утечка». «Утечка» памяти не может быть использована ни для чего, пока ваша программа не завершится и ОС не восстановит все свои ресурсы. Возможны еще более неприятные проблемы, если вы по ошибке освободите переменную кучи до того, как закончите с ней.

В C и C ++ вы несете ответственность за очистку переменных кучи, как показано выше. Однако есть языки и среды, такие как языки Java и .NET, такие как C #, которые используют другой подход, когда куча очищается самостоятельно. Этот второй метод, называемый «сборка мусора», намного проще для разработчика, но вы платите штраф за накладные расходы и производительность. Это баланс.

(Я замалчивал многие детали, чтобы дать более простой, но, надеюсь, более выровненный ответ)

227
24.08.2008 08:21:22
Если вы хотите поместить что-то в стек, но не знаете, насколько оно велико во время компиляции, alloca () может увеличить кадр стека, чтобы освободить место. Там нет freea (), весь кадр стека выталкивается, когда функция возвращается. Использование alloca () для больших выделений чревато опасностью.
DGentry 7.09.2008 02:17:23
Может быть, вы могли бы добавить одно или два предложения о расположении в памяти глобальных переменных
Michael Käfer 18.01.2017 22:02:17
В C не приводите возвращения malloc(), его причины UB, (char *)malloc(size);см stackoverflow.com/questions/605845/...
EsmaeelE 18.12.2017 22:43:14

Вы должны делать «управление памятью», когда хотите использовать память в куче, а не в стеке. Если вы не знаете, насколько большим будет массив до времени выполнения, тогда вам придется использовать кучу. Например, вы можете захотеть сохранить что-то в строке, но не знаете, насколько большим будет его содержимое, пока программа не запустится. В этом случае вы бы написали что-то вроде этого:

 char *string = malloc(stringlength); // stringlength is the number of bytes to allocate

 // Do something with the string...

 free(string); // Free the allocated memory
9
24.08.2008 06:57:26

Конечно. Если вы создаете объект, который существует вне области действия, в которой вы его используете. Вот надуманный пример (имейте в виду, что мой синтаксис будет отключен; мой C ржавый, но этот пример все еще иллюстрирует концепцию):

class MyClass
{
   SomeOtherClass *myObject;

   public MyClass()
   {
      //The object is created when the class is constructed
      myObject = (SomeOtherClass*)malloc(sizeof(myObject));
   }

   public ~MyClass()
   {
      //The class is destructed
      //If you don't free the object here, you leak memory
      free(myObject);
   }

   public void SomeMemberFunction()
   {
      //Some use of the object
      myObject->SomeOperation();
   }


};

В этом примере я использую объект типа SomeOtherClass во время жизни MyClass. Объект SomeOtherClass используется в нескольких функциях, поэтому я динамически распределяю память: объект SomeOtherClass создается при создании MyClass, используется несколько раз в течение срока службы объекта, а затем освобождается после освобождения MyClass.

Очевидно, что если бы это был реальный код, не было бы никакой причины (кроме, возможно, использования стека памяти) для создания myObject таким способом, но этот тип создания / уничтожения объектов становится полезным, когда у вас много объектов, и вы хотите точно контролировать когда они создаются и уничтожаются (например, чтобы ваше приложение не высасывало 1 ГБ ОЗУ в течение всего срока его службы), а в оконной среде это в значительной степени обязательно, поскольку вы создаете объекты (скажем, кнопки) , должны существовать вне пределов какой-либо конкретной функции (или даже класса).

-2
24.08.2008 07:00:35
Хех, да, это C ++, не так ли? Удивительно, что кому-то понадобилось пять месяцев, чтобы позвонить мне.
TheSmurf 21.01.2009 05:59:52

Вот пример. Предположим, у вас есть функция strdup (), которая дублирует строку:

char *strdup(char *src)
{
    char * dest;
    dest = malloc(strlen(src) + 1);
    if (dest == NULL)
        abort();
    strcpy(dest, src);
    return dest;
}

И вы называете это так:

main()
{
    char *s;
    s = strdup("hello");
    printf("%s\n", s);
    s = strdup("world");
    printf("%s\n", s);
}

Вы можете видеть, что программа работает, но вы выделили память (через malloc), не освобождая ее. Вы потеряли свой указатель на первый блок памяти, когда вызывали strdup во второй раз.

Это не имеет большого значения для этого небольшого объема памяти, но рассмотрим случай:

for (i = 0; i < 1000000000; ++i)  /* billion times */
    s = strdup("hello world");    /* 11 bytes */

Теперь вы израсходовали 11 гигабайт памяти (возможно, больше, в зависимости от вашего менеджера памяти), и если вы не потерпели крах, ваш процесс, вероятно, работает довольно медленно.

Чтобы исправить это, вам нужно вызвать free () для всего, что получено с помощью malloc () после того, как вы закончите его использовать:

s = strdup("hello");
free(s);  /* now not leaking memory! */
s = strdup("world");
...

Надеюсь, этот пример поможет!

17
24.08.2008 07:38:05
Мне больше нравится этот ответ. Но у меня есть маленький побочный вопрос. Я ожидал бы, что что-то подобное будет решено с библиотеками, разве нет библиотеки, которая бы имитировала основные типы данных и добавляла к ним функции освобождения памяти, чтобы при использовании переменных они также автоматически освобождались?
Lorenzo 17.08.2011 12:44:05
Ни один из них не является частью стандарта. Если вы зайдете в C ++, вы получите строки и контейнеры, которые выполняют автоматическое управление памятью.
Mark Harrison 18.08.2011 00:44:34
Понятно, есть ли сторонние библиотеки? Не могли бы вы назвать их?
Lorenzo 18.08.2011 08:48:54

Также вы можете использовать динамическое распределение памяти, когда вам нужно определить огромный массив, скажем, int [10000]. Вы не можете просто положить его в стек, потому что тогда, хм ... вы получите переполнение стека.

Другим хорошим примером может быть реализация структуры данных, скажем, связанного списка или двоичного дерева. У меня нет примера кода для вставки, но вы можете легко его погуглить.

2
24.08.2008 07:50:49

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

Пример:

int main() {
    char* myString = (char*)malloc(5*sizeof(char));
    myString = "abcd";
}

На данный момент вы выделили 5 байтов для myString и заполнили его «abcd \ 0» (строки заканчиваются нулем - \ 0). Если ваше распределение строк было

myString = "abcde";

Вы должны назначить «abcde» в 5 байтах, которые вы выделили для вашей программы, и завершающий нулевой символ будет помещен в конце этого - часть памяти, которая не была выделена для вашего использования и может быть бесплатно, но в равной степени может использоваться другим приложением - это критическая часть управления памятью, где ошибка будет иметь непредсказуемые (а иногда и неповторимые) последствия.

4
24.08.2008 07:58:13
Здесь вы выделяете 5 байтов. Освободите его, назначив указатель. Любая попытка освободить этот указатель приводит к неопределенному поведению. Примечание. C-строки не перегружают оператор =, так как копия отсутствует.
Martin York 23.09.2008 00:15:01
Тем не менее, это действительно зависит от malloc, который вы используете. Многие операторы malloc имеют размер 8 байтов. Поэтому, если этот malloc использует систему верхнего / нижнего колонтитула, malloc зарезервирует 5 + 4 * 2 (4 байта для верхнего и нижнего колонтитула). Это было бы 13 байтов, и malloc просто дал бы вам дополнительные 3 байта для выравнивания. Я не говорю, что это хорошая идея, потому что она будет работать только с системами, у которых malloc работает подобным образом, но по крайней мере важно знать, почему неправильная работа может сработать.
kodai 15.12.2009 01:11:28
Локи: я отредактировал ответ, чтобы использовать strcpy()вместо =; Я предполагаю, что это было намерение Криса до н.э.
echristopherson 31.08.2013 19:08:12
Я полагаю, что в современных платформах аппаратная защита памяти не позволяет процессам пользовательского пространства перезаписывать адресные пространства других процессов; вместо этого вы получите ошибку сегментации. Но это не часть С как таковая.
echristopherson 31.08.2013 19:23:30

(Я пишу, потому что чувствую, что ответы пока не совсем верны.)

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

typedef struct listelem { struct listelem *next; void *data;} listelem;

listelem * create(void * data)
{
   listelem *p = calloc(1, sizeof(listelem));
   if(p) p->data = data;
   return p;
}

listelem * delete(listelem * p)
{
   listelem next = p->next;
   free(p);
   return next;
}

void deleteall(listelem * p)
{
  while(p) p = delete(p);
}

void foreach(listelem * p, void (*fun)(void *data) )
{
  for( ; p != NULL; p = p->next) fun(p->data);
}

listelem * merge(listelem *p, listelem *q)
{
  while(p != NULL && p->next != NULL) p = p->next;
  if(p) {
    p->next = q;
    return p;
  } else
    return q;
}

Естественно, вам понадобятся несколько других функций, но в основном это то, для чего вам нужно управление памятью. Я должен отметить, что есть ряд трюков, которые возможны при «ручном» управлении памятью, например,

  • Используя тот факт, что malloc гарантированно (в соответствии со стандартом языка) возвращает указатель, делимый на 4,
  • выделять дополнительное пространство для какой-то зловещей цели,
  • создание пула памяти с ..

Получить хороший отладчик ... Удачи!

2
24.08.2008 14:13:13
Изучение структур данных является следующим ключевым шагом в понимании управления памятью. Изучение алгоритмов для правильного запуска этих структур покажет вам соответствующие методы для преодоления этих препятствий. Вот почему вы найдете Data-структуры и алгоритмы, преподаваемые на одних и тех же курсах.
aj.toulan 4.11.2013 17:47:41

@ Тед Персиваль :
... вам не нужно приводить возвращаемое значение malloc ().

Вы правы, конечно. Я считаю, что это всегда было правдой, хотя у меня нет копии K & R для проверки.

Мне не нравятся многие неявные преобразования в C, поэтому я склоняюсь к использованию приведений, чтобы сделать «магию» более заметной. Иногда это помогает удобочитаемости, иногда - нет, а иногда приводит к тому, что компилятор обнаруживает скрытую ошибку. Тем не менее, у меня нет твердого мнения об этом, так или иначе.

Это особенно вероятно, если ваш компилятор понимает комментарии в стиле C ++.

Да ... ты поймал меня там. Я провожу намного больше времени на C ++, чем на C. Спасибо, что заметил это.

0
23.05.2017 10:31:26
@echristopherson, спасибо. Вы правы - но, пожалуйста, обратите внимание, что этот Q / A был с августа 2008 года, до того, как переполнение стека было даже в публичной бета-версии. Тогда мы все еще выясняли, как должен работать сайт. Формат этого Вопроса / Ответа не обязательно должен рассматриваться как образец того, как использовать SO. Спасибо!
Euro Micelli 31.08.2013 23:50:45
Ах, спасибо за указание на это - я не осознавал, что этот аспект сайта все еще был в движении.
echristopherson 1.09.2013 02:53:12

@ Euro Micelli

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

0
23.05.2017 12:34:30

В C у вас есть два варианта. Во-первых, вы можете позволить системе управлять памятью за вас. Кроме того, вы можете сделать это самостоятельно. Как правило, вы хотели бы придерживаться первого как можно дольше. Однако автоматически управляемая память в C чрезвычайно ограничена, и вам придется вручную управлять памятью во многих случаях, например:

а. Вы хотите, чтобы переменная пережила функции, и вы не хотите иметь глобальную переменную. например:

struct pair {
   int val;
   struct pair * next;
}

struct pair * new_pair (int val) {
   struct pair * np = malloc (sizeof (struct pair));
   np-> val = val;
   np-> next = NULL;
   вернуть np;
}

б. Вы хотите иметь динамически распределенную память. Наиболее распространенный пример - массив без фиксированной длины:

int * my_special_array;
my_special_array = malloc (sizeof (int) * number_of_element);
для (я = 0; я

с. Вы хотите сделать что-то действительно грязное. Например, я хотел бы, чтобы структура представляла многие виды данных, и мне не нравится объединение (объединение выглядит слишком грязно):

struct data { int data_type; long data_in_mem; }; struct animal {/ * что-то * /}; struct person {/ * что-то другое * /}; struct animal * read_animal (); struct person * read_person (); / * В основном * / образец структуры данных; sampe.data_type = input_type; Переключатель (input_type) { case DATA_PERSON: sample.data_in_mem = read_person (); ломать; case DATA_ANIMAL: sample.data_in_mem = read_animal (); по умолчанию: printf («О, хо! Я предупреждаю вас, что снова и буду обвинять вашу ОС»); }

Видите, длинного значения достаточно, чтобы держать НИЧЕГО. Просто не забудьте освободить его, или вы пожалеете. Это один из моих любимых трюков в C: D.

Однако, как правило, вы хотите держаться подальше от ваших любимых трюков (T___T). Вы сломаете свою ОС, рано или поздно, если будете использовать их слишком часто. Пока вы не используете * alloc и free, можно с уверенностью сказать, что вы все еще девственны и что код по-прежнему выглядит красиво.

0
27.08.2008 12:08:54
«Видите, длинного значения достаточно, чтобы содержать НИЧЕГО» -: / о чем вы говорите, в большинстве систем длинное значение составляет 4 байта, точно так же, как int. Единственная причина, по которой он подходит для указателей, заключается в том, что размер long совпадает с размером указателя. Вы действительно должны использовать void *.
Score_Under 26.09.2012 16:30:27

Я думаю, что наиболее краткий способ ответить на этот вопрос - рассмотреть роль указателя в C. Указатель - это легкий, но мощный механизм, который дает вам огромную свободу за счет огромной способности выстрелить себе в ногу.

В C ответственность за то, чтобы ваши указатели указывали на вашу память, принадлежит вам и только вам. Это требует организованного и дисциплинированного подхода, если вы не оставляете указатели, что затрудняет написание эффективного C.

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

Распределение кучи менее присуще языку. По сути, это набор библиотечных вызовов, которые предоставляют вам право собственности на блок памяти заданного размера, пока вы не будете готовы вернуть («освободить») его. Звучит просто, но связано с невыразимым горе программиста. Проблемы просты (освобождение одной и той же памяти дважды или совсем не [утечки памяти], не выделение достаточного количества памяти [переполнение буфера] и т. Д.), Но их трудно избежать и отладить. Строго дисциплинированный подход абсолютно обязателен в практическом плане, но, конечно, язык на самом деле не обязывает его.

Я хотел бы упомянуть другой тип распределения памяти, который игнорировался другими постами. Можно статически распределять переменные, объявляя их вне любой функции. Я думаю, что в общем случае этот тип распределения получает плохой рэп, потому что он используется глобальными переменными. Однако ничто не говорит о том, что единственный способ использовать память, выделенную таким образом, - это недисциплинированная глобальная переменная в куче спагетти-кода. Метод статического размещения можно использовать просто, чтобы избежать некоторых ловушек в куче и методов автоматического распределения. Некоторые программисты на Си с удивлением узнают, что большие и сложные встроенные C-программы и игровые программы созданы без использования кучи.

5
2.09.2008 03:39:29

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

4
29.03.2009 23:00:00