Главная | Обратная связь | Поможем написать вашу работу!
МегаЛекции

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




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

1. Выделение памяти в соответствие с типом указателя

Оператор new состоит их ключевого слова new, за которым следует спецификатор типа. Этот спецификатор может относиться к встроенным типам или к типам классов. Например:

new int; размещает в памяти один объект типа int.

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

int *pi = new int;

Здесь оператор new создает один объект типа int, на который ссылается указатель pi. Выделение памяти из хипа во время выполнения программы называется динамическим выделением. Мы говорим, что память, адресуемая указателем pi, выделена динамически.

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

if (*pi == 0)

вероятно, даст false, поскольку объект, на который указывает pi, содержит случайную последовательность битов. Следовательно, объекты, создаваемые с помощью оператора new, рекомендуется инициализировать. Программист может инициализировать объект типа int из предыдущего примера следующим образом:

int *pi = new int(0);

Константа в скобках задает начальное значение для создаваемого объекта; теперь pi ссылается на объект типа int, имеющий значение 0. Выражение в скобках называется инициализатором. Это может быть любое выражение (не обязательно константа), возвращающее значение, приводимое к типу int.

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

int ival = 0; // создаем объект типа int и инициализируем его 0

int *pi = &ival; // указатель ссылается на этот объект не считая, конечно, того, что объект, адресуемый pi, создается библиотечной функцией new() и размещается в хипе.

Описанные операторы new могут вызывать одну проблему: хип, к сожалению, является конечным ресурсом, и в некоторой точке выполнения программы мы можем исчерпать его. Если функция new() не может выделить затребованного количества памяти, она возбуждает исключение bad_alloc.

Время жизни объекта, на который указывает pi, заканчивается при освобождении памяти, где этот объект размещен. Это происходит, когда pi передается оператору delete. Например,

delete pi;

Освобождает память, на которую ссылается pi, завершая время жизни объекта типа int. Программист управляет окончанием жизни объекта, используя оператор delete в нужном месте программы. Этот оператор вызывает библиотечную функцию delete(), которая возвращает выделенную память в хип. Поскольку хип конечен, очень важно возвращать ее своевременно.

Оператор delete может использоваться только по отношению к указателю, который содержит адрес области памяти, выделенной в результате выполнения оператора new. Попытка применить delete к указателю, не ссылающемуся на такую память, приведет к непредсказуемому поведению программы. Однако, как было сказано выше, этот оператор можно применять к нулевому указателю.

Вот три основные ошибки, связанные с динамическим выделением памяти:

• не освободить выделенную память. В таком случае память не возвращается в хип. Эта ошибка получила название утечки памяти;

• дважды применить оператор delete к одной и той же области памяти. Такое бывает, когда два указателя получают адрес одного и того же динамически размещенного объекта. В результате подобной ошибки мы вполне можем удалить нужный объект.

• изменять объект после его удаления. Такое часто случается, поскольку указатель, к которому применяется оператор delete, не обнуляется.

2. Выделение памяти под нетипизированный указатель

Оператор new выделяет область памяти размером соответствующим заданному типу. Программист имеет возможность выделить область памяти заданного размера. Для этого используются функции семейства alloc.

#include <stdlib.h> or #include<alloc.h>

void *malloc(unsigned size);

Функция malloc() выделяет блок памяти размером size байт. В случае успеха, malloc() возвращает указатель на выделенный блок памяти. Если недостаточно памяти для нового блока, это возвращается пустой указатель (NULL). Содержание блока остается неизменным

Функция calloc()

#include <stdlib.h>

void *calloc(unsigned nitems, unsigned size);

выделяет блок памяти размером nitems*size. Блок инициализируется нулями. Возвращаемые значения аналогичны функции malloc().

Перевыделить память позволяет функция realloc().

void *realloc(void *block, unsigned size);

realloc пытается изменить размер предварительно выделенного блока. Если size — ноль, блок памяти освобожден, и возвращается NULL. Аргумент block указывает на память, предварительно полученный, вызовами malloc, calloc, или realloc. Если block==NULL, realloc работает точно так же как malloc. realloc копирует содержимое старого блока памяти в новый. realloc возвращает адрес нового блока памяти, который может отличаться от старого.

Освобождение памяти, выделенной функциями calloc, malloc, или realloc осуществляется функцией free().

void free(void *block);

Работа с указателями

Указатель – это адрес памяти, распределяемой для размещения идентификатора (в качестве идентификатора может выступать имя переменной, массива, структуры, строкового литерала). В том случае, если переменная объявлена как указатель, то она содержит адрес памяти, по которому может находится скалярная величина любого типа. При объявлении переменной типа указатель, необходимо определить тип объекта данных, адрес которых будет содержать переменная, и имя указателя с предшествующей звездочкой (или группой звездочек). Формат объявления указателя:

спецификатор-типа [ модификатор ] * описатель.

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

В качестве модификаторов при объявлении указателя могут выступать ключевые слова const, near, far, huge. Ключевое слово const указывает, что указатель не может быть изменен в программе. Размер переменной объявленной как указатель, зависит от архитектуры компьютера и от используемой модели памяти, для которой будет компилироваться программа. Указатели на различные типы данных не обязательно должны иметь одинаковую длину.

Для модификации размера указателя можно использовать ключевые слова near, far, huge.

Примеры:

unsigned int * a; /* переменная а представляет собой указатель на тип unsigned int (целые числа без знака) */

double * x; /* переменная х указывает на тип данных с плавающей точкой удвоенной точности */

char * fuffer; /* объявляется указатель с именем fuffer который указывает на переменную типа char */

double nomer;

void *addres;

addres = & nomer;

(double *)addres ++;

/* Переменная addres объявлена как указатель на объект любого типа. Поэтому ей можно присвоить адрес любого объекта (& - операция вычисления адреса). Однако, как было отмечено выше, ни одна арифмитическая операция не может быть выполнена над указателем, пока не будет явно определен тип данных, на которые он указывает. Это можно сделать, используя операцию приведения типа (double *) для преобразования addres к указателю на тип double, а затем увеличение адреса. */

const * dr;

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

unsigned char * const w = &obj.

/* Переменная w объявлена как константный указатель на данные типа char unsigned. Это означает, что на протяжение всей программы w будет указывать на одну и ту же область памяти. Содержание же этой области может быть изменено. */

Адресная арифметика

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

Для указателей-переменных разрешены следующие операции: присваивания, сравнение, сложение, вычитание, инкремент ++ и декремент --.

Присваивание:

  1. указателю можно присвоить адрес переменной (не константы и не выражения);
  2. указателю можно присвоить указатель того же типа (или другого с приведением типа);
  3. указателю типа void * можно присвоить указатель любого типа;
  4. любому указателю можно присвоить NULL;
  5. указателю нельзя присвоить число. Константа ноль – единственное исключение из этого правила: ее можно присвоить указателю.

Язык С разрешает операцию сравнения указателей одинакового типа, при этом, по сути, сравниваются адреса в памяти.

Сравнение указателей в общем случае некорректно. Это связано с тем, что одним и тем же физическим адресам памяти могут соответствовать различные пары значений «сегмент смещение» (при страничной (сегментной) организации памяти разные страницы (сегменты) виртуальной памяти могут загружать в один и тот же физический блок оперативной памяти).

Но при определенных условиях указатели все же можно сравнивать. Если p и q указывают на элементы одного и того же массива, то такие отношения, как <, >= и т.д., работают надлежащим образом. Например, p < q истинно, если p указывает на более ранний элемент массива, чем q. Отношения == и!= тоже работают.

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

Если к указателю, описанному как type *p;, прибавляется или отнимается константа N, значение p изменится на N*sizeof(type).

int *p, i = 2;

int a[5] = {1, 2, 3, 4, 5};

p = &a[1]; // *p = 2

p++; // *p = 3, значение p увеличится на 2

p += i; // *p = 5, значение p увеличится на 2

p--; // *p = 4, значение p уменьшится на 2

p += 2; // *p = 2, значение p уменьшится на 4

В этом примере унарная операция p++ увеличивает p так, что он указывает на следующий элемент набора объектов того типа, что задан при определении указателя, а именно int. Здесь операция p += i увеличивает p так, чтобы он указывал на элемент, отстоящий на i элементов от текущего элемента.

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

Унарные операции * и & имеют более высокий приоритет, чем арифметические операции, так что присваивание

y = *ip + 1;

берет объект, на который указывает ip, и добавляет к нему 1, а результат присваивает переменной y. Аналогично

*ip += 1;

увеличивает на единицу объект, на который указывает ip; те же действия выполняют выражения

++*ip;

(*ip)++;

Два указателя на элементы одного и того же массива можно вычитать. Разность двух указателей type *p1, *p2; – это разность их значений, поделенная на sizeof(type), т.е. разность двух указателей – целое число, модуль которого определяет число на 1 большее, чем число элементов массива, расположенных между этими указателями (другими словами, получается разность индексов двух элементов массива). Понятно, что результат может быть отрицательным.

За исключением упомянутых выше операций (присваивание, сложение и вычитание указателя и целого, вычитание и сравнение двух указателей), вся остальная арифметика указателей является незаконной. Запрещено складывать два указателя, умножать, делить, сдвигать или маскировать их, а также прибавлять к ним переменные типа float или double.


Поделиться:





Воспользуйтесь поиском по сайту:



©2015 - 2024 megalektsii.ru Все авторские права принадлежат авторам лекционных материалов. Обратная связь с нами...