C Управление памятью
Я всегда слышал, что в Си вы должны действительно наблюдать, как вы управляете памятью. И я все еще начинаю изучать C, но до сих пор мне вообще не приходилось делать какие-либо операции по управлению памятью. Я всегда представлял, что мне нужно освобождать переменные и делать разные уродливые вещи. Но, похоже, это не так.
Может кто-нибудь показать мне (с примерами кода) пример того, когда вам нужно было бы заняться "управлением памятью"?
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#, которые используют другой подход, когда куча очищается сама по себе. Этот второй метод, называемый "сборкой мусора", намного проще для разработчика, но вы платите штраф за накладные расходы и производительность. Это баланс.
(Я замалчивал многие детали, чтобы дать более простой, но, надеюсь, более выровненный ответ)
Вот пример. Предположим, у вас есть функция 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");
...
Надеюсь, этот пример поможет!
Вы должны выполнять "управление памятью", когда хотите использовать память в куче, а не в стеке. Если вы не знаете, насколько большим будет массив до времени выполнения, тогда вам придется использовать кучу. Например, вы можете захотеть сохранить что-то в строке, но не знаете, насколько большим будет его содержимое, пока программа не запустится. В этом случае вы бы написали что-то вроде этого:
char *string = malloc(stringlength); // stringlength is the number of bytes to allocate
// Do something with the string...
free(string); // Free the allocated memory
Я думаю, что наиболее краткий способ ответить на этот вопрос - рассмотреть роль указателя в C. Указатель - это легкий, но мощный механизм, который дает вам огромную свободу за счет огромной способности выстрелить себе в ногу.
В C ответственность за то, чтобы ваши указатели указывали на вашу память, принадлежит вам и только вам. Это требует организованного и дисциплинированного подхода, если вы не оставляете указатели, что затрудняет написание эффективного C.
Опубликованные ответы на сегодняшний день сконцентрированы на автоматическом (стеке) и распределении кучи. Использование выделения стека создает автоматически управляемую и удобную память, но в некоторых случаях (большие буферы, рекурсивные алгоритмы) это может привести к ужасной проблеме переполнения стека. Точное знание того, сколько памяти вы можете выделить в стеке, очень зависит от системы. В некоторых встроенных сценариях вашим ограничением может быть несколько десятков байт, в некоторых сценариях рабочего стола вы можете безопасно использовать мегабайты.
Распределение кучи менее присуще языку. По сути, это набор библиотечных вызовов, который предоставляет вам право владения блоком памяти заданного размера до тех пор, пока вы не будете готовы вернуть его ("бесплатно"). Звучит просто, но связано с невыразимым горе программиста. Проблемы просты (освобождение одной и той же памяти дважды или вовсе не [утечки памяти], не выделение достаточного количества памяти [переполнение буфера] и т. Д.), Но их трудно избежать и отладить. Строго дисциплинированный подход абсолютно обязателен в практическом плане, но, конечно, язык на самом деле не обязывает его.
Я хотел бы упомянуть другой тип распределения памяти, который игнорировался другими постами. Можно статически распределять переменные, объявляя их вне любой функции. Я думаю, что в общем случае этот тип распределения получает плохой рэп, потому что он используется глобальными переменными. Однако ничто не говорит о том, что единственный способ использовать память, выделенную таким образом, - это недисциплинированная глобальная переменная в куче спагетти-кода. Метод статического размещения можно использовать просто для того, чтобы избежать некоторых ловушек в куче и методов автоматического распределения. Некоторые программисты на Си с удивлением узнают, что большие и сложные встроенные и игровые программы на Си были созданы без использования кучи.
Здесь есть несколько отличных ответов о том, как выделить и освободить память, и, на мой взгляд, более сложной стороной использования C является обеспечение того, чтобы единственной используемой вами памятью была выделенная вами память - если это не сделано правильно, то, что вы заканчиваете у двоюродного брата этого сайта - переполнение буфера - и вы можете перезаписывать память, которая используется другим приложением, с очень непредсказуемыми результатами.
Пример:
int main() {
char* myString = (char*)malloc(5*sizeof(char));
myString = "abcd";
}
На данный момент вы выделили 5 байтов для myString и заполнили его "abcd\0" (строки заканчиваются нулем - \0). Если ваше распределение строк было
myString = "abcde";
Вы должны назначить "abcde" в 5 байтах, которые вы выделили для вашей программы, и завершающий нулевой символ будет помещен в конце этого - часть памяти, которая не была выделена для вашего использования и может быть бесплатно, но в равной степени может использоваться другим приложением - это критическая часть управления памятью, где ошибка будет иметь непредсказуемые (а иногда и неповторимые) последствия.
Следует помнить, что всегда нужно инициализировать указатели в NULL, поскольку неинициализированный указатель может содержать псевдослучайный действительный адрес памяти, который может заставить ошибки указателя идти вперед бесшумно. Путем принудительного инициализации указателя с помощью NULL вы всегда можете отловить, используете ли вы этот указатель, не инициализируя его. Причина в том, что операционные системы "связывают" виртуальный адрес 0x00000000 с общими исключениями защиты, чтобы перехватить использование нулевого указателя.
Также вы можете использовать динамическое распределение памяти, когда вам нужно определить огромный массив, скажем, int[10000]. Вы не можете просто положить его в стек, потому что тогда, хм... вы получите переполнение стека.
Другим хорошим примером может быть реализация структуры данных, скажем, связанного списка или двоичного дерева. У меня нет примера кода для вставки, но вы можете легко его погуглить.
(Я пишу, потому что чувствую, что ответы пока не совсем верны.)
Стоит упомянуть причину управления памятью, когда у вас есть проблема / решение, требующее создания сложных структур. (Если ваши программы дают сбой, если вы выделяете много места в стеке сразу, это ошибка.) Как правило, первая структура данных, которую вам нужно изучить, это какой-то список. Вот один связанный, в верхней части моей головы:
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,
- выделение дополнительного места для какой-то зловещей цели,
- создание пула памяти с..
Получить хороший отладчик... Удачи!
В 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, можно с уверенностью сказать, что вы все еще девственны и что код по-прежнему выглядит красиво.
Один минус, который нужно добавить, это то, что указатели на стек больше не действительны, когда функция возвращается, поэтому вы не можете вернуть указатель на переменную стека из функции. Это распространенная ошибка и основная причина, почему вы не можете обойтись только с переменными стека. Если ваша функция должна возвращать указатель, вам нужно выполнить malloc и управлять памятью.
@ Тед Персиваль:
... вам не нужно приводить возвращаемое значение malloc().
Вы правы, конечно. Я считаю, что это всегда было правдой, хотя у меня нет копии K&R для проверки.
Мне не нравятся многие неявные преобразования в C, поэтому я склонен использовать приведение, чтобы сделать "магию" более заметной. Иногда это помогает удобочитаемости, иногда - нет, а иногда приводит к тому, что компилятор обнаруживает скрытую ошибку. Тем не менее, у меня нет твердого мнения об этом, так или иначе.
Это особенно вероятно, если ваш компилятор понимает комментарии в стиле C++.
Да... ты поймал меня там. Я трачу намного больше времени на C++, чем на C. Спасибо, что заметил это.
Конечно. Если вы создаете объект, который существует вне области действия, в которой вы его используете. Вот надуманный пример (имейте в виду, что мой синтаксис будет отключен; мой 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 ГБ ОЗУ в течение всего срока его службы), а в оконной среде это в значительной степени обязательно, поскольку вы создаете объекты (скажем, кнопки), должны существовать вне пределов какой-либо конкретной функции (или даже класса).