Различия между динамической памятью и "обычной" памятью

Каковы некоторые технические различия между памятью, выделенной оператором и памятью new, которая распределяется через объявление простой переменной, например int var? Имеет ли С++ какую-либо форму управления автоматической памятью?

В частности, у меня есть пара вопросов. Во-первых, поскольку с динамической памятью вы должны объявить указатель для хранения адреса фактической памяти, с которой работаете, не использует ли динамическая память больше памяти? Я не понимаю, почему указатель нужен вообще, если вы не объявляете массив.

Во-вторых, если бы я сделал простую функцию, например:

int myfunc() { int x = 2; int y = 3; return x+y; }

... И назовите его, освободит ли память, выделенную функцией, как только закончится ее объем существования? Как насчет динамической памяти?

Ответ 1

Примечание: Этот ответ слишком длинный. Я когда-нибудь позабочусь об этом. Между тем, комментарий, если вы можете думать о полезных изменениях.


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

Стек

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

Простой пример

int main(int argc, char * argv[])
{
    int a = 3;
    int b = 4;
    return a + b;
}

В этом случае у вас есть одно поле на полу с переменными argc (целое число), argv (указатель на массив char), a (целое число) и b (целое число).

Более одного окна

int main(int argc, char * argv[])
{
    int a = 3;
    int b = 4;
    return do_stuff(a, b);
}

int do_stuff(int a, int b)
{
    int c = a + b;
    c++;
    return c;
}

Теперь у вас есть поле на полу (для main) с argc, argv, a и b. В верхней части этого окна у вас есть еще один ящик (для do_stuff) с a, b и c.

В этом примере показаны два интересных эффекта.

  • Как вы, наверное, знаете, a и b были переданы по значению. Вот почему есть копия этих переменных в поле для do_stuff.

  • Обратите внимание, что вам не нужно free или delete или что-либо другое для этих переменных. Когда ваша функция вернется, поле для этой функции будет уничтожено.

Переполнение контейнера

    int main(int argc, char * argv[])
    {
        int a = 3;
        int b = 4;
        return do_stuff(a, b);
    }

    int do_stuff(int a, int b)
    {
        return do_stuff(a, b);
    }

Здесь у вас есть поле на полу (для main, как и раньше). Затем у вас есть поле (для do_stuff) с a и b. Затем у вас есть еще одна коробка (для вызова do_stuff), снова с a и b. А потом еще один. И скоро у вас переполнение стека.

Сводка стека

Подумайте о стеке как стеке ящиков. Каждый блок представляет собой выполнение функции, и это поле содержит локальные переменные, определенные в этой функции. Когда функция вернется, это окно будет уничтожено.

Более технические характеристики

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

Куча

Здесь происходит динамическое распределение памяти.

Представьте себе кучу как бесконечный зеленый луг памяти. Когда вы вызываете malloc или new, в куче выделяется блок памяти. Вам предоставляется указатель на доступ к этому блоку памяти.

int main(int argc, char * argv[])
{
    int * a = new int;
    return *a;
}

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

  • a - локальная переменная, поэтому она находится в поле main "

Обоснование для распределения динамической памяти

Конечно, использование динамически распределенной памяти, похоже, направлено на несколько байтов здесь и там для указателей. Однако есть вещи, которые вы просто не можете (легко) сделать без динамического распределения памяти.

Возврат массива

int main(int argc, char * argv[])
{
    int * intarray = create_array();
    return intarray[0];
}

int * create_array()
{
    int intarray[5];
    intarray[0] = 0;
    return intarray;
}

Что здесь происходит? Вы "возвращаете массив" в create_array. В действительности вы возвращаете указатель, который просто указывает на часть create_array "box", которая содержит массив. Что произойдет, когда create_array вернется? Его коробка уничтожена, и вы можете ожидать, что ваш массив станет поврежденным в любой момент.

Вместо этого используйте динамически выделенную память.

int main(int argc, char * argv[])
{
    int * intarray = create_array();
    int return_value = intarray[0];
    delete[] intarray;
    return return_value;
}

int * create_array()
{
    int * intarray = new int[5];
    intarray[0] = 0;
    return intarray;
}

Поскольку возвращаемая функция не изменяет кучу, ваш драгоценный intarray выходит невредимым. Помните delete[] его после того, как вы закончите.

Ответ 2

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

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

Что касается автоматического управления памятью, стандартная библиотека С++ предоставляет для этого auto_ptr.

Ответ 3

Память, выделяемая "новым", заканчивается в куче.

Память, выделяемая в функции, находится внутри функции, где функция помещается в стек.

Читайте о размещении стека и кучи здесь: http://www-ee.eng.hawaii.edu/~tep/EE160/Book/chap14/subsection2.1.1.8.html

Ответ 4

Память, выделенная новым оператором, извлекается из секции памяти, называемой "кучей", тогда как статические распределения для переменных используют раздел памяти, разделяемый с процедурами/функциями-вызовами ( "стек" ).

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

Ответ 5

Большая разница между "динамической" и "обычной" памятью была довольно хорошо отражена в самом вопросе.

Динамическая память не слишком хорошо поддерживается С++ вообще.

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

Вам нужен дополнительный указатель, так как вам нужен доступ к новой памяти. Некоторая память (если она динамическая или нет) является первой из них, которую не может обрабатывать язык программирования. Вы должны иметь к нему доступ. Это делается переменными. Но переменные в таких языках, как С++, хранятся в "обычной" памяти. Поэтому вам нужно иметь "указатели" - указатели - это форма косвенности, которая гласит: "Нет, я не то значение, которое вы ищете, но я указываю на это". Указатели - единственная возможность в С++ для доступа к динамической памяти.

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

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

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

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