Вопрос

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

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

Это было полезно?

Решение

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

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

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

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

Стек хорош тем, что он автоматический, но у него есть и два недостатка:(1) Компилятору необходимо заранее знать, насколько велики переменные, и (б) пространство стека несколько ограничено.Например:в 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 удивляются, узнав, что большие и сложные встроенные программы и игры на 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].Вы не можете просто положить это в стопку, потому что тогда, хм...вы получите переполнение стека.

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

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

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

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;
}

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

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

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

@Евро Мичелли

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

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

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

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

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

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

В 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;
   return np;
}

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

int *my_special_array;
my_special_array = malloc(sizeof(int) * number_of_element);
for(i=0; i

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

struct data{ int data_type; long data_in_mem; }; struct animal{/*something*/}; struct person{/*some other thing*/}; struct animal* read_animal(); struct person* read_person(); /*In main*/ struct data sample; sampe.data_type = input_type; switch(input_type){ case DATA_PERSON: sample.data_in_mem = read_person(); break; case DATA_ANIMAL: sample.data_in_mem = read_animal(); default: printf("Oh hoh! I warn you, that again and I will seg fault your OS"); }

Видите ли, длинного значения достаточно, чтобы хранить ЧТО-ЛИБО.Просто не забудьте освободить его, иначе ПОЖАЛЕЕТЕ.Это один из моих любимых способов развлечься на 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 ГБ ОЗУ за всю свою жизнь), а в оконной среде это в значительной степени обязательно, поскольку объекты, которые вы создаете (скажем, кнопки) , должны существовать далеко за пределами области действия какой-либо конкретной функции (или даже класса).

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top