- •Краткие теоретические сведения
- •1.1.1Логическая структура памяти программы
- •1.1.2 Указатели. Типизированные указатели.
- •1.1.4Состояния указателей
- •1.2 Примеры работы с указателями
- •1.2.1Два указателя содержат адрес одной и той же переменной
- •1.2.2Ситуация утечки памяти
- •1.3 Задания для самопроверки
- •1.4 Создание проекта
- •1.5 Проведение исследований по использованию типизированных указателей в программе
- •1.3 Содержание отчета
- •Контрольные вопросы и задания
ЛАБОРАТОРНАЯ РАБОТА № 1.
Статические и динамические переменные.
Указатели.
Процедуры управления динамической памятью программы.
Цели работы:
Усвоить понятия «динамическое распределние памяти», «статическое распределение памяти», работа с переменными при статическом и динамическом распределении памяти, «указатели».
Получить практические навыки в создании динамических переменных разных типов.
Создать простейший проект в системе QT Creator на языке С++, в котором осуществляется работа с переменными в динамической памяти.
Краткие теоретические сведения
1.1.1Логическая структура памяти программы
Традиционно при запуске программы, ей выделяется некоторый участок оперативной памяти, который во время ее работы, в зависимости от назначения и способа управления данными, разделяют на следующие разделы :
-
Свободная область («куча»)
Стек
Область данных (Статическая память)
Область кода
Рисунок 1.1 – Распределение памяти программы до начала ее выполнения
Область кода – это память, используемая для хранения кода программы – функций. Эта область памяти выделяется при запуске программы до начала ее выполнения для размещения в ней данных. Ее размер остается неизменным до окончания работы программы. Поэтому все функции имеют глобальное время жизни и существуют в течении всего времени выполнения программы.
Статическое распределение памяти – это память, которая выделяется при запуске программы до начала ее выполнения для размещения в ней данных. Ее размер остается неизменным до окончания работы программы.
В статической памяти размещаются глобальные, статические переменные.
Стековая память используется для хранения локальных переменных при вызове функций.
Динамическая память — это совокупность блоков памяти, выделяемых из доступной свободной оперативной памяти («кучи») непосредственно во время выполнения программы под размещение конкретных объектов. В процессе работы программы размер динамической памяти, занимаемый ею, может изменяться.
«Куча» - часть свободной оперативной памяти компьютера, выделенная программе для размещения в ней динамически создаваемых объектов.
При динамическом распределении памяти требуемое пространство памяти выбирается из кучи (heap). В результате распределение памяти для программы приобретает вид, показанный на рисунке 1.2:
Рисунок 1.2 – Распределение памяти программы во время ее выполнения
Некоторые современные языки программирования, например С++, обладают средствами для поддержания механизмов распределения памяти средствами управления кучей. Для этого в них используются указатели и встроенные процедуры и функции.
1.1.2 Указатели. Типизированные указатели.
Обращение к переменным в динамически-распределяемой памяти (динамическим переменным) осуществляется через указатель.
Как известно, память компьютера разбита на ячейки размером 1 байт. Все ячейки пронумерованы. Номер ячейки памяти – это и есть адрес этой ячейки.
Если переменная занимает несколько байт памяти, то ее адресом будет номер первого байта занимаемого участка.
Указатель – это переменная, предназначенная для хранения некоторого адреса памяти.
В указателе можно хранить адрес данных (переменной) или подпрограммы (функции).
Переменная типа указатель занимает, на сегодняшний день, в памяти 4 байта.
Кроме значения конкретного адреса указатель может содержать NULL. Это означает, что указатель не указывает ни на какую ячейку памяти.
Указатели подразделяются на бестиповые и типизированные.
Бестиповый указатель (указатель на неопределенный тип) содержит адрес некоторого байта памяти. Если не использовать некоторый механизм преобразования типов, с помощью него нельзя обратиться к конкретной переменной. Бестиповые указатели рассматриваются в следующем семестре. Следует отметить, что в языке программирования С++ бестиповый указатель объявляется следующим образом (см. Рисунок 1.3):
-
void * имя_указателя;
Рисунок 1.3 – Пример объявления бестипового указателя
При объявлении такого указателя вместо типа адресуемых им данных указывается ключевое слово void.
Типизированный указатель – это переменная, предназначенная для хранения адреса переменной определенного типа.
Для типизированного указателя нет встроенного типа. Его должен описывать сам программист.
Используя операцию разыменования указателя, с помощью типизированного указателя можно обратиться к переменной в динамической или статической памяти.
Указатель может содержать как адрес переменной в статической памяти, так и адрес переменной в динамической памяти.
1.1.2.1 Объявление указателя на определенный тип
Объявление переменной указателя на определенный тип на языке C++ (далее такую переменную будем называть просто «указатель») в общем виде имеет вид (см. Рисунок 1.4):
тип * имя_переменной;
где <тип> - это тип переменной, адрес которой будет содержать указатель
|
Рисунок 1.4 – Объявление указателя в общем виде
Особенностью объявления указателя является использование символа «*». В данном случае символ «*» обозначает, что следующая за ней переменная является указателем.
Приведем пример объявления типизированного указателя, предназначенного для хранения адреса переменной целого типа int (см. рисунок 1.5).
int * p_i ; |
Рисунок 1.5 – Пример объявления переменной типизированного указателя на переменную типа int
При формировании названия переменной указателя будем придерживаться следующего правила:
- название любой переменной типа указатель будет начинаться с буквы p (pointer - указатель).
Если для решения какой-либо задачи необходимо объявить переменную-указатель на структуру или массив, то, сначала необходимо описать тип для записи или массива, а после этого объявлять указатель.
1.1.2.2 Объявление указателей
Пример объявления переменных типа типизированный указатель показан на рисунке 1.6:
typedef struct { char Name[30]; int Age; char Address[30]; } TStruct; //*тип для структуры
TStruct * pStruct; //объявление указателя на структуру типа TStruct int * pi; // объявление указателя на переменную типа int |
Рисунок 1.6 – Пример объявления указателя на структуру и указателя на переменную типа int
Для объявления типизированного указателя можно использовать заранее описанный для него тип. Пример объявления двух типизированных указателей, с использованием заранее описанных типов показан на рисунке 1.7.
typedef struct { char Name[30]; int Age; char Address[30]; } TStruct; //тип для структуры
Typedef TStruct * TPStruct; //тип указателя на структуру Typedef Tint * TPInt; //тип указателя на переменную типа int
TPStruct pStruct; //объявление переменной-указателя на структуру типа TStruct TPInt p_i; // объявление переменной-указателя на переменную типа int |
Рисунок 1.7 – Пример объявления указателя на структуру и указателя на переменную типа int с использованием заранее описанных типов для них
Если указатели имеют разный тип, то нельзя указателю одного типа присвоить значение указателя другого типа. Другими словами, если оба указателя, участвующие в операции присваивания, типизированные, то оба они должны указывать на объекты одного и того же типа.
TStruct * pStruct; //объявление указателя на структуру типа TStruct int * pInt; //объявление указателя на переменную типа int
pInt = pStruct; // недопустимая операция
|
Рисунок 1.8 – Пример недопустимой операции с указателем
В C++ нельзя указателю явно присвоить конкретное число. (См. рисунок 1.9)
int * pInt; //объявление указателя на переменную типа int
pInt = 4895778; // недопустимая операция |
Рисунок 1.9 – Пример недопустимой операции с указателем
При выполнении блока программы из рисунка 1.6 в статически-распределяемой (или стековой) памяти появятся две переменные типа указатель, которые будут занимать по 4 байта каждая. При этом, следует отметить, что в динамически-распределяемой памяти никаких переменных не появилось. См. рисунок 1.10.
Статическая память
p_i
pStruct
|
Динамическая память
|
Рисунок 1.10 – Размещение в памяти переменных после выполнения программы на рисунке 1.6
1.1.2.3 Допустимые операции для указателей на определенный тип
В языке C++ для типизированных указателей допустимы следующие операции:
1. Присваивание.
Указателю можно присвоить значение другого указателя такого же типа (рисунок 1.11):
{ int * p1; int * p3;
p1=p3; // допустимая операция, так как указатели одинаковых типов
} |
Рисунок 1.11 – Допустимая операция – присваивание значение одного указателя другому
Указателю любого типа можно присвоить значение константы NULL. Эта константа объявляется в одном из стандартных библиотечных модулей и содержит значение, которое не может быть адресом ячейки памяти. Обычно это значение 0. Если указатель содержит значение NULL, то он называется нулевым указателем, что означает, что он не указывает ни на какую ячейку памяти. (См. рисунок 1.12)
{ int * p1; int * p3;
p1=NULL; // допустимая операция
} |
Рисунок 1.12 – Допустимая операция – присваивание значения константы NULL указателю
Указателю можно присвоить адрес другой переменной, используя оператор взятия адреса &.
Бестиповому указателю можно присвоить значение типизированного указателя, но не наоборот. (См. рисунок 1.13)
{ int * p1; // переменная - типизированный указатель
void * p3; // переменная – указатель на неопределенный тип
int d; // переменная типа int
p1=&d; // указателю p1 присваивается адрес переменной d
p3=p1; // бестиповому указателю p3 присваивается значение типизированного указателя p1
p1=p3; // запрещенная операция
}
|
Рисунок 1.13 – Допустимая операция – присваивание указателю адреса статической переменной
2. Проверка на равенство и неравенство
Значения указателей можно сравнивать между собой.
Значение указателя можно сравнивать со значением NULL.
Пример таких операций см. на рисунке 1.14.
{ int * p1; // переменная - типизированный указатель
void * p3; // переменная – указатель на неопределенный тип
int d; // переменная типа int
p1=&d; // указателю p1 присваивается адрес переменной d
p3=&d; // указателю p3 присваивается адрес переменной d
// Пример сравнения значений указателей if (p1 != p3) {p3=NULL;} // если значения указателей p1 и p3 не равны, то значению указателя p3 присваивается NULL
// Пример сравнения значения указателя со значением NULL if (p3 == NULL) { p1=NULL;} // если значение указателя p3 равно NULL, то значению указателя p1 присваивается NULL
}
|
Рисунок 1.14 – Примеры операций сравнения указателей
3. Разыменование указателя
Для доступа к переменной через указатель используется операция разыменования, которая в C++ обозначается как – «*».
Обращение к значению переменной через указатель в общем виде и пример обращения имеет вид, показанный на рисунке 1.15.
Обращение к переменной через указатель на нее в общем виде:
* Типизированный указатель =
Пример: { int d; // переменная целого типа в статически-распределяемой памяти int * p1; // переменная типизированный указатель, которая находится в статически-распределяемой памяти
d = 14; // в переменную d заносим значение 14 p1 = &d; // указателю p1 присваивается адрес переменной d
// Теперь можем обратиться к переменной d не по имени, а через // указатель, используя операцию разыменования указателя p1 // Например, увеличим значение переменной d на 5
*p1 = *p1 + 5; // увеличение значение переменной d на 5 // теперь в переменной d содержится значение 19
}
|
Рисунок 1.15 – Общий вид и пример использования операции разыменования указателя.
Операция
* p1
для компьютера означает следующее: «Взять адрес в переменной p1 и обратиться к переменной по этому адресу».
При обращении к переменной через указатель вместо ее имени пишется «* указатель », в остальном же работа с такой записью ничем не отличается от обычной.
1.1.3 Переменные в статически-распределяемой памяти и динамические переменные. Операторы и функции С++ для управления динамической памятью
Динамические переменные – переменные, которые создаются в памяти во время выполнения программы.
Динамические переменные не имеют собственных имен, обращение к ним осуществляется только при помощи указателей.
Для создания динамических переменных в С++ существуют встроенный оператор new и функция malloc(). Для работы с типизированными указателями предпочтение следует отдавать оператору new.
Работа с функцией malloc будет рассматриваться в следующем семестре.
Для размещения переменной в динамической памяти используется оператор new. (см. Рисунок 1.16):
указатель = new тип;
|
Рисунок 1.16 – Использование оператора new в общем виде
Оператор new запрашивает свободный участок памяти размером, зависящим от типа переменной, на которую будет указывать указатель, у исполнительной системы и адрес первого байта выделенного участка памяти заносит в указатель .
В результате работы оператора new в динамической памяти появляется переменная – динамическая переменная.
Приведем пример создания динамической переменной целого типа int и занесения в нее значения, равного 36. Пример программы для решения такой задачи показан на рисунке 1.17.
{ int * p1 ; //объявляем переменную – типизированный указатель // в статически-распределяемой памяти выделено 4 байта для //указателя p1 – в нем «мусор»
// Пока еще в памяти нет динамической переменной // Теперь используем оператор new для запроса памяти для динамической // переменной
p1 = new int ;
// Теперь в динамической памяти появилась переменная типа int // и переменная-указатель p1 содержит ее адрес (см.Рисунок 1.17)
// Заносим в переменную в динамической памяти значение 36, путем // разыменования указателя P1
*p1 = 36; } |
Рисунок 1.16 – Пример создания динамической переменной и занесения в нее определенного значения.
В результате действий, выполненных программой из рисунка 1.16, в памяти программы данные будут представлены следующим образом (Рисунок 1.17):
Рисунок 1.17 – Представление данных в памяти после выполнения программы рисунка 1.16
Для оператора и функции управления динамической памятью new и malloc() существуют оператор и функция, которые выполняют противоположное действие – освобождают динамическую память.
Если память запрашивалась при помощи оператора new, тогда ее освобождение производится оператором delete:
delete указатель;
Если же память запрашивалась функцией malloc, то освободит ее вызов функции free ():
В результате работы обеих механизмов освобождения памяти указатель принимает неопределенное значение, но НЕ NULL.
Рассмотрим различия между переменными в статически-распределяемой памяти и динамической переменными, которые показаны в таблице 1.1.
Таблица 1.1 – Сравнительные характеристики переменной в статически-распределяемой памяти и динамической переменной
Параметр сравнения |
Переменная в статической памяти |
Динамическая |
Момент появления |
Появляется в момент, когда компилятор обрабатывает строку объявления переменных |
Появляется в результате выполнения оператора new или функции malloc() |
Способ обращения к переменной |
По имени или через указатель |
Только через указатель |
В какой момент уничтожается |
В момент окончания программы или подпрограммы |
В результате выполнения оператора освобождения памяти – delete или free() |
Кроме перечисленных отличий, более никакой разницы нет при работе с переменными в статической и динамической памяти.
