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

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

86 голосов
/ 24 августа 2008

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

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

Ответы [ 12 ]

220 голосов
/ 24 августа 2008

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

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

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

17 голосов
/ 24 августа 2008

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

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

9 голосов
/ 24 августа 2008

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

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

 // Do something with the string...

 free(string); // Free the allocated memory
5 голосов
/ 02 сентября 2008

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

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

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

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

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

4 голосов
/ 30 марта 2009

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

4 голосов
/ 24 августа 2008

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

Пример:

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

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

myString = "abcde";

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

2 голосов
/ 24 августа 2008

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

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

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 ..

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

2 голосов
/ 24 августа 2008

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

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

0 голосов
/ 27 августа 2008

В 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

<p>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):</p>


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

0 голосов
/ 25 августа 2008

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...