Лекция №14. Динамическое распределение памяти. Использование указателей при решении задач 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Лекция №14. Динамическое распределение памяти. Использование указателей при решении задач



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

 

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

Для объектов, размер которых не представляется возможным определить заранее (например, изображений, файлов, структур), используется динамическое распределение памяти. Динамическая память представляет собой совокупность различных видов памяти, которая выделяется и освобождается по указанию программиста во время выполнения программы, в любом месте, в соответствии с алгоритмом решения задачи. Для доступа к динамической памяти используют указатели. При динамическом распределении памяти объекты размещаются в «куче» (heap) – специально организованной области памяти переменного размера. Причем, при конструировании объекта указывается размер запрашиваемой под объект памяти, и, в случае успеха, выделенная область памяти, условно говоря, «изымается» из «кучи», становясь недоступной при последующих операциях выделения памяти. Динамические переменные также называются адресуемыми указателями. По мере создания в программе новых объектов, количество доступной памяти уменьшается. Отсюда вытекает необходимость постоянно освобождать ранее выделенную память. В идеальной ситуации программа должна полностью освободить всю память, которая потребовалась для работы.

Некорректное распределение памяти приводит к ее «утечкам», т.е. ситуациям, когда выделенная память не освобождается. Многократные утечки памяти могут привести к исчерпанию всей оперативной памяти и нарушить работу операционной системы. Поэтому операция, противоположная по смыслу операции выделения памяти под объект, - это освобождение занятой ранее под какой-либо объект памяти: освобождаемая память, условно говоря, возвращается в «кучу» и становится доступной при дальнейших операциях выделения памяти.

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

Для выделения и освобождения динамической памяти в языках С/С++ используются специальные функции.

Функция malloc() – memory allocation - определена в заголовочном файле alloc.h и используется для инициализации указателей необходимым объемом памяти. Память выделяется из сектора оперативной памяти доступного для любых программ, выполняемых на данном компьютере. Аргументом функции malloc() является количество байт памяти, которую необходимо выделить, возвращает функция - указатель на выделенный блок в памяти. Поскольку различные типы данных имеют разные требования к памяти, объем адресуемой памяти вычисляется с помощью операции sizeof(). Например, при выделении указателю ptrVar необходимого объема памяти для хранения значения типа int можно использовать оператор

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

Размер выделяемой памяти можно определять, передавая пустой указатель. Например, int *ptrVar = malloc (sizeof (*ptrVar));

Операция sizeof(*ptrVar) оценит размер участка памяти, на который ссылается указатель, а так как ptrVar является указателем на участок памяти типа int, то sizeof() вернет размер целого числа.

Оператор резервирования может отказать в случае, если куча уже заполнена или оставшаяся память меньше, чем требуемое количество байтов. В этой ситуации функция malloc() возвращает нулевое значение.

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

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

После освобождения памяти хорошей практикой является сброс указателя в нуль, то есть присвоить *ptrVar = 0;. Если указателю присвоить 0, то он становится нулевым, другими словами, он уже никуда не указывает. В противном случае, даже после высвобождения памяти, указатель все равно будет указывать на неё, а, значит, можно случайно нанести вред другим программам, которые, возможно будут использовать эту память.

Для резервирования памяти вместо функции malloc() можно использовать функцию сalloc(), прототип которой также объявлен в заголовочном файле alloc.h. Эта функция тоже резервирует память в куче, но требует два аргумента: количество объектов, которое необходимо разместить, и размер одного объекта. Например, чтобы зарезервировать память для 10 значений типа long и присвоить указателю sPtr адрес первого значения, можно использовать оператор long *sPtr = сalloc(10, sizeof(long));

Параметры функции сalloc() могут задаваться в любом порядке, т.е. сalloc(num,size) и сalloc(size,num) резервируют память объемом num*size байтов. Кроме выделения памяти, эта функция устанавливает каждый зарезервированный ею байт равным нулю, т.е. инициализирует память.

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

- new - автоматически создает объект соответствующего размера в памяти и возвращает его адрес; если выделение памяти не произошло, возвращается нулевой указатель. Например, int *iPtr = new int;

Кроме того, при резервировании памяти посредством оператора new допускается присвоение значения созданному объекту сразу:

float *fPtr = new float (17.245);

- delete - освобождение зарезервированной ранее памяти, при этом размер освобождаемой памяти задавать не требуется: delete iPtr;

При работе с операторами new и delete подключать заголовочный файл alloc.h не нужно.

Следует помнить, попытка освободить одну и ту же память более одного раза, приводит к системной ошибке. Частично или полностью эти проблемы в С++ решаются созданием классов из библиотек STL и Boost, реализующих «умные указатели».

Указатели могут адресовать переменные всех видов – от простых целочисленных переменных типа int до сложных типов таких, как массивы и структуры. Кроме того, они не заменимы при работе с функциями.

В языках С/С++ при работе с массивами очень удобно использовать указатели, поскольку с их помощью можно организовать экономный и быстрый доступ к любому элементу массива. На рисунке Д.2 показана связь между элементами массива и указателями. Здесь указатель mPtr позволяет обратиться к элементу m[0], mPtr+1 указывает на следующий элемент, а mPtr+i на i-элемент массива М.

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

1) объявить указатель требуемого типа данных;

2) затем в программе вызвать функцию malloc() для выделения памяти в куче для массива или сalloc() для инициализации значений массива нулями;

3) работать с массивом как обычно, а после его использования освободить память с помощью функции free().

Например, для выделения памяти под массив из 100 элементов типа double следует выполнить действия:

double *BigArrayPtr;

BigArrayPtr = malloc(100*sizeof(double));

или BigArrayPtr = сalloc(100, sizeof(double)); // инициализация нулевыми значениями

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

BigArrayPtr [i] = random (50);

free (BigArrayPtr);

Для резервирования памяти под массив и ее последующего освобождения можно применять операторы new и delete:

double *BigArrayPtr = new double(100);

delete [ ] BigArrayPtr;

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

При работе с двумерными массивами можно представлять их в памяти как одномерные, а для выделения памяти воспользоваться следующей формулой: i_текущее = Nx * iY + iX,

где i_текущее - текущее значение индекса в одномерном массиве;

Nx – количество столбцов в исходной матрице;

iY, iX индексы текущего элемента в исходной матрице.

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

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

struct имя_структуры *имя_указателя;

Например,

struct date

{ int day; char month[9]; int year; }d;

struct date *dat_Ptr;

Затем с помощью функции malloc() выделить память в куче и присвоить адрес указателю:

указатель = (struct тип_структуры*) malloc (sizeof (struct тип_структуры));

Например, dat_Ptr = (struct data*) malloc (sizeof (struct data));

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

тип_структуры. имя_элемента = значение;

следует применить обращение

тип_структуры ­-> имя_элемента = значение;

Оператор доступа к полю структуры ­-> указывает на его расположение в памяти относительно начала структуры.

При обычном вызове функции компилятор С/C++ создает копию значений фактических парамет­ров и помещает эту копию в стек - участок памяти компьютера для временного хранения информации. Значения параметров из стека присваиваются формальным параметрам функции. После завершения работы функции происходит очистка стека. Таким образом, значения фактических параметров в основной памяти компьютера после завершения работы функции остаются без из­менений.

В случае, когда нужно изменить значения фактических параметров, т.е. осуществить присваивание значений формаль­ных параметров при выходе из функции фактическим парамет­рам основной программы, следует ис-пользовать оператор адреса (признак &) и переменные-указатели (признак *).

Оператор адреса предназначен для передачи адресов значе­ний фактических параметров из основной программы в функцию. Оператор адреса имеет вид: имя_функции (&фактич_параметр1,..., &фактич_параметр N);

Чтобы «объяснить» компилятору C++, что значения факти­ческих параметров будут передаваться из основной программы в функцию с помощью адреса (в стек будут записываться адреса фактических параметров, а не их значения), в заголовке функции объявляются переменные-указатели (формальные параметры) с признаком *:

тип_функции имя­_функции (тип *форм_пар1,..., тип *форм_парN)

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

#include <iostream.h>

void change (int *x, int *y)

{ *x=100; *y=200; }

void main (void)

{ int a,b; a=10; b=20;

cout<<"Параметры а и b до обращения к функции:"<<а<<” "<<b<<endl;

change(&а,&b);

cout<<"Параметры b после обращения к функции:"<<а<<" "<<b<<endl;}

В результате после завершения работы функции фактичес­кие параметры изменили свои значения, а на экран бу­дет выведено:

Параметры а и b до обращения к функции: 10 20

Параметры а и b после обращения к функции: 100 200

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

тип& имя_ссылки=имя_переменной;

После объявления ссылки в программе можно использовать имя переменной или имя ссылки. В качестве иллюстрации изменения значений фактических па­раметров при помощи ссылок рассмотрим пример:

#include <iostream.h>

void change (int& x, int& y)

{ x=100; y=200; }

void main (void)

{ int a,b;

int& as=a; //объявление ссылки as — псевдонима переменной а

int& bs=b; //объявление ссылки bs — псевдонима переменной b

a=10; b=20;

cout<<"параметры а и b до обращения к функции:"<<а<<" "<<b<<endl;

change(as,bs);

cout<<"параметры а и b после обращения к функции:"<<а<<" "<<b<<endl;}

В результате работы программы на экран бу­дет выведено:

Параметры а и b до обращения к функции: 10 20

Параметры а и b после обращения к функции: 100 200

Таким образом, результат работы программы полностью со­впадает с результатом работы программы, рассмотренной в пре­дыдущем примере. Однако не следует ув­лекаться частым использованием ссылок, так как это затрудняет понимание программы [1, 5, 7].

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

/*тип данных*/ (* /*имя указателя*/) (/*список аргументов функции*/);

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

И, наконец, указатель на файл (file pointer) – это указатель на информацию, которая определяет его характеристики (имя, статус, текущую позицию), а именно: на структуру типа FILE, которая определена в заголовочном файле stdio.h. Для объявления указателя на файл используется оператор FILE *fPtr;



Поделиться:


Последнее изменение этой страницы: 2016-09-20; просмотров: 553; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 18.117.81.240 (0.041 с.)