Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

lr07

.pdf
Скачиваний:
5
Добавлен:
19.02.2016
Размер:
732.65 Кб
Скачать

Лабораторна робота № 7

Покажчики Динамічний розподіл пам'яті

Мета роботи

Метою даної лабораторної роботи є отримання навичок по роботі з покажчиками. Ознайомлення з методами динамічного розподілу пам’яті за допомогою функцій виділення та звільнення пам’яті. Отримання навичок обробки масивів з використанням покажчиків.

Короткі теоретичні відомості до роботи

Покажчик – це зміна або константа стандартного типу даних для збереження адреси іншої змінної визначеного типу. Значення покажчика – це ціле число без знаку, яке містить адресу розміщення змінної в пам’яті і більше нічого не говорить про саму змінну. Тип змінної, що адресується може бути стандартний, визначений користувачем або void. Покажчик на тип void може адресувати значення будь-якого іншого типу. Розмір пам’яті для самого покажчика і формат збереження адреси (вмісту покажчика) залежить від типу комп’ютера та обраної моделі пам’яті.

Оголошення покажчиків

Зміна типу покажчик оголошується подібно звичайним змінним, але із застосуванням символу “*”.

Формат оголошення змінної-покажчика такий:

тип *ім'я_змінної;

Як і звичайна змінна покажчик може бути проініціалізований при оголошенні:

тип *ім'я_змінної = ініціалізатор;

Тут тип – найменування типу змінної, адресу якої буде містити зміна-покажчик. Покажчик містить адресу нульового байту цієї змінної, а тип змінної, що адресується визначає скільки байтів, починаючи з адреси визначеної покажчиком, займає це значення (див. рис. 1).

Ініціалізуватись покажчик може лише цілим значенням 0 (NULL в мові C) або адресою змінної відповідного покажчику типу. Гарантується, що немає об'єктів з нульовим адресою, а отже, покажчик, що дорівнює нулю, можна інтерпретувати як покажчик, який ні на що не посилається.

Приклади:

int

*pi;

// Покажчик на

int

char *pch;

// Покажчик на

char

int

*pi1, pi2;

//

Поганий стиль

оголошення, pi2 - не покажчик!

double *pd1, *pd2 = 0;

//

Два покажчики

на double

Операції адресації

Операція & називається операцією взяття адреси або адресації — це унарна операція, яка повертає адресу свого операнда. Наприклад, якщо в програмі є оголошення:

1

int a = 5; char c = 'G';

float r = 1.2e8;

Ці змінні будуть розміщені в пам’яті комп’ютера так, як показано на рис. 1. Якщо застосувати операцію & до змінних з даного прикладу, то &a дорівнюватиме FA01, &c FA05, &r FA06.

int *pa = &a; char *pc = &c; char *pc = &c;

Пам’ять

FA01

FA02

FA03

FA04

FA05

FA06

FA07

FA08

FA09

 

 

 

 

 

 

 

 

 

 

Змінна

 

a

 

c

 

r

 

 

 

 

 

 

Значення

5

 

'G'

1.2*108

 

 

 

 

 

 

 

 

Тип

 

int

 

char

 

float

 

 

 

 

 

 

 

 

 

 

Рис. 1. Змінні в пам’яті комп’ютера і покажчики на них

Отримані операцією & адреси можна використати для ініціалізації змінних-покажчиків:

int *pa = &a; char *pс = &c; float *pr = &r;

Операція * називається операцією непрямої адресації або операцією розіменування, повертає значення об'єкту, на який вказує її операнд (тобто покажчик). За допомогою цієї операції можна послатись через покажчик на відповідну змінну.

Після виконаних вище операцій присвоєння адрес покажчикам pa, , pr взаємозамінними стають a і *pa, c і *pc, r і *pr. Наприклад два оператори

x = a + 2; x = *pa + 2;

тотожні. В x буде занесене значення 7.

В результаті виконання наступних операторів виводиться те ж самк значення змінної r:

cout << "Значення, отримане через покажчик на r: " << *pr << endl; cout << "Значення змінної r: " << r << endl;

Значення, отримане через покажчик на r: 1.2e+008 Значення змінної r: 1.2e+008

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

Над покажчиками можна робити наступні дії:

додавання покажчика цілого числа (операції +, ++);

віднімання від покажчика цілого числа (операції , ––);

віднімати покажчик з покажчика.

При додаванні/відніманні ціле число N, яке додається або віднімається позначає не кількість байт, а кількість елементів того типу, на який вказує покажчик. Тобто адреса, на яку вказує покажчик збільшується або зменшується на кількість байтів рівну добутку:

N*<розмір_типу_елементу>

Одиницею зміни значення покажчика є розмір відповідного йому типу.

2

Таким чином покажчик завжди вказує на нульовий байт змінної базового типу (рис. 1). У результаті додавання цілого числа до покажчика і віднімання цілого числа від покажчика виходить новий покажчик.

Взагалі, виконання операцій додавання/віднімання над покажчиками мають сенс, якщо покажчики вказують на масив даних, а не окремі змінні. Якщо отриманий в результаті виконання адресної арифметики покажчик не вказує на елемент того ж масиву, що і вихідний покажчик, то результат його використання є не визначеним.

Результат віднімання покажчиків один з одного також визначений тільки в тому випадку, якщо обидва покажчики вказують на елементи одного і того ж масиву. Результатом віднімання одного покажчика з іншого буде кількість елементів масиву між цими покажчиками.

Покажчики і масиви

Масиви і покажчики в C++ тісно пов'язані і можуть бути використані майже еквівалентно. Ім'я масиву можна розуміти як константний покажчик на його перший елемент (з індексом 0). Таким чином можна присвоїти покажчику адресу нульового елементу масиву:

int arr[] = {11, 22, 33, 44, 55}; int *pArr;

pArr = &arr[0];

//або просто

pArr = arr; // arr == &arr[0]

Покажчики можна використовувати для виконання будь-якої операції з елементами масиву, включаючи індексування масиву. Через операцію розіменування (*) можна отримати доступ до будь-якого елементу масиву. Наприклад всі наступні команди еквівалентні і виводять на екран значення 3-го елементу масиву arr (значення 33), оголошеного вище:

cout << arr[2] << endl;

// індексування імені масиву

 

cout << *(arr + 2) << endl;

// розіменований покажчик на

3-й елемент

 

// (змщення 2)

 

cout << pArr[2] << endl;

// індексування покажчика на

масив

cout << *(pArr + 2) << endl; // розіменований покажчик на

3-й елемент

 

// (змщення 2)

 

Запис типу

*(pArr + N);

називають записом покажчик-зміщення, де pArr — покажчик на початок масиву, а N — зміщення відносно початку масиву.

Змінна-покажчик на масив (в нашому прикладі pArr) не є константою на відміну від імені масиву (в нашому випадку arr), тому її значення можна змінювати. Збільшуючи чи зменшуючи значення такого покажчика на масив програміст отримує ще один спосіб доступу до елементів масиву. Наприклад, можна перемістити покажчик на наступний елемент масиву:

pArr++;

cout << *pArr << endl; // pArr тепер вказує на 2-й елемент (індекс 1)

Тепер розглянемо двовимірні масиви. Нехай в програмі є оголошення:

int Р[5][10];

Це матриця з п'яти рядків і десяти чисел в кожному рядку (десяти стовпчиків). Двовимірний масив розташований в пам'яті в послідовності по рядках. Як і у випадку одновимірних масивів Р є покажчиком-константою на масив, тобто на елемент Р[0][0]. Індексоване ім'я масиву Р[i]

3

вказує на адресу i-го рядка. Йому тотожно наступне позначення у формі розіменованого покажчика:

*(P + i*10)

Звернення до елементу масиву Р[2][4] можна замінити на *(Р+2*10+4). У загальному випадку еквівалентні позначення:

P[i][j] і *(P+i*10+j)

Тут двічі працює операція індексування «квадратна дужка». Останній вираз можна записати інакше, без явної вказівки на довжину рядка матриці Р:

*(*(P+i)+j)

Для посилання на елемент тривимірного масиву A[i][j][k] справедливий вираз

*(*(*(A+i)+j)+k)

Покажчики в якості аргументів функцій

Існують три способи передачі аргументів у функцію — виклик за значенням, виклик по посиланню з аргументами посиланнями і виклик по посиланню з аргументами покажчиками.

Раніше ми порівнювали і зіставляли виклики за значенням і по посиланню з аргументами посилання. Тепер ми зосередимося на виклику по посиланню з аргументами покажчиками.

Як ми бачили оператор return можна використати для повернення одного значення з функції, що викликається, викликаючому операторові (чи для передачі управління з функції, що викликається, без повернення якого-небудь значення). Ми також бачили, що аргументи можуть бути передані функції з використанням аргументів посилань, щоб дати можливість функції модифікувати початкові значення аргументів (таким чином, з функції може бути "повернено" більше за одне значення), або щоб передати функції великі об'єкти даних і уникнути накладних витрат, супутніх передачі об'єктів викликом за значенням (яка вимагає копіювання об'єкту). Покажчики, подібно до посилань, теж можна використати для модифікації одного або більше за значення змінних в викликаючому операторові, або передавати покажчики на великі об'єкти даних, щоб уникнути накладних витрат, супутніх передачі об'єктів за значенням.

Передача функції покажчика

Щоб передати функції покажчик, необхідно оголосити параметр типу покажчик. Розглянемо приклад.

// Передача функції покажчика

//

#include "stdafx.h" #include <iostream>

using namespace std;

void f(int *j); // Функція f() оголошує параметр-покажчик //на intзначення.

int _tmain(int argc, _TCHAR* argv[])

{

int i; int *p;

p = &i; // Покажчик p тепер посилається на змінну i. f(p); // Передаємо покажчик. Функція f() ушивається з

// покажчиком на intзначення.

4

cout << i; // Змінна i тепер містить число 100.

return 0;

}

// Функція f() приймає покажчик на intзначення. void f(int *j)

{

*j = 100; // Змінною, що адресується покажчиком j // привласнюється число 100.

}

Як бачите, в цій програмі функція f() приймає один параметр: покажчик на цілочисельне значення. У функції main() покажчику р присвоюється адреса змінної i. Потім з функції main() викликається функція f(), а покажчик р передається їй в якості аргументу. Після того, як параметр-покажчик j отримає значення аргументу р, він (так само як і р) вказуватиме на змінну i, визначену у функції main(). Таким чином, при виконанні операції присвоєння

*j = 100;

змінна i набуває значення 100. Тому програма відобразить на екрані число 100. У загальному випадку приведена тут функція f() присвоює число 100 змінній, адреса якої була передана цій функції в якості аргументу.

У попередньому прикладі необов'язково було використовувати змінну-покажчик р. Замість неї при виклику функції f() досить узяти змінну i, попередивши її оператором "&" (при цьому, як ви вже знаєте, генерується адреса змінної i). Тоді виклик функції в попередньому прикладі матиме вид:

f(&i);

Передаючи покажчик функції, необхідно розуміти наступне. При виконанні деякої операції у функція, яка використовує покажчик, ця операція фактично виконується над змінною, що адресується цим покажчиком. Це означає, що така функція може змінити значення об'єкту, що адресується її параметром.

Передача функції покажчика на масив

Якщо масив є аргументом функції, то необхідно розуміти, що при виклику такої функції їй передається тільки адреса першого елементу масиву, а не повна його копія. (Пам'ятаєте, що в С++ ім'я масиву без індексу є покажчиком на перший елемент цього масиву.) Взагалі існує три способи оголосити параметр, який приймає покажчик на масив. По-перше, параметр можна оголосити як масив, тип і розмір якого співпадає з типом і розміром масиву, використовуваного при виклику функції. Другий спосіб оголосити параметр як безрозмірного масиву. Ці варіанти оголошення параметра-масиву були розглянуті раніше. Розглянемо спосіб оголошення параметра-масиву як покажчика. Якраз цей варіант частіше всього використовується професійними програмістами.

// Передача покажчика на масив

//

#include "stdafx.h" #include <iostream>

using namespace std;

void display(int *num);

5

int _tmain(int argc, _TCHAR* argv[])

{

int t[10], i;

for (i=0; i<10; ++i) t[i] = i;

display(t); // Передаємо функції масив t.

return 0;

}

// Параметр тут оголошений як покажчик. void display(int *num)

{

int i;

for (i=0; i<10; i++)

cout << num[i] << ' '; cout << endl;

}

Можливість такого оголошення параметра пояснюється тим, що будь-який покажчик (подібно до масиву) можна індексувати, як це показано вище, за допомогою символів квадратних дужок ([]).

Передача функції покажчика на рядок

Оскільки рядки в С++ — це звичайні символьні масиви, які завершуються нульовим символом, то при передачі функції рядка реально передається тільки покажчик (типу char*) на початок цього рядка. Розглянемо, наприклад, наступну програму. У ній визначається і використовується функція copy(), яка копіює рядки.

// Копіювання рядка з використанням покажчиків

//

#include "stdafx.h" #include <iostream>

using namespace std;

void copy(char *, const char *);

int _tmain(int argc, _TCHAR* argv[])

{

char string1[10], *string2 = "Привіт";

copy(string1, string2);

cout << "string1 = " << string1 << endl;

return 0;

}

//Копіювання s2 в s1 з використанням нотації покажчиків void copy(char *s1, const char *s2)

{

for( ; (*s1 = *s2) != '\0'; s1++, s2++) ; //порожнє тіло циклу

}

string1 = Привіт

6

Динамічний розподіл пам'яті

Для C++-програми існує два основні способи зберігання інформації в оперативній пам'яті комп'ютера. Перший полягає у використанні змінних. Область пам'яті, що надається змінним, закріплюється за ними під час компіляції і не може бути змінена при виконанні програми. Другий спосіб полягає у використанні C++-системи динамічного розподілу пам'яті. В цьому випадку пам'ять для даних виділяється по мірі необхідності з області вільної пам'яті, яка розташована між вашою програмою (з її постійною областю зберігання) і стеком. Цей розділ пам’яті називається "куча" (heap). Розташування програми в пам'яті схематично показане на рис. 2.

Стек

Куча

Глобальні дані

Код

програми

Рис. 2. Використання пам’яті в C++-програмі

Динамічне виділення пам'яті — це отримання програмою пам'яті під час її виконання.

Іншими словами, завдяки цій системі програма може створювати змінні під час виконання, причому в потрібній (залежно від ситуації) кількості. Щоб задовольнити запит на динамічне виділення пам'яті, використовується пам'ять з розділу, який називається "кучею".

Мова C++ має два оператори, new і delete, які виконують функції по динамічному виділенню і звільненню пам'яті. Оператор new дозволяє динамічно виділити область пам'яті, а оператор delete звільняє динамічну пам'ять, раніше виділену оператором new. Їх загальний формат:

змінна-покажчик = new тип_змінної; delete змінна-покажчик;

Тут елементом змінна-покажчик є покажчик на значення, тип якого заданий елементом тип_змінної. Оператор new виділяє область пам'яті, достатню для зберігання значення заданого типу, і повертає покажчик на цю область пам'яті. За допомогою оператора new можна виділити пам'ять для значень будь-якого допустимого типу. Оператор delete звільняє область пам'яті, що адресується заданим покажчиком. Після звільнення ця пам'ять може бути знову виділена в інших цілях при подальшому запиті new на виділення пам'яті.

7

Нижче наведено приклад програми, яка ілюструє використання операторів new і delete (а саме виділяє динамічну пам'ять для зберігання цілого значення).

// Демонстрація використання операторів new і delete.

//

#include "stdafx.h" #include <iostream>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

int *p;

p = new int; // Виділення динамічної пам'яті для int-значення.

*p = 100;

cout << "За адресою " << p << '\n';

cout << "зберігається значення " << *p << '\n';

delete p; // Звільнення пам'яті.

return 0;

}

За адресою 004D0D60

зберігається значення 100

Ця програма привласнює покажчику р адресу (узяту з "кучі") області пам'яті, яка матиме розмір, достатній для зберігання цілого числа. Потім в цю область пам'яті поміщається число 100, після чого на екрані відображається її вміст. Нарешті, динамічно виділена пам'ять звільняється.

Оператор delete необхідно використовувати тільки з тим покажчиком на пам'ять, який був повернений в результаті запиту new на виділення пам'яті. Використання оператора delete з іншим типом адреси може викликати серйозні проблеми (навіть повну відмову системи).

Ініціалізація динамічно виділеної пам'яті

Використовуючи оператор new, пам'ять, що динамічно виділяється, можна ініціалізувати. Для цього після імені типу задається початкове значення (ініціалізатор), яке береться до круглих дужок. Загальний формат операції виділення пам'яті з використанням ініціалізатора:

змінна-покажчик = new тип_змінної(ініціалізатор);

Безумовно, тип ініціалізатору має бути сумісний з типом об'єкту, для якого виділяється пам’ять.

Наприклад, в попередній програмі область пам'яті, що адресується покажчиком р, можна було б ініціалізувати значенням 87.

int *p;

p = new int(87); // Ініціалізація динамічної пам'яті числом 87. cout << *p << endl; // Виводить 87 на екран

8

Виділення динамічної пам'яті для масивів

За допомогою оператора new можна виділяти пам'ять і для масивів. Загальний формат

операції виділення пам'яті для одновимірного масиву:

змінна-покажчик = new тип_массиву[розмір];

Тут елемент розмір задає кількість елементів в масиві.

Щоб звільнити пам'ять, виділену для динамічно створеного масиву, використовується такий формат оператора delete:

delete [] змінна-покажчик;

Тут квадратні дужки означають, що динамічно створений масив видаляється, а уся область пам'яті, виділена для нього, автоматично звільняється.

Наприклад, при виконанні наступної програми виділяється пам'ять для 10-елементного масиву цілих чисел.

// Виділення пам'яті для масиву.

//

#include "stdafx.h" #include <iostream>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])

{

int *p, i;

p = new int [10]; // Виділяємо пам'ять для 10-елементного масиву.

for (i = 0; i < 10; i++) p[i] = i;

for (i = 0; i < 10; i++) cout << p[i] << " ";

delete [] p; // Звільняємо пам'ятьі, зайнятої масивом.

return 0;

}

Зверніть увагу на інструкцію delete. Як згадувалося вище, при звільненні пам'яті, яку займав масив, за допомогою оператора delete необхідно використати квадратні дужки ([ ]). При динамічному виділенні пам'яті для масиву важливо пам'ятати, що його не можна одночасно і ініціалізувати.

9

Порядок виконання роботи

1.Створити проект Консольної програми Win32, в якому будуть реалізовані наступні пункти роботи.

2.Оголосити покажчик на будь-який стандартний тип C++ (наприклад: char, int, long, float, double тощо). Пов’язати покажчик зі змінною відповідного типу (операція &) і продемонструвати доступ (читання/запис) до значення змінної через покажчик (операція *). В режимі відлагодження (F10, F11) знайти адресу відповідної змінної і її значення (Вікно Локальные або Контрольные значения).

3.Оголосити масив та заповнити його довільними значеннями. Визначити покажчик на цей масив. Вивчити в продемонструвати можливості доступу до елементів масиву через покажчик. В режимі відлагодження знайти адреси елементів масиву та їх значення

(Вікно Локальные або Контрольные значения).

4.Виділити динамічну змінну стандартного типу C++, ввести з клавіатури значення створеної змінної, додати до неї довільне число, вивести результат на екран і видалити цю змінну з пам’яті.

5.Створити динамічний масив елементів стандартного типу C++. Перед створенням розмір масиву потрібно запитати у користувача.

6.Написати наступні функції та викликати їх передавши в якості аргументу створений в попередньому пункті динамічний масив.

a.Функція заповнення масиву випадковими значеннями, яка повинна приймати покажчик на масив та розмір масиву:

void fill_array(double *arr, int size);

b.Функція друку елементів масиву на екран, яка повинна приймати покажчик на масив та розмір масиву:

void display_array(double *arr, int size);

7.Написати функцію зміни розміру динамічного масиву із збереженням даних в масиві. Функція повинна приймати покажчик на масив, поточний і новий розміри масиву:

void resize_array(int *arr, int size_old, int size_new);

8.Після виконання всіх дій з динамічним масивом звільнити пам'ять виділену під масив.

9.Створити та заповнити довільними значеннями двовимірний масив. Написати функцію, яка присвоює значення 0 тим елементам двовимірного масиву, які менші за задане користувачем ціле число. Функція повинна приймати покажчик на масив, кількість рядків, стовпчиків і граничне значення. Прототип функції може бути таким:

void modify_array(int *arr, int rows, int cols, int zero_level);

10

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]