Структуры и алгоритмы компьютерной обработки данных

Распределение памяти. Динамическое выделение памяти

Показывать лекцию целиком

Цель лекции: изучить способы выделения памяти, динамического выделения памяти, связи указателей и динамического распределения памяти, научиться решать задачи с использованием динамического выделения памяти в языке C++.

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

Второй способ, которым С++ может хранить информацию, заключается в использовании системы динамического распределения. При этом способе память распределяется для информации из свободной области памяти по мере необходимости. Область свободной памяти находится между кодом программы с ее постоянной областью памяти и стеком (рис 24.1). Динамическое размещение удобно, когда неизвестно, сколько элементов данных будет обрабатываться.

(рис 24.1) Распределение оперативной памяти для программ на С++

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

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

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

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

  • Указатель (на участок динамической памяти) определен как локальный объект автоматической памяти. В этом случае выделенная память будет недоступна при выходе за пределы блока локализации указателя, и ее нужно освободить перед выходом из блока.
  • Указатель определен как локальный объект статической памяти. Динамическая память, выделенная однократно в блоке, доступна через указатель при каждом повторном входе в блок. Память нужно освободить только по окончании ее использования.
  • Указатель является глобальным объектом по отношению к блоку. Динамическая память доступна во всех блоках, где "виден" указатель. Память нужно освободить только по окончании ее использования.
  • Все переменные, объявленные в программе размещаются в одной непрерывной области памяти, которую называют сегментом данных. Такие переменные не меняют своего размера в ходе выполнения программы и называются статическими. Размера сегмента данных может быть недостаточно для размещения больших объемов информации. Выходом из этой ситуации является использование динамической памяти. Динамическая память – это память, выделяемая программе для ее работы за вычетом сегмента данных, стека, в котором размещаются локальные переменные подпрограмм и собственно тела программы.

    Для работы с динамической памятью используют указатели. С их помощью осуществляется доступ к участкам динамической памяти, которые называются динамическими переменными. Для хранения динамических переменных выделяется специальная область памяти, называемая "кучей".

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

    В С++ используется два способа работы с динамической памятью:

  • использование операций new и delete ;
  • использование семейства функций mallос ( calloc ) (унаследовано из С).
  • Работа с динамической памятью с помощью операций new и delete

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

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

    Синтаксис:

    new ИмяТипа;

    или

    new ИмяТипа [Инициализатор];

    В выделенный участок заносится значение, определяемое инициализатором, который не является обязательным элементом. В случае успешного выполнения new возвращает адрес начала выделенного участка памяти. Если участок нужных размеров не может быть выделен (нет памяти), то операция new возвращает нулевое значение адреса ( NULL ).

    Синтаксис применения операции:

    Указатель = new ИмяТипа [Инициализатор];

    Операция new float выделяет участок памяти размером 4 байта. Операция new int(15) выделяет участок памяти 4 байта и инициализирует этот участок целым значением 15. Синтаксис использования операций new и delete предполагает применение указателей. Предварительно каждый указатель должен быть объявлен:

    тип *ИмяУказателя;

    Например:

    float *pi;   //Объявление переменной pi
    pi=new float; //Выделение памяти для переменной pi
    * pi = 2.25; //Присваивание значения

    В качестве типа можно использовать, например, стандартные типы int, long, float, double, char.

    Оператор new чаще всего используется для размещения в памяти данных определенных пользователем типов, например, структур:

    struct Node {
                 char *Name;
                 int Value;
                 Node *Next
                };
    
    Node *PNode; //объявляется указатель
    
    PNode = new Node; //выделяется память
    
    PNode->Name = "Ata"; //присваиваются значения
    PNode->Value = 1;
    PNode->Next = NULL;

    В качестве имени типа в операции new может быть использован массив:

    new ТипМассива

    При выделении динамической памяти для массива его размеры должны быть полностью определены. Например:

    ptr = new int [10];//10 элементов типа int или 40 байт
    ptr = new int [ ];//неверно, т.к. не определен размер

    Такая операция позволяет выделить в динамической памяти участок для размещения массива соответствующего типа, но не позволяет его инициализировать. В результате выполнения операция new возвратит указатель, значением которого служит адрес первого элемента массива. Например:

    int *n = new int;

    Операция new выполняет выделение достаточного для размещения величины типа int участка динамической памяти и записывает адрес начала этого участка в переменную n. Память под саму переменную n (размера, достаточного для размещения указателя) выделяется на этапе компиляции.

    int *b = new int (10);

    В данном операторе, кроме описанных выше действий, производится инициализация выделенной динамической памяти значением 10.

    int *q = new int [10];

    В этом случае операция new выполняет выделение памяти под 10 величин типа int (массива из 10 элементов) и записывает адрес начала этого участка в переменную q, которая может трактоваться как имя массива. Через имя можно обращаться к любому элементу массива.

    Есть ряд преимуществ использования new. Во-первых, операция new автоматически вычисляет размер необходимой памяти. Нет необходимости в использовании операции sizeof(). Более важно то, что она предотвращает случайное выделение неправильного количества памяти. Во-вторых, операция new автоматически возвращает указатель требуемого типа.

    Для освобождения выделенного операцией new участка памяти используется операции:

    delete указатель;

    Указатель адресует освобождаемый участок памяти, ранее выделенный с помощью операции new. Например:

    delete x;

    Для освобождения памяти, выделенной для массива, используется следующая модификация той же операции:

    delete [ ] указатель;

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

    Пример 1. Демонстрация выполнения операций с динамической памятью.

    #include "stdafx.h"
    #include <iostream>
    using namespace std;
    int _tmain(int argc, _TCHAR* argv[]){
      int *pa, *pb; 
      pa = new int; 
      *pa = 21; 
      pb = pa; 
      cout << *pa << "  " << *pb << "\n";
      pb = new int; 
      *pb = 28; 
      cout << *pa << "  " << *pb << "\n";
      delete pa;
      pa = pb; 
      cout << *pa << "  " << *pb << "\n";
      delete pa; 
      system("pause");
      return 0;
    }

    Результат выполнения программы:

    21  21
    21  28
    28  28

    Работа с динамической памятью с помощью библиотечных функций malloc (calloc) и free

    Средства для динамического выделения и освобождения памяти описаны в заголовочных файлах malloc.h и stdlib.h стандартной библиотеки (файл malloc.h ).

    Функции выделения и освобождения памяти
    Функция Прототип и краткое описание
    malloc void * malloc (unsigned s); возвращает указатель на начало области (блока) динамической памяти длинной в s байт. При неудачном завершении возвращает значение NULL.
    calloc void * calloc (unsigned n, unsigned m); возвращает указатель на начало области (блока) обнуленной динамической памяти, выделенной для размещения n элементов по m байт каждый. При неудачном завершении возвращает значение NULL.
    realloc void * realloc (void * bl, unsigned ns); изменяет размер блока ранее выделенной динамической памяти до размера ns байт, bl – адрес начала изменяемого блока. Если bl равен NULL (память не выделялась), то функция выполняется как malloc.
    free void * free (void * bl); освобождает ранее выделенный участок (блок) динамической памяти, адрес первого байта которого равен значению bl.

    Функции malloc(), calloc() и realloc() динамически выделяют память в соответствии со значениями параметров и возвращают адрес начала выделенного участка памяти. Для универсальности тип возвращаемого значения каждой из этих функций есть void *. Этот указатель можно преобразовать к указателю любого типа с помощью операции явного приведения типа ( тип * ).

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

    Пример 2. Ввести и напечатать в обратном порядке набор вещественных чисел, количество которых заранее не фиксировано, а вводится до начала ввода самих числовых значений.

    #include "stdafx.h"
    #include <iostream>
    using namespace std;
    int _tmain(int argc, _TCHAR* argv[]){
      float* t; //Указатель для выделяемого блока памяти 
      int i,n;
      printf("n=");//n - число элементов
      scanf("%d", n);
      t=(float *)malloc(n*sizeof(float));
      for (i=0; i<n; i++){ //цикл ввода чисел
        printf("x[%d]=",i);
        scanf("%f", t[i]);
      }
      //цикл печати результатов
      for (i=n-1; i>=0; i--){
        printf("\nx[%d]=%f",i,t[i]);
      }
      free(t); //освобождает память
      system("pause");
      return 0;
    }

    В программе int n – количество вводимых чисел типа float, float* t – указатель на начало области, выделяемой для размещения n вводимых чисел. Указатель t принимает значение адреса области, выделяемой для n значений типа float. Доступ к участкам памяти выделенной области выполняется с помощью операции индексирования: t[i] и t[i-1]. Оператор free(t); содержит вызов функции, освобождающей выделяемую ранее динамическую память и связанной с указателем t.

    Ключевые термины

    Динамическая память – это память, выделяемая программе для ее работы за вычетом сегмента данных, стека, в котором размещаются локальные переменные подпрограмм и собственно тела программы.

    Динамические переменные – это переменные, определенные в области динамической памяти.

    Операция выделения динамической памяти – это выделение достаточного для размещения величины участка динамической памяти.

    Операция освобождения динамической памяти – это освобождение участка динамической памяти, выделенного ранее.

    Сегмент данных – это непрерывная область памяти, в которой размещаются объявленные в программе переменные.

    Функция выделения динамической памяти – это функция выделения памяти в соответствии со значениями параметров, возвращающая адрес начала выделенного участка памяти.

    Функция освобождения динамической памяти – это функция освобождения динамической памяти, выделенной ранее.

    Краткие итоги

  • В ходе выполнения программ выделяются статическая и динамическая области памяти.
  • Возможны три варианта работы с динамической памятью: указатель определяется как локальный объект автоматической или статической памяти, указатель является глобальным объектом для блока памяти.
  • Доступ к участкам динамической памяти осуществляется через динамические переменные.
  • Работа с динамической памятью начинается с выделения участка памяти, а завершается освобождением ранее выделенного участка.
  • Выделение и освобождение динамической памяти выполняется с помощью операций или функций для работы с динамической памятью.
  • Лабораторная работа 24. Распределение памяти. Динамическое выделение памяти

    Цель работы: изучить способы выделения памяти, динамического выделения памяти, связи указателей и динамического распределения памяти, научиться решать задачи с использованием динамического выделения памяти в языке C++.

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

    Теоретические сведения.

    Ознакомьтесь с материалом лекции 24.

    Задания к лабораторной работе.

    Выполните приведенные ниже задания.

  • Составьте программу нахождения действительных корней квадратного уравнения, расположив его коэффициенты в динамической памяти.
  • Дана последовательность целых чисел, количество которых заранее не определено. Считать концом последовательности ввод нулевого значения. Ввести и напечатать на разных строках четные и нечетные числа данной последовательности. Реализуйте данную программу двумя способами: 1) с помощью операций new и delete ; 2) с помощью библиотечных функций malloc (calloc) и free.
  • Дана последовательность целых чисел, количество которых заранее не определено. Считать концом последовательности ввод нулевого значения. Вывести на печать числа по закономерности, пока образуется пустая строка.
    an      an-1   ...   a3      a2      a1
    an-1    an-2   ...   a2
    ..................
  • Дана последовательность символов латинского алфавита, количество которых заранее не определено. Считать концом последовательности символ пробела. Вывести на печать гласные буквы из данной последовательности.
  • Указания к выполнению работы.

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

    Следует реализовать каждое задание в соответствии с приведенными этапами:

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

    Отчет по лабораторной работе должен соответствовать следующей структуре.

  • Титульный лист.
  • Словесная постановка задачи. В этом подразделе проводится полное описание задачи. Описывается суть задачи, анализ входящих в нее физических величин, область их допустимых значений, единицы их измерения, возможные ограничения, анализ условий при которых задача имеет решение (не имеет решения), анализ ожидаемых результатов.
  • Математическая модель. В этом подразделе вводятся математические описания физических величин и математическое описание их взаимодействий. Цель подраздела – представить решаемую задачу в математической формулировке.
  • Алгоритм решения задачи. В подразделе описывается разработка структуры алгоритма, обосновывается абстракция данных, задача разбивается на подзадачи. Схема алгоритма выполняется по ЕСПД (ГОСТ 19.003-80 и ГОСТ 19.002-80).
  • Листинг программы. Подраздел должен содержать текст программы на языке программирования С++, реализованный в среде MS Visual Studio 2010.
  • Контрольный тест. Подраздел содержит наборы исходных данных и полученные в ходе выполнения программы результаты.
  • Выводы по лабораторной работе.
  • Ответы на контрольные вопросы.
  • Контрольные вопросы

  • Для чего используется динамическая память в программировании?
  • Какая область памяти выделяется под размещение динамических данных?
  • Как долго хранятся данные в динамической памяти?
  • Какие возможны варианты доступа к динамической памяти?
  • Что возвращает операция выделения динамической памяти в случае успешного выполнения?
  • Что возвращает операция выделения динамической памяти, если участок требуемого размера не может быть выделен?
  • Почему тип функций выделения динамической памяти определен как *void?
  • Почему при завершении работы с динамической памятью ее необходимо освободить? Какие могут быть последствия для работы программы, если не освобождать динамическую память?
  • Существуют ли ограничения на данные при применении к ним операции или функции освобождения динамической памяти?
  • Вернуться к учебному плану