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

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

  • автор:

удаление памяти, выделенную для динамического массива

Дана строка символов, необходимо из этой строки сформировать двумерный динамический массив с заранее известным количеством строк и заранее неизвестным количеством символов в каждой строке без чисел. `

int main() < setlocale(LC_ALL, "Russian"); int n; cout > n; cin.clear(); char *a = new char[n+1]; int L = 0; char cBuffer; int iBuffer; int count = 0; int counter = 0; int nomerMax = 0; gets_s(a, n+1); cin.getline(a, n+1); int stroka = 0; for (int i = 0; i < strlen(a); i++) < while (a[i] == 48 || a[i] == 49 || a[i] == 50 || a[i] == 51 || a[i] == 52 || a[i] == 53 || a[i] == 54 || a[i] == 55 || a[i] == 56 || a[i] == 57) i++; L = i; while (!((a[i] == 48) || (a[i] == 49) || (a[i] == 50) || (a[i] == 51) || (a[i] == 52) || (a[i] == 53) || (a[i] == 54) || (a[i] == 55) || (a[i] == 56) || (a[i] == 57))) < if (i == strlen(a)) break; i++; >if (L < i) stroka++; >cout int h = 0; for (int I = L; I < i; I++) < h++; if (maxSize < h) < maxSize = h; nomerMax = count; >> massivPodstrok[count] = new char[h]; count++; > cout int h = 0; for (int I = L; I < i; I++) < iBuffer = a[I]; cBuffer = (char)iBuffer; if (count < stroka) < massivPodstrok[count][h] = cBuffer; cout h++; > cout cout ` 

проблемка в том, что происходит HEAP CORRUPTION при удалении памяти, помогите плз разобраться где накосячил с выделением памяти под массив

Отслеживать
Виталий Китов
задан 13 фев 2018 в 16:06
Виталий Китов Виталий Китов
51 3 3 серебряных знака 8 8 бронзовых знаков

@Виталий Китов » заранее известным количеством строк и заранее неизвестным количеством символов в каждой строке» это std::vector . И никаких проблем с выделением памяти

13 фев 2018 в 16:24
но все же вопрос был не о этом. Мало ли что надо человеку.
13 фев 2018 в 16:25
нет, они запрещены, потому что преподают си, а не с++
13 фев 2018 в 16:27
@KoVadim я так понимаю до 11 года с++ не существовало — был только Си? 🙂
13 фев 2018 в 16:32

Я согласен с KoVadim — если вы изучаете С++ или работаете, то нужно стараться написать именно на С++, особенно что вопрос задается по этому тэгу. Такой код, что написал автор вопроса, отбивает желание разобраться

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

хранятся на стеке. Как следует из названия, стек работает с переменными по схеме 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> (и это не самый плохой случай) может быть преодолена с помощью псевдонимов. Например:

    using CharPtr = std::unique_ptrCharacter>; // определили псевдоним для уникальных указателей на Character 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 . Не забывайте объявлять деструктор базового класса виртуальным, если возможна работа с объектами классов-потомков через указатель на объект базового класса (а такая возможность есть всегда).

    Документация и ссылки

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

    Динамические массивы в C++ — урок 8

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

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

    Например, необходимо создать динамический массив из N элементов, где значение N задается пользователем. В предыдущем уроке мы учились выделять память для переменных, используя указатели. Выделение памяти для динамического массива имеет аналогичный принцип.

    Создание динамического массива

     using namespace std; int main() < int num; // размер массива cout > num; // получение от пользователя размера массива int *p_darr = new int[num]; // Выделение памяти для массива for (int i = 0; i < num; i++) < // Заполнение массива и вывод значений его элементов p_darr[i] = i; cout delete [] p_darr; // очистка памяти return 0; > 

    Синтаксис выделения памяти для массива имеет вид указатель = new тип[размер] . В качестве размера массива может выступать любое целое положительное значение.

    Динамическая память

    При создании массива с фиксированными размерами под него выделяется определенная память. Например, пусть у нас будет массив с пятью элементами:

    double numbers[5] = ;

    Для такого массива выделяется память 5 * 8 (размер типа double) = 40 байт. Таким образом, мы точно знаем, сколько в массиве элементов и сколько он занимает памяти. Однако это не всегда удобно. Иногда бывает необходимо, чтобы количество элементов и соответственно размер выделяемой памяти для массива определялись динамически в зависимости от некоторых условий. Например, пользователь сам может вводить размер массива. И в этом случае для создания массива мы можем использовать динамическое выделение памяти.

    Для управления динамическим выделением памяти используется ряд функций, которые определены в заголовочном файле stdlib.h :

      malloc() . Имеет прототип

    void *malloc(unsigned s);
    void *calloc(unsigned n, unsigned m);
    void *realloc(void *bl, unsigned ns);
    void *free(void *bl);

    malloc

    Функция malloc() выделяет память длиной для определенного количества байт и возвращает указатель на начало выделенной памяти. Через полученный указатель мы можем помещать данные в выделенную память. Рассмотрим простой пример:

    #include #include // для подключения функции malloc int main(void) < int *ptr = malloc(sizeof(int)); // выделяем память для одного int *ptr = 24; // помещаем значение в выделенную память printf("%d \n", *ptr); free(ptr); >

    Здесь с помощью функции malloc выделяется память для одного объекта int . Чтобы узнать, сколько байтов надо выделить, передаем в функцию malloc размер типа int на текущей и в результате получаем указатель ptr , который указывает на выделенную память

    int *ptr = malloc(sizeof(int));

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

    int *ptr = malloc(sizeof *ptr);

    Для универсальности возвращаемого значения в качестве результата функция malloc() (как и calloc() и realloc() ) возвращает указатель типа void * . Но в нашем случае создается массив типа int, для управления которым используется указатель типа int * , поэтому выполняется неявное приведение результата функции malloc к типу int * .

    Далее через этот указатель с помощью операции разыменования помещаем в выделенный участок памяти число 24:

    *ptr = 24;

    В дальнейшем, используя указатель, можно получить значение из выделенного участка памяти:

    printf("%d \n", *ptr);

    После завершения работы освобождаем память, передавая указатель в функцию free() :

    free(ptr);

    Стоит отметить, что теоретически мы можем столкнуться с тем, что функции malloc() не удастся выделить требуемую память, и тогда она возвратит NULL. Чтобы избежать подобной ситуации перед использованием указателя мы можем проверять его на значение NULL:

    #include #include // для подключения функции malloc int main(void) < int *ptr = malloc(sizeof(int)); // выделяем память для одного int if(ptr != NULL) < *ptr = 24; // помещаем значение в выделенную память printf("%d \n", *ptr); >free(ptr); >

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

    free(ptr); ptr = NULL;
    Выделение памяти для массива

    Подобным образом можно выделять память и под набор объектов. Например, выделим память для массива из 4-х чисел int:

    #include #include int main(void) < int n = 4; int *ptr = malloc(n * sizeof(int)); // выделяем память для 4-х чисел int if(ptr) < // помещаем значения в выделенную память ptr[0] = 1; ptr[1] = 2; ptr[2] = 3; ptr[3] = 5; // получаем значения for(int i = 0; i < n; i++) < printf("%d", ptr[i]); >> free(ptr); >
    Выделение памяти для структуры

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

    #include #include struct person < char* name; int age; >; int main(void) < // выделяем память для одной структуры person struct person *ptr = malloc(sizeof(struct person)); if(ptr) < // помещаем значения в выделенную память ptr->name = "Tom"; ptr->age = 38; // получаем значения printf("%s : %d", ptr->name, ptr->age); // Tom : 38 > free(ptr); return 0; >

    calloc

    Функция calloc() имеет прототип

    void *calloc(unsigned n, unsigned m);

    Она выделяет память для n элементов по m байт каждый и возвращает указатель на начало выделенной памяти. В случае неудачного выполнения возвращает NULL

    В отличие от функции malloc() она инициализирует все выделенные байты памяти нулями. Например, выделим память для одного объекта int :

    #include #include int main(void) < // выделяем память для одного объекта int int *ptr = calloc(1, sizeof(int)); if(ptr) < // получаем значение по умолчанию - 0 printf("Initial value: %d", *ptr); // Initial value: 0 // устанавливаем новое значение *ptr = 15; // получаем новое значение printf("New value: %d", *ptr); // New value: 15 >free(ptr); return 0; >
    Initial value: 0 New value: 15

    Подобным образом можно выделить память и для других объектов. Например, выделим память для массива из 4-х объектов int :

    #include #include int main(void) < // выделяем память для 4-х объектов int int n = 4; int *ptr = calloc(n, sizeof(int)); if(ptr) < // устанавливаем значения ptr[0] = 1; ptr[1] = 2; ptr[2] = 3; ptr[3] = 5; // получаем значения for(int i = 0; i < n; i++) < printf("%d", ptr[i]); >> free(ptr); >

    realloc

    Функция realloc() позволяет изменить размер памяти, ранее выделенной с помощью функций malloc() b calloc() . Имеет прототип

    void *realloc(void *bl, unsigned ns);

    Первый параметр представляет указатель на ранее выделенный блок памяти. А второй параметр представляет новый размер блока памяти в байтах.

    Если указатель bl имеет значение NULL , то есть память не выделялась, то действие функции аналогично действию malloc

    Рассмотрим небольшой пример:

    #include #include int main(void) < // выделяем память для 1-го объекта int int size = sizeof(int); int *ptr = malloc(size); if(ptr) < // отображаем адрес и размер памяти printf("Addresss: %p \t Size: %d\n", (void*)ptr, size); >// расширяем память до размера 4-х объектов int size = 4 * sizeof(int); int *ptr_new = realloc(ptr, size); // если выделение памяти прошло успещно if(ptr_new) < printf("Reallocation\n"); // заново отображаем адрес и размер памяти printf("Addresss: %p \t Size: %d\n", (void*)ptr_new, size); free(ptr_new); // освобождаем новый указатель >else < free(ptr); // освобождаем старый указатель >>

    Здесь сначала выделяем память для одного объекта int с помощью функции malloc.

    int size = sizeof(int); int *ptr = malloc(size);

    Если память успешно выделена, то выводим на консоль адрес и размер выделенного блока памяти. Затем с помощью функции realloc расширяем память до 4 объектов int

    size = 4 * sizeof(int); int *ptr_new = realloc(ptr, size);

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

    Консольный вывод в моем случае

    Addresss: 0000018B078A82F0 Allocated: 4 Reallocation Addresss: 0000018B078A82F0 Allocated: 16

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

    #include #include int main(void) < int size = sizeof(int); int *ptr = malloc(size); if(ptr) < printf("Addresss: %p \t Allocated: %d\n", (void*)ptr, size); >size = 4 * sizeof(int); ptr = realloc(ptr, size); // используем старый указатель if(ptr) < printf("Reallocation\n"); printf("Addresss: %p \t Allocated: %d\n", (void*)ptr, size); >free(ptr); >

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

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