Лучший способ справиться с распределением памяти в C? - PullRequest
12 голосов
/ 06 апреля 2009

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

В C ++ у меня есть конструкторы и деструкторы, у меня есть довольно простые функции new и delete, и я знаю, как инкапсулировать их с помощью RAII, используя умные указатели и внутри классов.

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

Когда я писал это, я понял, что это больше вопрос о том, как я понимаю поток С, чем что-либо еще, но один вопрос за раз.

Редактировать : Спасибо за ответы, но мне нужно перефразировать себя.

Когда я говорю, что я использую RAII и умные указатели для C ++, я не хочу того же для C, я знаю, что это не то же самое. Но то, как я управляю распределением памяти в C ++, связано с этими методами.

Например, в моих классах я динамически добавляю и уничтожаю память, которую использует мой класс. Таким образом, я могу достичь своего рода инкапсуляции, мне не нужно знать, когда / как / почему класс обрабатывает свою память, он просто делает. Это означает, что я могу «скрыть» низкую обработку памяти и просто сосредоточиться на нескольких «больших» классах.

То, что я хочу знать, это то, что является лучшим методом обработки памяти в C? Для меня нет классов с конструкторами / деструкторами. Хорошо ли выделять память в начале функции или использовать функцию, которая ее создает для меня? И как мне снова их освободить?

Это широкие вопросы, и они отличаются от ситуации к ситуации, но как вы предпочитаете с этим справляться? Какие советы и уроки вы можете дать?

Ответы [ 12 ]

18 голосов
/ 07 апреля 2009

Частично путаница заключается в том, что по своей природе это труднее в C. malloc и free подобны new, а delete: malloc выделяет новую память и возвращает указатель на эту память. free делает эту память доступной снова, , если это память, которая была выделена с помощью malloc . В противном случае он просто создает хэш некоторого фрагмента памяти. Это не волнует.

Важной особенностью malloc / free является выбор и постоянное поддержание дисциплинированного использования. Вот несколько советов:

ВСЕГДА проверяйте возвращаемый указатель от malloc на NULL

if((p = (char *) malloc(BUFSIZ)) == NULL {
   /* then malloc failed do some error processing. */
}

Для безопасности ремней и подтяжек установите указатель на NULL после его освобождения.

free(p);
p = NULL ;

попытаться malloc и освободить кусок памяти в той же области, если это возможно:

 {  char * p ;
   if((p = malloc(BUFSIZ)) == NULL {
       /* then malloc failed do some error processing. */
   }

 /* do your work. */

   /* now you're done, free the memory */

   free(p);
   p = NULL ;  /* belt-and suspenders */
 }

Если вы не можете, проясните, что вы возвращаете malloc 'ed память, чтобы вызывающий мог ее освободить.

 /* foo: do something good, returning ptr to malloc memory */
 char * foo(int bar) {
     return (char *) malloc(bar);
 }
10 голосов
/ 06 апреля 2009

Пока я писал это, я понял это больше вопрос обо мне понимание потока С, чем что-нибудь еще, но один вопрос на время.

Честно говоря, я думаю, вам следует прочитать K & R , если вы этого не сделали.

7 голосов
/ 07 апреля 2009

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

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

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

Вам стоит взглянуть на проект Apache Portable Runtime. У них есть библиотека пула памяти (документы на http://apr.apache.org/docs/apr/1.3/group__apr__pools.html). Если APR слишком сложен для погружения, вы можете реализовать очень простой пул памяти, используя три функции и структуру данных связанного списка. Псевдокод будет что-то вроде:

struct Pool {
  void* memoryBlock;
  struct Pool *next;
}

struct Pool *createPool(void) {
  /* allocate a Pool and return it */
}

void addToPool(struct Pool *pool, void *memoryBlock) {
  /* create a new Pool node and push it onto the list */
}

void destroyPool(struct Pool *pool) {
  /* walk the list, free each memory block then free its node */
}

Использование пула выглядит примерно так:

int main(void) {
  struct Pool *pool = createPool();
  /* pool is empty */

  doSomething(pool);

  /* pool full of crap, clean it up and make a new one */
  destroyPool(pool);
  pool = createPool();
  /* new pool is empty */

  doMoreStuff(pool);
  destroyPool(pool);

  return 0;
}
6 голосов
/ 07 апреля 2009

Печальная правда в том, что C не предназначен для того, чтобы охватить все эти проблемы управления памятью.

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

Это не обязательно элегантно, но я не думаю, что есть много способов сделать его действительно элегантным, не имитируя ООП в C.

2 голосов
/ 07 апреля 2009

Ну, в C вы должны делать все управление памятью вручную, как вы уже обнаружили. Это не должно удивлять.

1 голос
/ 07 апреля 2009

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

Некоторые другие идеи для рассмотрения.

  1. не соглашайтесь на стандартный malloc / free. найдите лучший вариант с открытым исходным кодом или напишите, который подходит для использования в памяти созданных вами объектов. Кроме того, здесь мы говорим о C, вы собираетесь перезаписывать ваши объекты бесплатно несколько раз и забыть освободить некоторые из них, поэтому встроите некоторую поддержку отладки в ваш malloc. написать свой текст несложно, если вы не можете найти тот, который соответствует вашим потребностям.

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

  3. посмотрите на стратегии, подобные пулам Objective-C

  4. если вы думаете, что понимаете, как работает C ++, то добавить поведение конструктора к распределению памяти в фабрике объектов не так сложно, а использование произвольно созданного свободного может предоставить вам возможность вызывать деструктор в Свободный объект вернул бы вам часть поведения C ++, которое вам понравилось

1 голос
/ 07 апреля 2009

Один из способов «скрыть» выделение и удаление памяти - передать ее в пользовательские контейнеры. Передайте контейнер немодушному объекту. Пусть он беспокоится о malloc, а когда я удаляю объект, пусть он беспокоится о свободе. Конечно, это работает, только если вы храните объект только в одном контейнере. Если у меня есть ссылки на объекты повсеместно, я создам эквивалент методов конструктора и деструктора с синтаксисом c:

 glob* newGlob(); 
 void freeGlob(glob* g);

(под объектом я имею в виду все, на что вы бы указали - не объекты c ++).

1 голос
/ 07 апреля 2009

Я не знаю, как их спрятать и автоматизировать.

C и C ++ - это разные языки. Теперь, скажи это сто раз себе. Будь громким.

Что вы подразумеваете под сокрытием? Что вы подразумеваете под автоматизацией? Можете ли вы опубликовать несколько примеров? Зачем вам нужно прятаться и / или автоматизировать.

Хорошие места в Интернете, чтобы начать с выделения памяти C:

0 голосов
/ 28 января 2015

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

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

Если вы выделяете память в одном C-файле, вы должны освободить ее в том же файле. Возможно, это более ограничительно, чем необходимо, однако, если вы пишете библиотеку, вам определенно следует освободить всю память в вашей библиотеке, которая является malloc'd в вашей библиотеке. Это связано с тем, что в Windows dll имеет кучи, отличную от exe, поэтому неправильное размещение памяти в dll и освобождение ее в exe приводит к повреждению вашей кучи.

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

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

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

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

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

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

Phil

0 голосов
/ 07 апреля 2009

Я не совсем уверен, что вы спрашиваете, но C довольно прост:

struct Foo *f0 = malloc(sizeof(*f));   // alloc uninitialized Foo struct
struct Foo *f1 = calloc(1,sizeof(*f)); // alloc Foo struct cleared to all zeroes

//You usually either want to clear your structs using calloc on allocation, or memset. If 
// you need a constructor, just write a function:
Foo *Foo_Create(int a, char *b)
{
   Foo *r = calloc(1,sizeof(*r));
   r->a = a;
   r->b = strdup(b);
   return r;
}

Here is a simple C workflow with arrays:
struct Foo **foos = NULL;
int n_foos = 0;
...
for(i = 0; i < n_foos; ++i)
{
   struct Foo *f = calloc(1,sizeof(*f));
   foos = realloc(foos,sizeof(*foos)*++n_foos); // foos before and after may be different
   foos[n_foos-1] = f;
}

Если вам нравится, вы можете написать макросы, чтобы помочь:

#define MALLOCP(P) calloc(1,sizeof(*P)) // calloc inits alloc'd mem to zero

Пара баллов:

  • malloc, calloc, realloc и т. Д. Все используют free (), поэтому управлять этими вещами легко. Просто будь последовательным.
  • производительность для mallocs может быть медленной. Кто-то разместил ссылку на это выше. В наши дни ключевое значение имеет быстрое многопоточное распределение, см. Tcmalloc et al. Вы, вероятно, не должны беспокоиться об этом.
  • в современной архитектуре виртуальной памяти malloc почти никогда не выйдет из строя , если вы не находитесь вне виртуального адресного пространства. Если это произойдет, переключитесь на 64 бит;)
  • Убедитесь, что вы используете систему, в которой есть проверка границ, очищенные значения, отслеживание утечек и все такое (см. Valgrind, куча отладки win32 и т.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...