Страницы

Поиск по вопросам

суббота, 11 января 2020 г.

Как использовать динамическую память в c++?

#cpp


Как работает динамическая память и какими операторами пользоваться для работы с ней
в C++?
    


Ответы

Ответ 1



В С++ объекты создаются динамически при помощи new-выражений и разрушаются при помощи delete-выражений. T* obj = new T; delete obj; T* arr = new T[100]; delete[] arr; New-выражения и delete-выражения используют функции выделения динамической памяти operator new, operator new[] и функции освобождения operator delete, operator delete[] соответственно. Реализация С++ предоставляет несколько глобальных функций выделения и освобождения. При этом они могут быть переопределены пользователем без нарушения ODR (но не более одного раза в программе). void* operator new(std::size_t); void operator delete(void*) noexcept; void operator delete(void*, std::size_t) noexcept; void* operator new[](std::size_t); ... Пользователь также может добавлять свои функции выделения и освобождения, в виде статических членов класса или свободных функций в глобальном пространстве имен. Такие пользовательские функции должны соблюдать семантику встроенных (не выделять уже выделенные адреса и т.п.). Выражения new и delete для одного объекта Функции operator new и operator delete только выделяют и освобождают память. Конструирование и разрушение объекта происходит в самом выражении new и delete. Для вызова конструктора используется new который не выделяет память - размещающий new. // Эквивалент кода // T* obj = new T(a1, a2); T* obj; { void* mem = operator new(sizeof(T)); try { obj = new(mem) T(a1, a2); // Размещающий new, вызывает конструктор T на памяти mem } catch (...) { // Исключение в конструкторе operator delete(mem); throw; } } // Примерный* эквивалент кода // delete obj; obj->~T(); // Вызов деструктора operator delete(obj); // *) При виртуальном деструкторе компилятор сгенерирует что-то похожее на void* mem = obj->~T(); // псевдокод operator delete(mem); // Т.к. при множественном наследовании может быть mem != obj Из кода выше видно, что new-выражение требует обе функции, operator new и operator delete. Функции operator new и operator delete в стандартной библиотеке Обычный new void* operator new(std::size_t size); Использование: T* p = new T; assert(p != nullptr); Примерная реализация: void* operator new(std::size_t size) { for (;;) { void* mem = unspecified_alloc(size); // Может быть malloc, зависит от реализации. if (mem) return mem; // Пользовательская функция-обработчик нехватки памяти. std::new_handler user_handler_fn = std::get_new_handler(); if (!user_handler_fn) throw std::bad_alloc(); user_handler_fn(); // Либо предоставляет память, // либо бросает bad_alloc или завершает программу. } } Пользователь может установить свою функцию new_handler при помощи функции std::set_new_handler. Не-бросающий исключений new void* operator new(std::size_t size, const std::nothrow_t&) noexcept; Использование: T* p = new(std::nothrow) T; if (!p) { /* не удалось выделить память */ } Примерная реализация: void* operator new(std::size_t size, const std::nothrow_t&) noexcept { try { return operator new(size); } catch (const std::bad_alloc&) { return nullptr; } } Размещающий new void* operator new(std::size_t size, void* ptr) noexcept { return ptr; } Ничего не выделяет, возвращают аргумент ptr. Позволяют вызвать конструктор объекта. char mem[sizeof(T)]; T* p = new(mem) T; Размещающий operator delete также существует, и также ничего не делает. Нужен только потому что new-выражение требудет наличия operator delete. Динамические массивы, new[] и delete[] Функции operator new[] и operator delete[] служат для выделения памяти, и ничем не отличаются от функций для одного объекта. Стандартные реализации просто вызывают operator new: void* operator new[](std::size_t size) { return operator new(size); } Размер динамического массива сохраняется самим выражением new[], примерно так: // Эквивалент кода (без учета выравнивания) // T* arr = new T[100]; T* arr; { void* mem = operator new[](sizeof(std::size_t) + sizeof(T)); std::size_t& size = *reinterpret_cast(mem); arr = reinterpret_cast(reinterpret_cast(mem) + sizeof(std::size_t)); try { for (size = 0; size != 100; ++size) { new(tmp_arr + size) T; // Конструктор элемента } } catch (...) { while (size != 0) { --size; arr[size].~T(); // Деструктор элемента } operator delete[](mem); throw; } } // Эквивалент кода (без учета выравнивания) // delete[] arr; { std::size_t& size = reinterpret_cast(arr)[-1]; while (size != 0) { --size; arr[size].~T(); // Деструктор элемента } operator delete[](size); } Массивы созданные при помощи выражения new T[N] могут быть освобождены только через delete[], и наоборот, объекты созданные new T(), должны быть быть освобождены delete. Размещающий new[] void* operator new[](std::size_t size, void* ptr) noexcept; Он существует, но мы не знаем сколько байт будут резервироваться копилятором под размер объекта, поэтому не сдедует его использовать: // char mem[100 * sizeof(T) + sizeof(std::size_t)]; // Компилятор может использовать больше чем sizeof(std::size_t), тогда мы выйдем за границы mem. // new(mem) T[100];

Ответ 2



Динамическая память Выделяется и освобождается — с помощью специальных инструкций (т. е. по инициативе разработчика). Это позволяет по ходу работы программы контролировать и корректировать объём используемой памяти и, следовательно, создавать программы способные обрабатывать большие объёмы данных, обходя ограниченность физической памяти машины. Выделяется во время работы программы. Все объекты, выделяемые динамически, размещаются в куче (heap). Если не освобождать динамическую память, то у вас memory leak. Динамическая память, как и любая память, будет освобождена по завершении программы (учтите это, ибо выше писалось только про освобождение разработчиком). Работа с динамической памятью Выделяется память с помощью оператора new, а освобождается — с помощью оператора delete. В момент, когда динамическая память выделена, она должна быть связана с некоторым указателем, подходящего типа (при выделении указывается тип и количество необходимых ячеек данного типа). int* p; p = new int; *p = 10; cout << *p; // 10 delete p; // память освобождена Если не освобождать динамическую память, то она будет занята до завершения программы, что неприемлемо. При выделении одной динамической переменной (одной ячейки памяти), можно сразу инициализировать её значение: int* p; p = new int(10); cout << *p; // 10 delete p; // память освобождена Можно выделять сразу несколько ячеек динамической памяти, получая динамический массив. Для этого его размер указывается в квадратных скобках после типа. Чтобы удалить динамический массив и освободить память используется оператор delete[]. int* p; p = new int[13]; for (int i=0; i<13; i++) { *(p+i) = i + 1; cout << *(p+i) << ' '; // 1 2 3 ... 13 } delete[] p; // память освобождена, из неё удалены все элементы Cразу после создания динамический массив автоматически заполняется нулями (в отличии от обычного массива в статической или стековой памяти). Если в указатель, уже хранящий адрес какого-то фрагмента динамической памяти, записать новый адрес, то фрагмент динамической памяти будет потерян, т. е. он не будет освобождён, но к нему никак нельзя будет обратиться (например, чтобы освободить этот фрагмент). int* p; p = new int(13); int a = 666; p = &a; // теперь до 13 никак не добраться Проблема становится особенно острой, когда в памяти теряются целые массивы (они занимают больше места, чем отдельные переменные). int* p; for (int i=1; i<=10; i++) { p = new int[100]; } delete[] p; На каждом шаге цикла создаётся динамический массив из 100 элементов. Всего таких массивов будет создано 10, но только от последнего из них память будет освобождена после выхода из цикла. 9 массивов продолжат занимать место в памяти до конца программы. 9 массивов * 100 элементов * 4 байта = 3600 байт потерянной памяти, которую никак нельзя использовать (ни в этой программе, не в других запущенных). Очень важно после использования динамической памяти не забывать освобождать её в нужный момент! Сайт-источник : http://itedu.ru/courses/cpp/cpp-dynamic-memory-allocation Примеры: С++: Представлена выше, выделена зеленым шрифтом. Си: Стандартные функции динамического выделения памяти Функции динамического выделения памяти находят в оперативной памяти непрерывный участок требуемой длины и возвращают начальный адрес этого участка. Функции динамического распределения памяти: void* malloc(РазмерБлокаПамятиВБайтах); void* calloc(ЧислоЭлементов, РазмерЭлементаВБайтах); void* realloc(УказательНаБлокПамяти, НовыйРазмерБлокаВБайтах); Для использования функций динамического распределения памяти необходимо подключение библиотеки malloc.h: #include malloc.h Поскольку все представленные функции в качестве возвращаемого значения имеют указатель на пустой тип void, требуется явное приведение типа возвращаемого значения. Для определения размера массива в байтах, используемого в качестве аргумента функции malloc()требуется количество элементов умножить на размер одного элемента. Поскольку элементами массива могут быть как данные простых типов, так и составных типов (например, структуры), для точного определения размера элемента в общем случае рекомендуется использование функции int sizeof(тип); которая определяет количество байт, занимаемое элементом указанного типа. Память, динамически выделенная с использованием функций calloc(), malloc(), может быть освобождена с использованием функции free(указатель); "Правилом хорошего тона" в программировании является освобождение динамически выделенной памяти в случае отсутствия ее дальнейшего использования. Однако если динамически выделенная память не освобождается явным образом, она будет освобождена по завершении выполнения программы. Функция realloc выполняет перераспределение блоков памяти. Размер блока памяти, на который ссылается параметр ptrmem изменяется на size байтов. Блок памяти может уменьшаться или увеличиваться в размере. Эта функция может перемещать блок памяти на новое место, в этом случае функция возвращает указатель на новое место в памяти. Содержание блока памяти сохраняется даже если новый блок имеет меньший размер, чем старый. Отбрасываются только те данные, которые не вместились в новый блок. Если новое значение size больше старого, то содержимое вновь выделенной памяти будет неопределенным. В случае, если ptrmem равен NULL, функция ведет себя именно так, как функция malloc, т. е. выделяет память и возвращает указатель на этот участок памяти. В случае, если size равен 0, ранее выделенная память будет освобождена, как если бы была вызвана функция free, и возвращается нулевой указатель. Сайт-источник: https://prog-cpp.ru/c-alloc/ Примеры: Выделение памяти при помощи malloc: int *p = (int *)malloc(sizeof(int)); *p = 34; printf("%i", *p); // 34 free(p); Выделение при помощи calloc: int *p = (int *)calloc(5, sizeof(int)); int i; for(i = 0; i < 5; i++) *(p+i) = i + 1; for(i = 0; i < 5; i++) printf("%i ", *(p+i)); // 1 2 3 4 5 free(p); Выделение памяти при помощи realloc: int *p = (int *)realloc(NULL, 5 * sizeof(int)); int i; for(i = 0; i < 5; i++) *(p+i) = i + 1; for(i = 0; i < 5; i++) printf("%i ", *(p+i)); // 1 2 3 4 5 int *b = (int *)realloc(p, 6 * sizeof(int)); *(b+5) = 6; for(i = 0; i < 6; i++) printf("%i ", *(b+i)); // 1 2 3 4 5 6 free(b); // нам не надо освобождать p, так как старый блок памяти // освобождается автоматически. При этом, p может совпадать // с b или же указывать на мусор. Небольшие заметки по поводу памяти в плюсах (на основе опыта, исправляйте ошибки и дополняйте меня):  если у нас недостаточно памяти, valloc, calloc и realloc возвращают NULL. Поэтому проверки на null поинетры нужны. В плюсах new либо вернёт значение, либо кинет исключение;  если вы пытаетесь использовать память, которая вам не принадлежит, то это undefined behavior. Может быть, вас убъёт ось за то, что лезете не в свой кусок памяти. Может, упадёте с segmentation fault, или попортите данные своего же приложения и будете долго и муторно искать ошибку не там, а может быть, у вас девушка забеременеет. Или всё будет работать.  То, что malloc выглядит функцией для инициализации одной переменной, а calloc – для массива – это херня. Все три (включая realloc) выделяют последовательный кусок памяти, так что хоть malloc(n * sizeof(int)), хоть calloc(n, sizeof(int)) – одна херня.  в память только для чтения нельзя писать, например: char *str = "Foo"; *str = 'b';  NULL поинтеру нельзя присвавать какое либо значение, это segmentation fault. Например: int * pointer = NULL; *pointer = 13;

Комментариев нет:

Отправить комментарий