Как я могу правильно обрабатывать malloc-неудачу в C, особенно когда есть более одного malloc?
Предположим, что это часть моего кода:
int foo()
{
char *p, *q ;
if((p = malloc(BUFSIZ)) == NULL) {
return ERROR_CODE;
}
if((q = malloc(BUFSIZ)) == NULL) {
free(p)
return ERROR_CODE;
}
/* Do some other work... */
free(p);
free(q);
}
Поскольку возможно, что первая malloc
успешна, но вторая не удалась, я использую free(p)
во втором "обработчике ошибок". Но что, если есть больше malloc
, и что, если я хочу изменить код (корректируя их порядок, добавляя или удаляя некоторые malloc
)?
Я знаю, что в С++ есть такие вещи, как RAII и безопасные исключения, и т.д. Но в целом, как правильно обращаться с ошибкой malloc
в C? (возможно, используя некоторые goto
?)
Ответы
Ответ 1
Ваш код в порядке, но для многих переменных я бы предпочел:
int
foo()
{
char *p = NULL;
char *q = NULL;
int ret = 0;
if (NULL == (p = malloc(BUFSIZ)))
{
ret = ERROR_CODE;
goto error;
}
// possibly do something here
if (NULL == (q = malloc(BUFSIZ)))
{
ret = ERROR_CODE;
goto error;
}
// insert similar repetitions
// hopefully do something here
error:
free (p);
free (q);
return ret;
}
Обратите внимание, что освобождение NULL
определяется как no-op.
Это позволяет избежать n
уровней отступа для переменных n
. Вы можете очистить дескрипторы файлов и т.д. Аналогично (хотя вам нужно поставить условие вокруг close()
).
Теперь, если вы знаете, что можете выделить их все сразу, тогда dasblinkenlight имеет хороший ответ, но здесь другой способ:
int
foo()
{
int ret = 0;
char *p = malloc(BUFSIZ);
char *q = malloc(BUFSIZ);
char *r = malloc(BUFSIZ);
if (!p || !q || !r)
{
ret = ERROR_CODE;
goto exit;
}
// do something
exit:
free(p);
free(q);
free(r);
return ret;
}
Окончательная возможность: если вы действительно хотите выйти из программы на malloc
, попробуйте использовать параметр mallopt
M_CHECK_ACTION
. Это приводит к проверке ошибок malloc()
и вызывает abort()
, возможно, распечатывает полезное сообщение.
На странице man:
NAME
mallopt
- задайте параметры выделения памяти
СИНТАКСИС
#include <malloc.h>
int mallopt(int param, int value);
ОПИСАНИЕ
Функция mallopt()
настраивает параметры, управляющие поведением функций выделения памяти (см. malloc(3)
). Аргумент param
указывает параметр, который необходимо изменить, и value
указывает новое значение для этого параметра.
Для param
могут быть заданы следующие значения:
M_CHECK_ACTION
Установка этого параметра контролирует реакцию glibc при обнаружении различных ошибок программирования (например, освобождение одного и того же указателя дважды). 3 младших значащих бита (2, 1 и 0) значения, присвоенного этому параметру, определяют поведение glibc следующим образом:
Бит 0. Если этот бит установлен, напечатайте однострочное сообщение на stderr
, в котором содержится подробная информация об ошибке. Сообщение начинается с строки "*** glibc detected ***"
, за которой следует имя программы, имя функции выделения памяти, в которой была обнаружена ошибка, краткое описание ошибки и адрес памяти, где была обнаружена ошибка.
Бит 1. Если этот бит установлен, то после печати любого сообщения об ошибке, указанного бит 0, программа завершается вызовом abort(3)
. В версиях glibc с 2.4, если бит 0 также установлен, то между печатью сообщения об ошибке и прерыванием программа также печатает трассировку стека в виде backtrace(3)
и печатает отображение памяти процесса в стиле /proc/[pid]/maps
(см. proc(5)
).
Бит 2: (поскольку glibc 2.4) Этот бит действует только в том случае, если бит 0 также установлен. Если этот бит установлен, то однострочное сообщение, описывающее ошибку, упрощается, чтобы содержать только имя функции, в которой была обнаружена ошибка, и краткое описание ошибки.
Ответ 2
Так как вполне нормально проходить NULL
до free()
, вы можете выделить все, что вам нужно, в "прямой линии", проверить все в один снимок, а затем освободить все, как только вы закончите, независимо от того, независимо от того, выполняете ли вы какую-либо работу:
char *p = malloc(BUFSIZ);
char *q = malloc(BUFSIZ);
char *r = malloc(BUFSIZ);
if (p && q && r) {
/* Do some other work... */
}
free(p);
free(q);
free(r);
Это работает до тех пор, пока нет промежуточных зависимостей, т.е. у вас нет структур с многоуровневыми зависимостями. Когда вы это сделаете, неплохо определить функцию для освобождения такой структуры, не предполагая, что все блоки памяти не являются NULL
.
Ответ 3
Для большого количества распределений я бы потратил время на создание диспетчера памяти, который отслеживает распределения. Таким образом, вам никогда не придется беспокоиться о утечках, независимо от того, выполняется ли функция.
Общая идея заключается в создании обертки для malloc
, которая записывает успешные распределения, а затем освобождает их по запросу. Чтобы освободить память, вы просто передаете специальный размер функции-обертки. Использование размера 0
для свободной памяти подходит, если вы знаете, что ни одно из ваших фактических распределений не будет для блоков размером 0
. В противном случае вы можете использовать ~0ULL
в качестве размера запроса.
Вот простой пример, который позволяет до 100 распределений между frees.
#define FREE_ALL_MEM 0
void *getmem( size_t size )
{
static void *blocks[100];
static int count = 0;
// special size is a request to free all memory blocks
if ( size == FREE_ALL_MEM )
{
for ( int i = 0; i < count; i++ )
free( blocks[i] );
count = 0;
return NULL;
}
// using a linked list of blocks would allow an unlimited number of blocks
// or we could use an array that can be expanded with 'realloc'
// but for this example, we have a fixed size array
if ( count == 100 )
return NULL;
// allocate some memory, and save the pointer in the array
void *result = malloc( size );
if ( result )
blocks[count++] = result;
return result;
}
int foo( void )
{
char *p, *q;
if ( (p = getmem(BUFSIZ)) == NULL ) {
return ERROR_CODE;
}
if ( (q = getmem(BUFSIZ)) == NULL ) {
getmem( FREE_ALL_MEM );
return ERROR_CODE;
}
/* Do some other work... */
getmem( FREE_ALL_MEM );
return SUCCESS_CODE;
}
Ответ 4
Это вопрос привычки, но я предпочитаю:
int returnFlag = FAILURE;
if ((p = malloc...) != NULL)
{
if ((q = malloc..) != NULL)
{
// do some work
returnFlag = SUCCESS; // success only if it is actually success
free(q);
}
free(p);
}
return returnFlag; // all other variants are failure
Ответ 5
ЕСЛИ вы ожидаете выделить большое количество предметов, это может стать беспорядочным. Старайтесь избегать подхода "goto". Не из-за старой "goto is bad" этики, а потому, что этот способ действительно может лежать безумием и утечками памяти.
Это немного перебор для небольших чисел malloc, но вы можете рассмотреть что-то вроде этого:
void free_mem(void **ptrs, size_t len)
{
for (size_t i = 0; i < len; ++i)
{
free(ptrs[i]);
ptrs[i] = NULL;
}
}
int foo(...)
{
void *to_be_freed[N];
int next_ptr = 0;
for (size_t i = 0; i < N; ++i) to_be_freed[i] = NULL;
p = malloc(..);
if (!p)
{
free_mem(to_be_freed,N);
return ERROR_CODE;
}
to_be_freed[next_ptr++] = p;
// Wash, rinse, repeat, with other mallocs
free_mem(to_be_freed,N)
return SUCCESS;
}
В действительности вы можете, возможно, обернуть malloc чем-то, что отслеживает это. Поместите массив и размер массива в структуру и передайте это с желаемым размером выделения.
Ответ 6
Я думаю, что первый ответ является наиболее общей целью, поскольку он может использоваться для ошибок, отличных от тех, которые вызваны malloc. Однако я бы удалил gotos и использовал один проход в цикле, как это.
int foo()
{
char *p = NULL;
char *q = NULL;
int ret = 0;
do {
if (NULL == (p = malloc(BUFSIZ)))
{
ret = ERROR_CODE;
break;
}
// possibly do something here
if (NULL == (q = malloc(BUFSIZ)))
{
ret = ERROR_CODE;
break;
}
// insert similar repetitions
// hopefully do something here
} while(0);
free (p);
free (q);
return ret;
}