Я всегда слышал, что в C нужно действительно следить за тем, как вы управляете памятью. И я все еще начинаю изучать C, но до сих пор мне вообще не приходилось выполнять какие-либо действия, связанные с управлением памятью ... Я всегда представлял, что мне придется выпускать переменные и делать разные уродливые вещи. Но похоже, что это не так.
Может ли кто-нибудь показать мне (с примерами кода) пример того, когда вам нужно будет заняться «управлением памятью»?
Ответы:
Есть два места, куда переменные можно поместить в память. Когда вы создаете такую переменную:
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 #, которые используют другой подход, когда кучу очищают самостоятельно. Этот второй метод, называемый «сборкой мусора», намного проще для разработчика, но вы платите штраф в виде накладных расходов и производительности. Это баланс.
(Я замалчил многие детали, чтобы дать более простой, но, надеюсь, более уравновешенный ответ)
источник
malloc()
, его причины UB,(char *)malloc(size);
см stackoverflow.com/questions/605845/...Вот пример. Предположим, у вас есть функция 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 с удивлением узнают, что большие и сложные встроенные программы на C и игровые программы были созданы без использования распределения кучи.
источник
Здесь есть несколько отличных ответов о том, как выделить и освободить память, и, на мой взгляд, более сложной стороной использования C является обеспечение того, чтобы единственная память, которую вы используете, - это память, которую вы выделили - если это сделано неправильно, что вы закончите Это двоюродный брат этого сайта - переполнение буфера - и вы можете перезаписывать память, используемую другим приложением, с очень непредсказуемыми результатами.
Пример:
int main() { char* myString = (char*)malloc(5*sizeof(char)); myString = "abcd"; }
На этом этапе вы выделили 5 байтов для myString и заполнили его «abcd \ 0» (строки заканчиваются на нуль - \ 0). Если ваше выделение строк было
myString = "abcde";
Вы должны были бы назначить «abcde» в 5 байтах, которые вы выделили для своей программы, и завершающий нулевой символ будет помещен в конец этого - часть памяти, которая не была выделена для вашего использования и может быть бесплатно, но в равной степени может использоваться другим приложением - это критическая часть управления памятью, где ошибка будет иметь непредсказуемые (а иногда и неповторимые) последствия.
источник
strcpy()
вместо=
; Я предполагаю, что это было намерением Криса BC.Следует помнить, что всегда инициализируйте ваши указатели значением 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; }
Естественно, вам нужны еще несколько функций, но в основном это то, для чего вам нужно управление памятью. Я должен отметить, что есть ряд уловок, которые возможны при "ручном" управлении памятью, например,
Получите хороший отладчик ... Удачи!
источник
@ Euro Micelli
Один минус, который следует добавить, заключается в том, что указатели на стек больше недействительны, когда функция возвращается, поэтому вы не можете вернуть указатель на переменную стека из функции. Это распространенная ошибка и основная причина, по которой нельзя обойтись только переменными стека. Если ваша функция должна возвращать указатель, вам нужно выполнить malloc и заняться управлением памятью.
источник
Вы, конечно, правы. Я считаю, что так было всегда, хотя у меня нет копии K&R чтобы проверить.
Мне не нравится много неявных преобразований в C, поэтому я предпочитаю использовать приведение типов, чтобы сделать «магию» более заметной. Иногда это помогает читабельности, иногда нет, а иногда заставляет компилятор обнаруживать тихую ошибку. Тем не менее, у меня нет твердого мнения по этому поводу, так или иначе.
Да ... ты поймал меня там. Я провожу гораздо больше времени на C ++, чем на C. Спасибо, что заметили это.
источник
В C у вас есть два разных варианта. Во-первых, вы можете позволить системе управлять памятью за вас. Или вы можете сделать это самостоятельно. Как правило, вы хотели бы придерживаться первого как можно дольше. Однако автоматически управляемая память в C чрезвычайно ограничена, и вам нужно будет вручную управлять памятью во многих случаях, например:
а. Вы хотите, чтобы переменная пережила функции, и вы не хотите иметь глобальную переменную. пример:
б. вы хотите иметь динамически выделяемую память. Самый распространенный пример - массив без фиксированной длины:
Видите ли, длинного значения достаточно, чтобы вместить ВСЕ. Просто не забудьте освободить его, иначе вы пожалеете. Это один из моих любимых приемов, позволяющих развлечься в C: D.
Однако, как правило, вам следует держаться подальше от любимых приемов (T___T). Рано или поздно вы сломаете свою ОС, если будете использовать их слишком часто. Пока вы не используете * alloc и free, можно с уверенностью сказать, что вы все еще девственник, и что код по-прежнему выглядит хорошо.
источник
Конечно. Если вы создаете объект, который существует вне области видимости, в которой вы его используете. Вот надуманный пример (имейте в виду, что мой синтаксис будет отключен; мой 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 ГБ ОЗУ за все время своего существования), а в оконной среде это в значительной степени обязательно, так как объекты, которые вы создаете (например, кнопки) , должны существовать вне области видимости какой-либо конкретной функции (или даже класса).
источник