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

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

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

Ответ 1

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

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

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

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

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

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

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

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

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

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

free(p);

Что делает этот второй вариант "неприятным бизнесом" в том, что не всегда легко узнать, когда переменная больше не нужна. Забыв освободить переменную, когда она вам не понадобится, она заставит вашу программу потреблять больше памяти, в которой она нуждается. Эта ситуация называется "утечкой". "Просочившаяся" память не может использоваться ни для чего, пока ваша программа не закончится, и ОС не восстановит все свои ресурсы. Даже более неприятные проблемы возможны, если вы освобождаете переменную кучи по ошибке, прежде чем вы на самом деле выполняете ее.

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

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

Ответ 2

Вот пример. Предположим, что у вас есть функция 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");
...

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

Ответ 3

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

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

 // Do something with the string...

 free(string); // Free the allocated memory

Ответ 4

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

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

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

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

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

Ответ 5

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

Ответ 6

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

Пример:

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

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

myString = "abcde";

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

Ответ 7

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

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

Ответ 8

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

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

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,
  • выделение дополнительного пространства для какой-либо зловещей цели,
  • создание пул памяти s..

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

Ответ 9

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

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

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

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

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

Ответ 10

@Euro Micelli

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

Ответ 11

В 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

c. You want to do something REALLY dirty. For example, I would want a struct to represent many kind of data and I don't like union (union looks soooo messy):

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, можно с уверенностью сказать, что вы все еще девственны и что код по-прежнему выглядит хорошо.

Ответ 12

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