Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лекции по информатике.doc
Скачиваний:
118
Добавлен:
02.05.2014
Размер:
1.53 Mб
Скачать

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

Ранее мы рассматривали область действия и время жизни переменных.

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

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

Замечание. Распространенной ошибкой является использование адреса   автоматической переменной после выхода из функции. Конструкция типа:

int*

func()

{

intx;

. . .

return&х;

}

дает непредсказуемый результат.

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

Если переменная определена вне функции, память для нее отводится статически, один раз в начале выполнения программы, и переменная уничтожается только тогда, когда выполнение программы завершается – глобальная переменная. Можно статически выделить память и под переменную, определенную внутри функции или блока. Для этого нужно использовать ключевое слово static в его определении:

double globalMax;

// переменная определена вне функции

void

func(int x)

{

static bool visited = false;

if (!visited) {

. . . // инициализация

visited = true;

}

. . .

}

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

Если бы переменная visited не была объявлена static, то инициализация происходила бы при каждом вызове функции.

Третий способ выделения памяти в языке Си++ – динамический.

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

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

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

int*Piece;

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

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

{

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

return;

}

. . .

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

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

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

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

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

memset(Piece, 0, 15);

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

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

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

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

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

Выражение, содержащее операцию new, имеет следующий вид:

указатель_на_тип = newимя_типа (инициализатор)

Инициализатор – это необязательное выражение, которое задает начальное значение переменной (может использоваться для всех типов, кроме массивов).

При выполнении оператора

int*N=newint

создаются 2 объекта:

  • динамический безымянный объект;

  • указатель на этот объект с именем N, значением которого является адрес динамического объекта.

Можно создать и другой указатель на тот же динамический объект:

int *other_point=N,

Если указателю N присвоить другое значение, то можно потерять доступ к динамическому объекту:

int *N=new(int);

int i=0;

N=&i;

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

При выделении памяти объект можно инициализировать:

int*N=newint(3);

Динамически распределять память можно не только под обычные переменные, но и под массивы:

int*array=newint[50];

Теперь с этой динамически выделенной памятью можно работать как с обычным массивом:

*(аггау+1)=10;

array[б]=array[5]+11 ;

В случае успешного завершения операция new возвращает указа­тель со значением, отличным от нуля.

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

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

Для начала опишем его следующим образом:

main ()

{

int **array, i, j, N, M;

Далее создаем матрицу:

array=newint*[N]; // Выделяет память для одномерного массива

for (int i=0; i<N; i++)

array[i]=newint[M]; // При помощи цикла повторений

// выделяет память для двумерного массива

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

for (int i=0; i<N; i++)

for (int j=0; j<M; j++)

array[i] [j]=0;

Теперь все готово для ввода элементов созданного двумерного массива.

Освобождение динамически выделенной памяти

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

deleteN; // Удаляет динамический объект типаint,

// если было N=newint

deletearray; // Удаляет динамический массив длиной 50,

// если было int *mas = new int[50]

Совершенно безопасно применять операцию к указателю null. Результат же повторного применения операции delete к одному и тому же указателю не определен. Обычно происходит ошибка, приводящая к зацикливанию.

Чтобы избежать подобных ошибок, можно применять следую­щую конструкцию:

int *N=new int[10];

if (N)

{

delete N;

N=NULL;

} else cout<<"память уже освобождена "<<endl;

Выделение памяти под строки

В следующем фрагменте программы мы динамически выделяем память под строку переменной длины и копируем туда исходную строку

// стандартная функция strlen подсчитывает

// количество символов в строке

int length = strlen(src_str);

// выделить память и добавить один байт

// для завершающего нулевого байта

char* buffer = new char[length + 1];

strcpy(buffer, src_str);

// копирование строки

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

char* newstr;

newstr = new char[length];

if (newstr == NULL) { // проверить результат

// обработка ошибок

}

// память выделена успешно