Когда следует использовать функцию динамического выделения памяти вместо прямого объявления переменных?

Ниже приведен пример прямого объявления переменных.

double multiplyByTwo (double input) {
  double twice = input * 2.0;
  return twice;
}

Ниже приведен пример динамического выделения памяти.

double *multiplyByTwo (double *input) {
  double *twice = malloc(sizeof(double));
  *twice = *input * 2.0;
  return twice;
}

Если бы у меня был выбор, я все время буду использовать прямое объявление переменных, потому что код выглядит более читабельным. Когда обстоятельства, когда динамическое распределение памяти более подходит?

4 ответа

Решение

"Если бы у меня был выбор, я все время буду использовать прямое объявление переменных"

Как хорошо, вы должны. Вы не используете кучу памяти, если вам не нужно. Очевидно, возникает вопрос: когда мне нужна динамическая память?

  • Пространство стека ограничено, если вам нужно больше места, вам придется выделять его самостоятельно (представьте большие массивы, например, struct huge_struct array[10000]). Чтобы понять, насколько велик стек, смотрите эту страницу. Обратите внимание, что фактический размер стека может отличаться.
  • C передает аргументы и возвращает значения по значению. Если вы хотите вернуть массив, который распадается на указатель, вы в конечном итоге вернете указатель на массив, который находится за пределами (недопустимым), что приведет к UB. Подобные функции должны распределять память и возвращать указатель на нее.
  • Когда нужно что-то изменить размер (realloc), или вы не знаете, сколько памяти вам понадобится для хранения чего-либо. Массив, который вы объявили в стеке, имеет фиксированный размер, указатель на блок памяти может быть перераспределен (malloc новый блок>= текущий размер блока + memcpy + free оригинальный указатель в основном то, что realloc делает)
  • Когда определенный кусок памяти должен оставаться действительным в течение различных вызовов функций. В некоторых случаях глобальные переменные не подходят (подумайте о многопоточности). Кроме того: глобалы почти во всех случаях считаются плохой практикой.
  • Общие библиотеки обычно используют кучную память. Это потому, что их авторы не могут предположить, что их код будет иметь много свободного места в стеке. Если вы хотите написать разделяемую библиотеку, вы, вероятно, обнаружите, что пишете много кода для управления памятью

Итак, несколько примеров для пояснения:

//perfectly fine
double sum(double a, double b)
{
    return a + b;
}
//call:
double result = sum(double_a, double_b);
//or to reassign:
double_a = (double_a, double_b);
//valid, but silly
double *sum_into(double *target, double b)
{
    if (target == NULL)
        target = calloc(1, sizeof *target);
    *target = b;
    return target;
}
//call
sum_into(&double_a, double_b);//pass pointer to stack var
//or allocate new pointer, set to value double_b
double *double_a = sum_into(NULL, double_b);
//or pass double pointer (heap)
sum_into(ptr_a, double_b);

Возвращение "массивов"

//Illegal
double[] get_double_values(double *vals, double factor, size_t count)
{
    double return_val[count];//VLA if C99
    for (int i=0;i<count;++i)
        return_val[i] = vals[i] * factor;
    return return_val;
}
//valid
double *get_double_values(const double *vals, double factor, size_t count)
{
    double *return_val = malloc(count * sizeof *return_val);
    if (return_val == NULL)
        exit( EXIT_FAILURE );
    for (int i=0;i<count;++i)
        return_val[i] = vals[i] * factor;
    return return_val;
}

Необходимость изменить размер объекта:

double * double_vals = get_double_values(
    my_array,
    2,
    sizeof my_array/ sizeof *my_array
);
//store the current size of double_vals here
size_t current_size = sizeof my_array/ sizeof *my_array;
//some code here
//then:
double_vals = realloc(
    double_vals,
    current_size + 1
);
if (double_vals == NULL)
    exit( EXIT_FAILURE );
double_vals[current_size] = 0.0;
++current_size;

Переменные, которые должны оставаться в области действия дольше:

struct callback_params * some_func( void )
{
    struct callback_params *foo = malloc(sizeof *foo);//allocate memory
    foo->lib_sum = 0;
    call_some_lib_func(foo, callback_func);
}

void callback_func(int lib_param, void *opaque)
{
    struct callback_params * foo = (struct callback_params *) opaque;
    foo->lib_sum += lib_param;
}

В этом сценарии наш код вызывает некоторую библиотечную функцию, которая обрабатывает что-то асинхронно. Мы можем передать функцию обратного вызова, которая обрабатывает результаты работы библиотеки. Библиотека также предоставляет нам средства для передачи некоторых данных в этот обратный вызов через void *opaque,

call_some_lib_func будет иметь подпись в соответствии с:

void call_some_lib_func(void *, void (*)(int, void *))

Или в более читаемом формате:

void call_some_lib_func(void *opaque, void (*callback)(int, void *))

Так что это функция, называемая call_some_lib_func, что принимает 2 аргумента: void * называется opaqueи указатель на функцию, которая возвращает void и принимает int и a void * в качестве аргументов.

Все, что нам нужно сделать, это разыграть void * к правильному типу, и мы можем манипулировать им. Также обратите внимание, что some_func возвращает указатель на непрозрачный указатель, поэтому мы можем использовать его везде, где нам нужно:

int main ( void )
{
    struct callback_params *params = some_func();
    while (params->lib_sum < 100)
        printf("Waiting for something: %d%%\r", params->lib_sum);
    puts("Done!");
    free(params);//free the memory, we're done with it
    //do other stuff
    return 0;
}

Когда обстоятельства, когда динамическое распределение памяти более подходит?

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

Помимо приведенного выше случая, есть некоторые другие сценарии, такие как

  1. Если мы хотим иметь структуру данных, которая может быть изменена во время выполнения, нам нужно перейти к динамическому распределению памяти.

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

  3. Обычно размер стека будет умеренно ограничен. Если вы хотите создать и использовать огромный массив, лучше использовать динамическое распределение памяти. Это выделит память из кучи.

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

Позже вам нужно будет вручную освободить память.

Прямое объявление попадает в стек и удаляется при выходе из функции. В операторе return происходит то, что копия переменной создается до того, как она будет уничтожена.

Рассмотрим этот пример:

На куче

void createPeople():
    struct person *p = makePerson();
    addToOffice(p);
    addToFamily(p);

Против в стеке

void createPeople():
    struct person p = makePerson();
    addToOffice(p);
    addToFamily(p);

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

Во втором случае создается копия лица для офиса и семьи. Теперь может случиться, что вы измените данные своей копии в офисе, а копия в семье останется прежней.

Поэтому, если вы хотите предоставить нескольким сторонам доступ к одному и тому же объекту, он должен быть в стеке.

Динамическое выделение памяти необходимо, когда вы собираетесь транспортировать данные из локальной области (например, функции).
Кроме того, когда вы не можете знать заранее, сколько памяти вам нужно (например, пользовательский ввод).
И, наконец, когда вы знаете объем необходимой памяти, но он переполняет стек. В противном случае не следует использовать динамическое выделение памяти из-за читабельности, накладных расходов во время выполнения и безопасности.

Другие вопросы по тегам