Что из себя представляет динамическое выделение памяти
Перейти к содержимому

Что из себя представляет динамическое выделение памяти

  • автор:

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

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

Распределение памяти осуществляется функцией
void * malloc ( size_t size );

Прототип функции находится в файлах и .

Данная функция выделяет память, объём которой в байтах указывается как параметр функции. Функция возвращает адрес выделенной памяти или NULL, если память не может быть выделена. NULL – это специальная константа, которая представляет собой недействительный указатель.

Функция возвращает указатель на пустой тип void * , который может быть преобразован к любому типу и должен быть преобразован к нужному типу. Обязательно надо проверять результат, возвращаемый функцией!

Существуют также ещё две функции, которые позволяют сделать выделение памяти более гибким:
void * calloc ( size_t num , size_t size ); void * realloc ( void * memblock , size_t size );

Первая функция выделяет память под массив из num элементов, каждый из которых имеет размер size . Элементы массива инициализируются нулями.

Вторая функция изменяет размер блока, выделенного ранее функциями malloc , calloc или realloc . Параметр memblock содержит адрес блока для изменения, а параметр size – необходимый размер блока. Положение блока в оперативной памяти может измениться, при этом содержимое будет скопировано.

После того, как отпала необходимость в выделенной памяти, надо освободить её:
void free ( void * memblock );

В языке С++ были введены новые операторы для выделения и освобождения динамической памяти: new и delete .

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

2. Списки

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

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

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

3. Примеры

Пример 2. Работа со списком (см. пример в лекции 6). #include #include #include #include struct S < char str[21]; int a; struct S *next; // Можно объявить указатель на еще не объявленную структуру >; void main( int argc, char *argv[]) < struct S *begin, *cur, *prev; int a, check = 0; char str[21] = ""; FILE *in, *out; char ans; if (argc < 3) < printf("Too few arguments.\n"); return ; >if ((in = fopen(argv[1], «r»)) == NULL) < printf("It is impossible to open file '%s'.\n", argv[1]); return ; >if ((out = fopen(argv[2], «w»)) == NULL) < printf("It is impossible to open file '%s'.\n", argv[2]); fclose(in); return ; >// Строим список, добавляя элементы в конец. Первый элемент вводим отдельно begin = ( struct S *)malloc( sizeof ( struct S)); if (begin == NULL) < printf("Insufficient memory\n"); fclose(in); fclose(out); return ; >fscanf(in, «%s%d», begin->str, &(begin->a)); begin->next = NULL; prev = begin; while (!feof(in)) < cur = ( struct S *)malloc( sizeof ( struct S)); if (cur == NULL) < printf("Insufficient memory\n"); fclose(in); fclose(out); return ; >fscanf(in, «%s%d», cur->str, &(cur->a)); cur->next = NULL; prev->next = cur; prev = cur; > fclose(in); printf(«Use Str for search? «); ans = getche(); if (ans == ‘y’ || ans == ‘Y’) < printf("\nInput string for search: "); scanf("%s",str); >printf(«\nUse A for search? «); ans = getche(); if (ans == ‘y’ || ans == ‘Y’) < check = 1; printf("\nInput A: "); scanf("%d", &a); >// Вывод элементов, удовлетворяющих условию for (cur = begin; cur; cur = cur->next) if ((!*str || strcmp(str, cur->str) == 0) && (!check || a == cur->a)) fprintf(out, «%-30s %3d\n», cur->str, cur->a); // Удаление элементов, не удовлетворяющих условию. Адрес предыдущего элемента понадобится при удалении for (prev = NULL, cur = begin; cur; ) if ((*str && strcmp(str, cur->str) != 0) || (check && a != cur->a)) if (prev == NULL) // Если нужно удалить первый элемент < begin = cur->next; free(cur); cur = begin; > else // Удаление не первого элемента < prev->next = cur->next; free(cur); cur = prev->next; > else // Если удалять не надо, просто меняем указатели < prev = cur; cur = cur->next; > for (cur = begin; cur; cur = cur->next) fprintf(out, «%-30s %3d\n», cur->str, cur->a); fclose(out); // Освобождение памяти, выделенной для списка for (cur = begin; cur; ) < prev = cur; cur = cur->next; free(prev); > begin = NULL; > Содержание

Что из себя представляет динамическое выделение памяти?

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

Какое из следующих определений представляет собой правильную запись операции сложения целого числа и объекта:

Что нужно сделать для освобождения памяти после выполнения такого кода ?

char *a; a = new char[20];

Что произойдёт если операция выделения памяти new завершится неудачно?

Отметьте правильный вариант освобождения всей памяти, выделенной для трехмерного массива для следующей программы

long (*lp)[2][4];lp = new long[3][2][4];

Динамическое распределение памяти C: как работает динамическая память

Lorem ipsum dolor

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

Мы будем очень благодарны

если под понравившемся материалом Вы нажмёте одну из кнопок социальных сетей и поделитесь с друзьями.

Язык C++

хранятся на стеке. Как следует из названия, стек работает с переменными по схеме FILO (first in last out). Управление стеком происходит автоматически. При выходе переменной из области видимости, соответствующая ей в стеке память освобождается. Этот механизм позволяет разработчику не следить за удалением автоматических переменных. Стек работает очень быстро, но имеет ограниченный размер, который обычно не превосходит нескольких мегабайт.

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

Кучу имеет смысл использовать в двух случаях:

  • Необходимо хранить большой объект. Хранение больших объектов на стеке может привести к его переполнению (stack overflow).
  • Автоматическое управление памятью в стеке не соответствует логике программы. Чаще всего такая ситуация возникает, когда созданный объект должен продолжать свое существование после выхода из блока, в котором он был создан. Ниже мы рассмотрим пример.

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

Ручное управление памятью

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

Создать объект в куче можно с помощью оператора new :

int *intptr = new int(7); auto *vecptr = new std::vectorstd::string>(); 

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

delete intptr; delete vecptr; 

Вернемся к примеру из раздела про наследование, в котором мы строили модель символов в графическом текстовом редакторе. Напомним, что мы создали абстрактный базовый класс Character и два его наследника Letter и Digit . Допустим, нам надо реализовать функцию, которая возвращает полиморфный список символов (текст документа). Без динамического выделения нам будет сложно решить эту задачу. Например:

std::listCharacter*> create_document()  // Тут есть проблема Letter l1('a'); Letter l2('b'); Digit d1('1'); Digit d2('2'); return std::listCharacter*>&l1, &l2, &d1, &d2>; > 

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

std::listCharacter*> create_document()  auto* l1 = new Letter('a'); auto* l2 = new Letter('b'); auto* d1 = new Digit('1'); auto* d2 = new Digit('2'); return std::listCharacter*>l1, l2, d1, d2>; > 

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

// здесь есть проблема void print_document()  auto doc = create_document(); for (auto item : doc)  cout  <item; > > 

Использование функции print_document приводит к утечке памяти: при каждом ее вызове в куче выделяется память, которая никогда не освобождается. Более аккуратная реализация выглядит так:

void print_document()  auto doc = create_document(); for (auto item : doc)  cout  <item; > // здесь может быть проблема, которую мы обсудим позже for (auto item : doc)  delete item; > > 

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

Существуют версии операторов new и delete для создания и удаления массивов объектов:

int* p = new int[10]; // выделяем массив из 10 переменных типа int delete[] p; 

При освобождении памяти важно использовать правильную версию оператора delete , что дополнительно усложняет разработку программ с ручным управлением памятью. Хорошей новостью является то, что оператор delete[] сам определяет размер удаляемого массива.

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

Владение ресурсами и идиома RAII

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

Ясно организовать владение ресурсами практически в любой программе можно, следую идиоме RAII (resource acquisition is initialization, получение ресурса есть инициализация), которая (в несколько упрощенном виде) состоит в следующем:

  • Каждый ресурс следует инкапсулировать в класс, при этом
    • Конструктор выполняет выделение ресурса
    • Деструктор выполняет освобождение ресурса

    Мы уже видели пример RAII-объекта в C++, когда говорили про работу с файлами. Объект fstream владеет ресурсом — файловым дескриптором — и отвечает за его освобождение, а вся работа с файлом происходит через этот объект.

    Умные указатели

    В рамках идиомы RAII в современном C++ решены сложности работы с динамическим выделением памяти. Логика работы с динамической памятью инкапсулирована в специальных классах std::unique_ptr и std::shared_ptr , которые называют умными указателями. При конструировании такого объекта происходит выделение памяти, а при вызове деструктора — освобождение. Например:

    #include int main()  auto luptr = std::make_uniqueLetter>('l'); auto dsptr = std::make_sharedDigit>('7'); return 0; > 

    При выходе из функции main выделенная в куче память корректно будет освобождена. Объекты std::unique_ptr и std::shared_ptr различаются с точки зрения владения объектом. Уникальный указатель std::unique_ptr единолично владеет ресурсом. Это означает, что не может быть два разных объекта std::unique_ptr не могут быть связаны с одним и тем же ресурсом. Это, например, означает, что объект std::unique_ptr не имеет копирующего конструктора и копирующего оператора присваивания. Вместо этого возможно использование перемещающего конструктор и перемещающего оператора присваивания. Например:

    auto luptr = std::make_uniqueLetter>('l'); // std::unique_ptr luptr2 = luptr; // ошибка, уникальное владение auto luptr3 = std::move(luptr); // перемещение возможно. luptr передал // владение и потерял связь с объектом 

    Объекты std::shared_ptr можно копировать. При этом несколько объектов std::shared_ptr оказываются связанными с одним ресурсом (динамически выделенной памятью). Освобождение памяти происходит в момент, когда последний ссылающийся на эту память объект std::shared_ptr вышел из области видимости. Необходимость подсчета ссылок в объектах std::shared_ptr приводит к определенным накладным расходам. Например объекты std::shared_ptr занимают больше памяти, чем объекты std::unique_ptr . Объекты std::unique_ptr при этом не уступают в производительности простым указателям.

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

    std::liststd::unique_ptrCharacter>> create_document()  std::liststd::unique_ptrCharacter>> doc; doc.push_back(std::make_uniqueLetter>('a')); doc.push_back(std::make_uniqueLetter>('b')); doc.push_back(std::make_uniqueDigit>('1')); doc.push_back(std::make_uniqueDigit>('2')); return doc; > 

    и не заботится больше о ручном освобождении ресурсов. Несмотря на некоторую громоздкость синтаксиса умные указатели значительно упрощают разработку на C++. Мы рекомендуем использовать умные указатели вместо низкоуровневых операторов new и delete для работы с динамической памятью.

    Сложность обращения с длинными названиями типов в C++ вроде std::list> (и это не самый плохой случай) может быть преодолена с помощью псевдонимов. Например:

    // определим псевдоним для уникальных указателей на Character using CharPtr = std::unique_ptrCharacter>; using Document = std::listCharPtr>; Document create_document()  Document doc; doc.push_back(std::make_uniqueLetter>('a')); doc.push_back(std::make_uniqueLetter>('b')); doc.push_back(std::make_uniqueDigit>('1')); doc.push_back(std::make_uniqueDigit>('2')); return doc; > 

    Виртуальный деструктор

    В заключение этого раздела обсудим один тонкий момент, связанный с полиморфизмом и освобождением ресурсов в C++. Функция create_document корректно работает с динамической памятью. Однако, если использовать классы Character , Letter и Digit в том виде, в каком мы их оставили в разделе про наследование, то освобождение памяти при удалении объекта Document будет выполнено неверно. Контейнер std::list работает с (умными) указателями на объекты абстрактного класса Character . При удалении объекта std::list происходит удаление всех объектов типа std::unique_ptr , которые в свою очередь вызывают деструкторы объектов Character . Вместо этого мы хотим, чтобы для каждого объекта вызывался деструктор нужного класса-наследника. Вызов только деструктора базового класса снова может привести к утечке памяти.

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

    class Character  // . virtual ~Character() = default; >; 

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

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

    Резюме

    В этом разделе мы обсудили основы работы с динамической памятью в C++. Рекомендуемыми инструментами работы с динамической памятью являются умные указатели std::unique_ptr и std::shared_ptr . Не забывайте объявлять деструктор базового класса виртуальным, если возможна работа с объектами классов-потомков через указатель на объект базового класса (а такая возможность есть всегда).

    Источники

    • en.cppreference.com/w/cpp/memory/new/operator_new
    • en.cppreference.com/w/cpp/memory/new/operator_delete
    • en.cppreference.com/w/cpp/language/raii
    • Идиома RAII (wikipedia)
    • en.cppreference.com/w/cpp/keyword/using
    • en.cppreference.com/w/cpp/language/destructor

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *