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

90

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

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

The.Anti.9
источник
Хорошее место для изучения G4G
EsmaeelE

Ответы:

231

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

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

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

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

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

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

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

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

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

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

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

Евро Мичелли
источник
3
Если вы хотите поместить что-то в стек, но не знаете, насколько он велик во время компиляции, alloca () может увеличить кадр стека, чтобы освободить место. Freea () отсутствует, весь кадр стека выталкивается при возврате функции. Использование alloca () для больших объемов памяти чревато опасностями.
DGentry,
1
Может быть, вы могли бы добавить одно или два предложения о расположении в памяти глобальных переменных
Михаэль
В C не приводите возвращения malloc(), его причины UB, (char *)malloc(size);см stackoverflow.com/questions/605845/...
EsmaeelE
17

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

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

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

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

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

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

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

Марк Харрисон
источник
Мне этот ответ больше нравится. Но у меня есть небольшой побочный вопрос. Я бы ожидал, что что-то подобное будет решено с помощью библиотек, разве нет библиотеки, которая бы точно имитировала основные типы данных и добавляла бы к ним функции освобождения памяти, чтобы при использовании переменных они также освобождались автоматически?
Лоренцо
Ни один из них не входит в стандарт. Если вы перейдете на C ++, вы получите строки и контейнеры, которые автоматически управляют памятью.
Марк Харрисон
Понятно, а есть сторонние библиотеки? Не могли бы вы назвать их?
Лоренцо
9

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

Джереми Рутен
источник
5

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

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

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

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

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

Билл Форстер
источник
4

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

Пример:

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

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

Крис BC
источник
Здесь вы выделяете 5 байтов. Освободите его, назначив указатель. Любая попытка освободить этот указатель приводит к неопределенному поведению. Примечание. C-строки не перегружают оператор =, поскольку нет копии.
Мартин Йорк,
Хотя это действительно зависит от используемого вами malloc. Многие операторы malloc выравниваются по 8 байтам. Итак, если этот malloc использует систему верхнего / нижнего колонтитула, malloc зарезервирует 5 + 4 * 2 (4 байта для верхнего и нижнего колонтитула). Это будет 13 байтов, а malloc просто даст вам дополнительные 3 байта для выравнивания. Я не говорю, что это хорошая идея, потому что это будет только для систем, у которых malloc работает так, но по крайней мере важно знать, почему неправильные действия могут сработать.
kodai
Локи: Я отредактировал ответ, чтобы использовать strcpy()вместо =; Я предполагаю, что это было намерением Криса BC.
echristopherson
Я верю, что в современных платформах аппаратная защита памяти предотвращает перезапись процессами пользовательского пространства адресных пространств других процессов; вместо этого вы получите ошибку сегментации. Но это не часть C как таковая.
echristopherson
4

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

Эрнан
источник
2

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

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

Serge
источник
2

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

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

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

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

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

Андерс Эурениус
источник
Изучение структур данных - следующий ключевой шаг в понимании управления памятью. Изучение алгоритмов для надлежащего управления этими структурами покажет вам подходящие методы преодоления этих препятствий. Вот почему вы найдете структуры данных и алгоритмы, преподаваемые на одних и тех же курсах.
aj.toulan
0

@ Euro Micelli

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

Джонатан Бранам
источник
0

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

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

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

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

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

Евро Мичелли
источник
@echristopherson, спасибо. Вы правы - но обратите внимание, что этот вопрос / ответ был с августа 2008 года, до того, как Stack Overflow даже был в публичной бета-версии. Тогда мы все еще думали, как сайт должен работать. Формат этого вопроса / ответа не обязательно следует рассматривать как образец того, как использовать SO. Благодаря!
Euro Micelli
Ах, спасибо, что указали на это - тогда я не понимал, что этот аспект сайта все еще был изменен.
echristopherson 01
0

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

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

struct pair {
   int val;
   пара структур * следующий;
}

struct pair * new_pair (int val) {
   пара структур * np = malloc (sizeof (пара структур));
   np-> val = val;
   np-> next = NULL;
   return np;
}

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

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

c. Вы хотите сделать что-то ДЕЙСТВИТЕЛЬНО грязное. Например, я хотел бы, чтобы структура представляла разные типы данных, и мне не нравится объединение (объединение выглядит так беспорядочно):

struct data { int data_type; long data_in_mem; }; struct animal {/ * что-то * /}; struct person {/ * что-то другое * /}; структура животное * read_animal (); структура человека * read_person (); / * В главном * / образец данных структуры; sampe.data_type = input_type; switch (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, можно с уверенностью сказать, что вы все еще девственник, и что код по-прежнему выглядит хорошо.

магия
источник
«Видите ли, длинного значения достаточно, чтобы вместить ВСЕ» -: / о чем вы говорите, в большинстве систем длинное значение составляет 4 байта, точно так же, как int. Единственная причина, по которой здесь помещаются указатели, заключается в том, что размер long совпадает с размером указателя. Однако вам действительно стоит использовать void *.
Score_Under
-2

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

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

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

TheSmurf
источник
1
Хех, да, это же C ++, не так ли? Удивительно, что мне потребовалось пять месяцев, чтобы позвонить мне по этому поводу.
TheSmurf