Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лк8.Операторы управления динамической памятью.docx
Скачиваний:
1
Добавлен:
22.11.2019
Размер:
44.32 Кб
Скачать

Многомерные массивы

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

int m[5][8];

Такой код генерирует массив из 5 элементов, каждый из которых является массивом из 8 элементов типа int. Можно обратиться к любому из 5 подмассивов непосредственно (m[3]), либо к конкретному элементу конкретного подмассива (m[3][7]).

m[3] имеет тип "массив из восьми int", т.е. int[8], и, как любой массив, автоматически приводится к int* в любом value context.

sizeof(m[3]) будет 8*sizeof(int), а использование m[3] слева от присваивания и как операнд ++/-- запрещено.

Операторы управления динамической памятью

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

Как это делалось в Си

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

int *Piece;

Piece = (int*)malloc(sizeof(int)); /* аргумент функции malloc() - число байт, которые надо выделить */

if (Piece == NULL) /* malloc() возвращает NULL, если не может выделить память */

{

printf("Ошибка выделения памяти: видимо, недостаточно места в ОЗУ\n")

return;

}

. . .

free(Piece); /* аргумент free() - указатель на уже ненужный кусок памяти */

Если возникала необходимость выделить память под несколько переменных одного типа, расположенных рядом (то есть под массив), аргумент malloc()'а просто домножали на нужное количество ячеек массива:

int *Piece = (int*)malloc(15 * sizeof(int));

Был, правда, у malloc() один "недостаток": выделяя память, он не изменял содержимое ячеек, поэтому там могло оказаться совершенно произвольное значение. С этим боролись либо с помощью специальной функции memset(ptr, c, n) (она заполняет n байт памяти начиная с места, на которое указывает ptr, значением c), либо с помощью calloc(). Функция calloc() принимает два параметра: число ячеек массива, под которые надо выделить память, и размер этой ячейки в байтах; делает эта функция следующее: выделяет нужное количество памяти (непрерывный кусок) и обнуляет все значения в нём. Таким образом, код:

int *Piece = (int*)malloc(15 * sizeof(int));

memset(Piece, 0, 15 * sizeof(int));

эквивалентен коду:

int *Piece = (int*)calloc(15, sizeof(int));

Операторы new и delete

Идеология языка C++ предполагает, что каждый объект создаётся (объявляется) именно в том месте, где он нужен, и является работоспособным сразу после создания. Для этого каждый класс имеет определённый набор конструкторов — функций, которые должны автоматически запускаться при создании объекта (экземпляра данного класса) и инициализировать его члены (data members). Конструкторы одного класса отличаются только количеством и типом передаваемых параметров, то есть являются перегруженными функциями. Однако, к сожалению, функции malloc() и сalloc() не умеют автоматически запускать конструкторы, и потому непригодны для динамического создания объектов. В языке Си++ им имеется адекватная замена — оператор new. Рассмотрим пример:

MyClass *mc = new MyClass(5);

В данном случае создаётся экземпляр класса MyClass, после чего с помощью его конструктора, принимающего в качестве параметра целое число (в данном случае, число 5), объект "инициализируется" этим числом. Адрес вновь созданного объекта присваивается указателю mc. Если для класса определён конструктор по умолчанию, после имени класса допускается не указывать пустые скобки. Писать их или нет — это, как говорится, дело вкуса:

new MyClass(); // эти две строки кода

new MyClass; // абсолютно эквивалентны

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

Важное отличие оператора new от функции malloc() заключается в том, что он возвращает значение типа "указатель-на-объект" (то есть MyClass *), в то время как функция malloc() — "указатель-на-что-угодно" (void *). Подобная типизация в Си++ — не редкость, она строже, чем та, что используется в Си, и, следовательно, менее ошибкоопасна. Извратившись, и в Си++ можно скомпилировать код, где указатель на один класс приводится к указателю на другой класс, никак не связанный с первым — но в Си++ это можно сделать только специально. Точнее, в Си разрешено неявное автоприведение void* в любой указатель, в Си++ — запрещено.

Для каждого класса, помимо конструкторов, определён ещё и деструктор, то есть функция, отвечающая за корректное уничтожение объекта. Деструктор никогда никаких параметров не принимает, и потому не может быть перегружен. Проблема с деструктором возникает та же, что и с конструктором: функция free() не умеет его вызывать. Поэтому в Си++ введён ещё один оператор — delete. Синтаксис его очень прост:

delete mc;

где mc — указатель на класс. Именно для этого класса и вызовется деструктор, поэтому, если Вы объявили его как "указатель-на-что-угодно", деструктор не будет вызван вообще. Собственно, именно поэтому void * не рекомендуется использовать. Другой пример:

class Base

{

. . .

};

class Derived : public Base

{

. . .

};

int main( void )

{

Base *ptr = new Derived; // присваивать указателю на предка адрес потомка -

//можно, но почти всегда требует виртуального деструктора

. . .

delete ptr;

return 0;

}

В этом случае оператором delete вызовется деструктор базового класса Base, хотя требуется вызвать деструктор класса-потомка Derived. Казалось бы, применение RTTI (Run-Time Type Info) в среде Microsoft Visual Studio позволило бы спастись от этой напасти, но увы и ах... (автор языка Страуструп считал RTTI злом, ибо это средство провоцирует разработчиков ломать полиморфизм и уходить из ООП-парадигмы в сторону спагетти). В принципе, гибким решением этой пробемы является применение виртуальных деструкторов. Страуструп рекомендует делать деструктор виртуальным всегда, если в классе есть хотя бы одна иная виртуальная функция. Также имеет смысл делать деструктор виртуальным всегда, когда класс будет создаваться по new. Вообще же, как мы видим, при использовании delete надо проявлять особую осторожность.

Динамические массивы в Cи++

Вы, возможно, уже заметили, что при создании объектов с помощью оператора new мы выделяем память ровно под один экземпляр класса, в то время как используя malloc() или calloc() имеем возможность создать целый массив из произвольного числа элементов. На самом деле, синтаксис языка Cи++ позволяет использовать для этих целей конструкцию, аналогичную оператору new:

MyClass *mc = new MyClass[15];

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

Итак, в нашем примере будет выделена память под массив из 15 объектов класса MyClass, и каждый из них будет инициализирован с помощью конструктора по умолчанию. Если такой конструктор не определён, то попытка использовать new[] приведёт к ошибке. Применить к элементам созданного динамического массива какой-либо другой конструктор, увы, нельзя; поэтому такая запись вызовет легкое недоумение компилятора:

MyClass *mc1 = new MyClass("hello, world!")[134];

MyClass *mc2 = new MyClass()[2]; // это тоже ошибка, нельзя комбинировать два типа скобок в одном new

В пару к оператору new[] введён оператор delete[]. Система сама помнит, сколько памяти было выделено под этот динамический массив, поэтому указывать число элементов не требуется. Просто напишите:

delete[] mc;

и компьютер сделает всё за Вас. Предостережение при использовании delete[] такое же, как и для delete: если хотите избежать утечек памяти (memory leak), следите за тем, чтобы вызывались "правильные" деструкторы.

Категорически запрещено путать delete и delete[] — то, что создано как массив, обязательно требует delete[], и обратно. Компилятор, как правило, не может отследить такое, потому ошибиться (и вызвать крах программы) довольно легко для начинающих.