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

10 Указатели

.pdf
Скачиваний:
23
Добавлен:
20.03.2016
Размер:
600.25 Кб
Скачать

МІНІСТЕРСТВО ОСВІТИ І НАУКИ УКРАЇНИ

Національний технічний університет «Харківський політехнічний інститут»

МЕТОДИЧНІ ВКАЗІВКИ до лабораторної роботи

«Використання вказівників у програмах мовою C++»

з курсу «Програмування» для студентів напряму 6.040302 – Інформатика

і курсу «Програмування та алгоритмічні мови» для студентів напряму 6.040303 – Системний аналіз

Затверджено редакційно-видавничою радою університету, протокол № 2 від 06.12.12.

Харків НТУ «ХПІ»

2013

Методичні вказівки до лабораторної роботи «Використання вказівників у програмах мовою C++» з курсу «Програмування» для студентів напряму 6.040302 – Інформатика і курсу «Програмування та алгоритмічні мови» для студентів напряму 6.040303 – Системний аналіз / Уклад. М. І. Безменов, О. М. Безменова. – Х. : НТУ «ХПІ», 2013. – 24 с.

Укладачі: М. І. Безменов, О. М. Безменова

Рецензент І. П. Гамаюн

Кафедра системного аналізу і управління

© Безменов М.І., Безменова О. М., 2013

ВСТУП

Можливість безпосереднього доступу до пам’яті за допомогою вказівників – один з найбільш значущих засобів програмування мовою C++. При роботі з вказівниками програмісти досить часто зіштовхуються зі специфічними проблемами, оскільки в деяких ситуаціях механізм роботи вказівників може виявитися доволі заплутаним.

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

1.ТЕОРЕТИЧНІ ОСНОВИ

1.1.Загальні відомості про вказівники

Кожна змінна в програмі – це об’єкт, що має ім’я і значення. З точки зору машинної реалізації ім’я змінної відповідає адресі тієї ділянки пам’яті, яка виділена цій змінній, а значення змінної – умісту цієї ділянки пам’яті.

Щоб отримати адресу, у мові C++ використовують унарну операцію &. Таким чином, якщо змінна має ім’я v, то її адресою є &v. Операція & може застосовуватися тільки до об’єктів, які мають ім’я і розміщені в пам’яті. Вона не застосовується до констант, виразів, бітових полів структур, регістрових змінних, файлів.

Для зберігання адрес використовують змінні спеціального типу – «вказівники». При цьому значенням вказівника є адреса змінної конкретного типу, і не можна у вказівниках одного типу зберігати адреси змінних іншого типу.

При описанні або визначенні вказівника найпростішим є використання такого формату:

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

Наприклад:

char *ch; int *n1, *n2; double *f;

Тут символ * – це операція розіменування, яка застосовується до адреси. Результатом цієї операції є об’єкт, на який указує вказівник. Таким чином, *n1

– це змінна типу int, а n1 – адреса, за якою зберігається значення типу int. Зі сказаного раніше випливає, що оператор

n1 = n2;

3

є коректним, а оператор n1 = ch;

некоректний, оскільки в останньому випадку типи лівої і правої частин не співпадають, і для виконання оператора треба застосувати операцію перетворення типу:

n1 = (int*) ch;

УC++ визначений тип void, якому відповідає відсутність значення. Не можна оголосити змінну типу void, але вказівник на тип void визначити можна, і такому вказівнику без додаткового перетворення можна присвоїти вказівник на будь-який тип (але не навпаки – у цьому випадку треба перетворювати тип).

Узаголовковому файлі stdio.h, а також у деяких інших заголовкових файлах визначена константа NULL, яка позначає посилання на неіснуючу адресу (умовне нульове значення вказівника). Це значення без перетворення типу можна записувати в будь-який вказівник.

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

тип *ім’я_вказівника = вираз_що_ініціалізує;

або

тип *ім’я_вказівника (вираз_що_ініціалізує);

Вираз_що_ініціалізує може бути

явно заданою адресою:

char *p_char = (char *)0xB8000000;

вказівником, що вже має значення:

char *p_char = (char *)0xB8000000; char *p_screen = pchar;

виразом, який дозволяє отримати адресу за допомогою операції &:

char *ch;

char *pt = &ch;

1.2. Операції з вказівниками

Над вказівниками можна виконувати ряд операцій: присвоювання (=);

4

розіменування, або доступу за адресою (*);

перетворення типів;

отримання адреси (&);

підсумовування і віднімання (+ і );

присвоювання після підсумовування і присвоювання після віднімання

(+= і -=);

інкремент, або автозбільшення (++);

декремент, або автозменшення (––);

індексування ([]);

порівняння (==, !=, <, >, <=, >=);

динамічного розподілу пам’яті (new);

звільнення пам’яті (delete).

Розглянемо перелічені операції.

Присвоювання

Особливістю операції присвоювання вказівнику значення є те, що автоматичне приведення типів при цьому не здійснюється, і при присвоюванні вказівнику деякого типу значення іншого типу (у тому числі значення вказівника) потрібне виконання явного перетворення типу. Як про це говорилось раніше, винятком є вказівник на тип void. Так, для наступних визначень:

int n, *p_int; long int *p_long;

double d, *p_double; void *p_void;

bool *p_bool;

помилковими будуть оператори

p_int = &d; p_long = p_int; p_double = p_void;

а оператори

p_int = &n; p_double = &d; p_void = p_int; p_int = NULL; p_int = (int *) &d;

p_long = (long *) p_int; p_long = (long int *) p_int; p_double = (double *) p_void; p_bool = (bool *) p_void;

5

є правильними.

Розіменування

Операція розіменування записується в такому форматі:

(тип *) вираз

Вона неодноразово розглядалась раніше. З її допомогою можна, наприклад, отримати абсолютне значення адреси, на яку посилається вказівник:

int

*p = (int

*)0xB8000000;

 

int

a = (int)

p;

// a == 134217728

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

int n = 10, *p; p = &n;

справедливим є співвідношення *p == 10.

Отримання адреси

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

bool *p_bool;

void *p_void = &p_bool;

Підсумовування і віднімання

Якщо два вказівники мають той самий тип, то до них застосовна операція віднімання, результатом виконання якої є «відстань» між ділянками пам’яті, на які вказують вказівники, в одиницях, кратних довжині базового типу вказівників (але не в байтах). Результат віднімання вказівників має тип unsigned long. При цьому різниця між двома числовими значеннями вказівників дорівнює кількості байт, на яку зміщені в пам’яті два вказівники відносно один до одного. Віднімання вказівників різного типу заборонене.

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

Наприклад, нехай виконуються такі оператори:

double d;

double *pd1, *pd2; int k;

pd1 = &d; cin >> k;

pd2 = pd1 + k;

6

У результаті вказівник pd2 буде вказувати на ділянку пам’яті, яка зміщена відносно адреси змінної d на k * sizeof(double) байт. При цьому напрям зсуву залежитиме від знаку значення змінної k.

Слід ураховувати те, що операції додавання (вирахування) вказівника до (з) цілого значення можуть привести до того, що вказівник буде адресувати комірку пам’яті, доступ до якої не передбачався в програмі. Збільшення/зменшення вказівника за допомогою операцій «+» і «-» є більш менш безпечним тільки при обробці вмісту масивів, оскільки масиви завжди займають безперервну ділянку пам’яті, причому адреси елементів масиву збільшуються зі збільшенням їх індексів незалежно від того, де визначений масив.

Наприклад, підсумовування елементів масиву можна забезпечити виконанням такого фрагменту програми:

int a[] = {2, 3, 4}, sum = 0;

for (int *p = &a[0], i = 0; i < 3; i++, p++) sum += *p;

Але виконуватимуться і наступні два оператори for:

for (int *p = &a[0], i = 0; i >= -10; i--, p--) sum += *p;

for (int *p = &a[0], i = 0; i < 30; i++, p++) sum += *p;

При цьому, якщо перший з трьох операторів for здійснює обробку тільки елементів масиву, то інші два оператори for підсумовують ще й вміст комірок пам’яті, які в масив не входять.

Підсумовування вказівників заборонене.

Операції += і -= виконуються з урахуванням правил виконання операції присвоювання і операцій + та -.

Інкремент і декремент

Операції ++ і -- виконуються аналогічно підсумовуванню (відніманню) одиниці до (з) вказівника.

Індексування

Операція індексування вказівника забезпечує доступ до об’єкта, який змі-

щений у пам’яті на величину, що визначається значенням індексу. При цьому зсув у байтах дорівнює добутку значення індексу і розміру базового типу. Таким чином, якщо i – деяке значення цілого типу, а P – вказівник з деяким базовим типом, то має місце співвідношення P[i] == *(P + i).

Звичайним образом над вказівниками виконуються всі операції порівняння. При цьому порівнювати можна тільки вказівники з тим самим базовим типом.

7

Операції new і delete розглядатимуться окремо.

За винятком операції присвоювання і операцій порівняння, ніякі дії над вказівниками з базовим типом void не визначені.

1.3. Константи і вказівники

Вказівник може бути оголошеним константою. У цьому разі після ініціалізації він завжди буде посилатися на ту саму ділянку пам’яті і змінити це посилання виявляється неможливим. Формат оголошення константного вказівника (вказівника-константи) такий:

тип * const ім’я_вказівника ініціалізатор;

Наприклад:

double e = 2.72; double pi = 3.14;

double * const Pt_const = π

Надалі в програмі можливе будь-яке змінення змінної pi, але змінити значення змінної Pt_const не вдасться. Так, якщо за допомогою оператора

pi = 3.1415;

здійснити задавання для змінної pi більш точного значення, то значенням виразу *Pt_const також буде число 3.1415. У той же час оператор

*Pt_const = &e;

діагностуватиметься компілятором як помилковий. Вказівник може посилатися на константу:

тип const *ім’я_вказівника ініціалізатор;

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

Так, синтаксично правильними є такі оператори:

const int ZERO=0, ONE=1; const int *p_to_const = &ZERO; p_to_const = &ONE;

p_to_const = NULL;

Тут вказівник по черзі посилається на різні константи. Однак оператор

*p_to_const = 7;

є синтаксично помилковим – він здійснює спробу змінити значення константи.

8

Можливе також використання константних вказівників на константу:

тип const * const ім’я_вказівника ініціалізатор;

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

Звичайний вказівник може отримати адресу іменованої константи, але при цьому необхідно приведення типів:

const int ZERO=0;

int * p_to_const = (int *) &ZERO;

1.4. Вказівники і масиви

Особливістю масивів є те, що ім’я масиву є константним вказівником, який адресує перший елемент. Особливістю імені масиву як вказівника є те, що до нього застосовні всі операції, які не приводять до змінення значення вказівника. Насамперед, мова йде про операцію індексування, яка при обробці вмісту масиву зустрічається частіш за все. Можна також додавати (віднімати) ціле значення до (з) імені масиву, отримуючи вказівник, що адресує елемент масиву з індексом, який визначається доданим (вирахуваним) числом Так, якщо A – масив з елементами будь якого типу, а k – ціле значення, то

A + k == A[k],

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

Операція & при її застосуванні до імені масиву дає адресу елементу з індексом 0 (але іншого типу). У той же час у будь-який вказівник можна записати «значення» імені масиву з тим самим базовим типом. Операції перетворення типу і порівняння, застосовані до імені масиву, виконуються за звичайними правилами, але при порівнянні повинне бути жорстке дотримання типів.

Особливе місце займає операція розміру об’єкта або типу sizeof(). Якщо її застосувати до імені статичного масиву, то результатом її виконання є обсяг пам’яті в байтах, яку фактично займає масив. Застосування ж цієї операції до будь-якого іншого вказівника дає розмір пам’яті, відведеної під вказівник (завжди 4).

2. ПРИКЛАДИ ПРОГРАМ

Приклад 1. Дано натуральне число n і масив з n цілих чисел ( n 20 ). Чи правда, що вміст масиву однаково читається в прямому і зворотному напрямках?

9

Розв’язок.

#include <iostream>

 

#include <conio.h>

 

using namespace std;

 

int main()

 

{

 

int n;

// Кількість елементів масиву

int a[20];

// Масив

cout << "n = ";

 

cin >> n;

 

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

 

{

cout << "a[" << i << "] = ";

cin >> *(a + i); // Доступ до окремих елементів масиву // здійснюється додаванням до вказівника цілого

}

 

//

числа і розіменуванням

bool flag = true;

 

 

 

// Вказівники pt1 і pt2 встановлюються на початок

for (int *pt1 =

a, *pt2 = a + n - 1;

// і кінець масиву

pt1 < pt2;

pt1++, pt2--)

//

і зсуваються назустріч

// один одному, поки виконується умова pt1 < pt2 if (*pt1 != *pt2) // Порівняння двох елементів масиву

{

flag = false; break;

}

if (flag)

cout << "Yes"; else

cout << "No";

cout << "\nPress any key"; _getch();

return 0;

}

Приклад 2. Дано натуральне число n ( n 100 ) і дійсні числа v1 , v2 , …,

vn , w1 , w2 , …, wn . Обчислити (v1 wn ) (v2 wn 1 ) (vn w1 ) .

Розв’язок.

#include <iostream> #include <conio.h> using namespace std; int main()

{

10