Студопедия.Орг Главная | Случайная страница | Контакты | Мы поможем в написании вашей работы!  
 

Покажчики



1.8.1 Основні відомості про покажчики

В результаті процесу компіляції програми всі імена змінних будуть перетворені в адреси комірок пам'яті, в яких містяться відповідні значення даних. У командах машинної програми при цьому знаходяться машинні адреси розміщення значень змінних. Саме це і є пряма адресація - виклик значення за адресою в команді. Наприклад, в операторі присвоювання: k = j на машинному рівні відбувається копіювання значення з області ОП, що відведена змінній j, в область ОП, яка відведена змінній k. Таким чином, при виконанні машинної програми реалізуються операції над операндами - значеннями змінних, розташованими за визначеними адресами ОП. На машинному рівні імена змінних у командах не використовуються, а тільки адреси, сформовані транслятором з використанням імен змінних. Проте програміст не має доступу до цих адрес, якщо він не використовує покажчики.

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

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

Тип змінної, що адресується, може бути стандартний, нумерований, структурний, об'єднання або void. Покажчик на тип void може адресувати значення будь-якого типу. Розмір пам'яті для самого покажчика і формат збереженої адреси (вмісту покажчика) залежить від типу комп'ютера та обраної моделі пам'яті. Константа NULL зі стандартного файлу stdio.h призначена для ініціалізації покажчиків нульовим (незайнятим) значенням адреси.

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

тип [модифікатор] * імені-покажчика;

де тип - найменування типу змінної, адресу якої буде містити змінна-покажчик (на яку він буде вказувати).

Модифікатор необов'язковий і може мати значення:

near - ближній, 16-бітний покажчик (встановлюється за замовчуванням), призначений для адресації 64-кілобайтного сегмента ОП;

far - дальній, 32-бітний покажчик, містить адресу сегмента і зсув у ньому: може адресувати ОП обсягом до 1 Мб;

huge - величезний, аналогічний покажчику типу far, але зберігається у нормалізованому форматі, що гарантує коректне виконання над ним операцій; застосовується до функцій і до покажчиків для специфікації того, що адреса функції або змінної, що адресується, має тип huge;

• імені-покажчика - ідентифікатор змінної типу покажчик;

• визначає змінну типу покажчик.

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

Нижче наведено приклади деяких можливих оголошень покажчиків:

int *pi; /* - покажчик - змінна на дані типу int */

float *pf; /* - покажчик - змінна на дані типу float */

int ml [5]; /* - ім'я масиву на 5 значень типу int; ml - покажчик-константа, про це йтиметься згодом */

int *m2[10]; /* m2 - ім'я масиву на 10 значень типу покажчик на значення типу int, m2 - покажчик-константа */

int (*m3)[10]; /* - покажчик на масив з 10 елементів типу int; m3 - покажчик-константа */

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

За допомогою покажчиків, наприклад, можна:

1. обробляти одновимірні та багатовимірні масиви, рядки, символи, структури і масиви структур;

2. динамічно створювати нові змінні в процесі виконання програми;

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

4. передавати функціям адреси фактичних параметрів;

5. передавати функціям адреси функцій в якості параметрів.

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

1.8.2 Моделі пам'яті

У мові Сі для операційної системи MS-DOS розмір ОП (оперативної пам'яті) для розміщення покажчика залежить від типу використаної моделі пам'яті. У програмах на мові Сі можна використовувати одну з шести моделей пам'яті: крихітну (tiny), малу (small, по замовчуванню), середню (medium), компактну (compact), велику (large) і величезну (huge).

Взагалі оперативна пам'ять для виконання програми на мові Сі використовується для:

• розміщення програми (коду програми);

• розміщення зовнішніх (глобальних) і статичних даних (що мають специфікатори extern і static, про них йтиметься нижче);

• динамічного використання ОП для змінних, сформованих у процесі виконання програми (купа, динамічна ОП, про них йтиметься нижче);

• для розміщення локальних (auto - автоматичних) змінних, змінних функцій (стек) під час виконання програми.

Рис. 1.10. Структура оперативної пам'яті

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

Крихітна (tiny model) ОП. Модель пам'яті використовується при дефіциті ОП. Для коду програми, статичних даних, динамічних даних (купи) та стеку виділяється 64 Кб. Змінна - покажчик типу near (ближній) займає 2 байти.

Мала (small model) ОП. Для програми призначається 64 Кб. Стек, купа і статичні дані займають по 64 Кб. Ця модель приймається по замовчуванню та використовується для вирішення маленьких і середніх задач. Покажчик типу near займає 2 байти і містить адресу - зсув усередині сегмента ОП з 64 Кб.

Середня (medium model) ОП. Розмір ОП для програми дорівнює 1 Мбайт. Стек, купа і статичні дані розміщаються в сегментах ОП розміром 64 Кб. Цю модель застосовують для дуже великих програм і невеликих обсягів даних. Покажчик у програмі типу far займає 4 байти. Для адресації даних покажчик типу near займає 2 байти.

Компактна (compact model) ОП. Для програми призначається 64 Кб. Для даних - 1 Мбайт. Об'єм статичних даних обмежується 64 Кб. Розмір стека повинен бути не більш 64 Кб. Ця модель використовується для малих і середніх програм, що вимагають великого об'єму даних. Покажчики в програмі складаються з 2 байтів, а для даних - з 4 байтів.

Велика (large model) ОП. ОП для програми обмежена 1 Мб. Для статичних даних призначається 64 Кб. Купа може займати до 1 Мб. Програма і дані адресуються покажчиками, що займають 4 байти. Модель використовується для великих задач. Окрема одиниця даних, наприклад масив, повинна займати не більш 64 Кб.

Величезна (huge model) ОП. Аналогічна великій моделі. Додатково в ній знімається обмеження на розмір окремої одиниці даних.

1.8.3 Основні операції над покажчиками

Мова Сі надає можливість використання адрес змінних програми за допомогою основних операцій - & та *:

За допомогою основних операцій можна отримати значення адреси змінної а використовуючи непряму адресацію - одержати значення змінної за її адресою.

Призначення цих операцій:

& ім'я змінної - одержання адреси; визначає адресу розміщення значення змінної визначеного типу;

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

Оператор присвоювання значення адреси покажчику має вигляд:

Ім'я_змінної_покажчика = & ім'я змінної;

Наприклад:

int i, *pi; /* pi -змінна покажчик */

pi = &i; /* pi одержує значення адреси 'i' */

Операція & - визначення адреси змінної повертає адресу ОП свого операнда. Операндом операції & повинне бути ім'я змінної того ж типу, для якого визначений покажчик лівої частини оператора присвоювання, що одержує значення цієї адреси. У вищенаведеному прикладі це тип int.

Операції * і & можна писати впритул до імені операнду або через пробіл. Наприклад: &і, * pi.

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

У загальному вигляді оператор присвоювання, що використовує ім'я покажчика та операцію непрямої адресації, можна представити у вигляді:

ім'я змінної * ім'я-покажчика;

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

Наприклад:

i= *pi; /* 'i' одержує значення, розташоване за адресою, що міститься в покажчику 'pi' */

Як і будь-які змінні, змінна pi типу покажчик має адресу і значення. Операція & над змінною типу покажчик: &pi - дає адресу місця розташування самого покажчика, pi - ім'я покажчика визначає його значення, a *pi - значення змінної, що адресує покажчик.

Звичайно, усі ці значення можна надрукувати. Наприклад, за допомогою наступної програми:

#include <stdio.h>

void main()

{

char c = 'A';

int i = 7776;

int *pi = &i;

char *pc = &c;

printf ("pi=%u,*pi=%d, &pi=%u\n", pi, *pi, &pi);

printf ("pc=%u, *pc=%c, &pc=%u\n", pc, *pc, &pc);

}

У результаті виконання буде виведено:

pi = 65522, *pi = 7776, &pi = 65520

pc = 65525, *рс = А, &pc = 65518

Одне з основних співвідношень при роботі з покажчиками - це симетричність операцій адресації та непрямої адресації. Вона полягає в тому, що:

&х == х, тобто вміст за адресою змінної х є значення х. Наприклад, оголошення покажчика pi і змінних i та j:

int *pi, i = 123, j;

pi = &i; /*-присвоювання покажчику значення адреси i */

j = *pi; /* - присвоювання j вмісту за адресою pi */

Тут змінна j отримує вміст, розташований за адресою змінної i, тобто значення змінної, що адресує покажчик pi: j = * pi = * &i = i;. Два останніх вищенаведених оператора виконують те саме, що один оператор: j = i.

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

void func()

{

int х;

int *pх; /* pх - покажчик на змінну типу int*/

pх= &х; /* адреса змінної х заноситься в рх*/

*pх=77; /* число зберігається за адресою, на яку вказує рх */

}

Розглянемо цей приклад на конкретному малюнку: функція займає область пам'яті, починаючи з адреси 0х100, х знаходиться за адресою 0х102, а рх - 0х106. Тоді перша операція присвоювання, коли значення &х(0х102) зберігається в рх, матиме вигляд, зображений на рис. 1.11 зліва:

Наступну операцію, коли число 77 записується за адресою, яка знаходиться в рх та дорівнює 0х102 (адреса х), відображає рис. 1.11 справа. Запис *рх надає доступ до вмісту комірки, на яку вказує рх.

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

#include<stdio.h>

void main()

{

int i = 123, *pi = &i; /* pi-покажчик на значення типу int */

printf("розмір покажчика pi = %d\n", sizeof(pi));

printf("адреса розміщення покажчика pi=%u\n", &pi);

printf("адреса змінної i = %u\n", &i);

printf("значення покажчика pi = %u\n", pi);

printf("значення за адресою pi = %d\n", *pi);

printf("значення змінної i = %d\n", i);

}

Результати виконання програми:

розмір покажчика pi = 2

адреса розміщення покажчика pi = 65522

адреса змінної i= 65524

значення покажчика pi = 65524

значення за адресою pi = 123

значення змінної i = 123

Покажчики можна використовувати:

1. у виразах, наприклад, для одержання значень, розташованих за адресою, що зберігається у покажчику;

2. у лівій частині операторів присвоювання, наприклад:

a. для одержання значення адреси, за якою розташоване значення змінної;

b. для одержання значення змінної.

Наприклад, якщо pi - покажчик цілого значення (змінної i), то *pi можна використовувати в будь-якому місці програми, де можна використовувати значення цілого типу. Наприклад:

int i = 123, j, *pi;

pi = &i; /*pi у лівій частині оператора присвоювання */

j = *pi + 1; /*-це еквівалентно: j = i + 1; pi-у виразі правої частини оператора присвоювання*/

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

d = sqrt ((double) *pi); /* *pi - фактичний параметр */

fscant (f, "%d", pi); /* pi - фактичний параметр */

printf ("%d\n", *pi); /* *pi - фактичний параметр */

У виразах унарні операції & і *, пов'язані з покажчиками, мають більший пріоритет, ніж арифметичні. Наприклад:

*рх = &х;

у = 1 + *рх; /*-спочатку виконується '*', потім '+' */

Останній оператор еквівалентний:

у = 1 + х;

Для звертання до значення за допомогою покажчика-змінної його можна використовувати в операторі присвоювання скрізь, де може бути ім'я змінної. Наприклад, після виконання оператора: рх = &х; цілком еквівалентними є такі описи:

Оператор: Його еквівалент: Або:

*рх =0; х = 0;

*рх += 1; *рх = *рх + 1; х = х + 1;

(*рх)++; *рх = *рх + 1; х = х + 1;

(*рх)--; *рх = *рх - 1; х = х - 1;

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

#include <stdio.h>

char c; /* змінна символьного типу*/

main()

{

char *pc; /* покажчик на змінну символьного типу*/

pc=&c;

for(c='A';c<='Z';c++)

printf("%c",*pc);

return 0;

}

У операторі printf("%c",*pc) має місце розіменування покажчика (*рс) - передача у функцію значення, що зберігається за адресою, яка міститься у змінній рс. Щоб дійсно довести, що рс є псевдонімом с, спробуємо замінити *рс на с у виклику функції - і після заміни програма працюватиме абсолютно аналогічно. Оскільки покажчики обмежені заданим типом даних, типовою серйозною помилкою їх використання буває присвоєння адреси одного типу даних покажчика іншого типу, на що компілятор реагує таким чином:

"Suspicious pointer conversion in function main()"

На ТС це лише попередження (підозріле перетворення покажчика у функції main()(?!)), і якщо на нього ніяк не відреагувати, то програма працюватиме й надалі (адже помилку зафіксовано не буде) і залишається лише здогадуватися, який результат буде надалі. Зазначимо, що компілятор BС++ з приводу такого "підозрілого перетворення" пішов все-таки далі: він просто відмовляється працювати, видаючи повідомлення про помилку. Відповідальність за ініціалізацію покажчиків повністю покладається на програміста, і більш детально про це йтиметься далі.

1.8.4 Багаторівнева непряма адресація

У мові Сі можна використовувати багаторівневу непряму адресацію, тобто непряму адресацію на 1, 2 і т.д. рівні. При цьому для оголошення і звертання до значень за допомогою покажчиків можна використовувати відповідно кілька символів зірочка: *. Зірочки при оголошенні ніби уточнюють призначення імені змінної, визначаючи рівень непрямої адресації для звертання до значень за допомогою цих покажчиків. Приклад оголошення змінної і покажчиків для багаторівневої непрямої адресації можна привести наступний:

int i = 123 /* де: i - ім'я змінної */

int *pi = &i; /* pi - покажчик на змінну і */

int **ppi = &pi; /* ppi - покажчик на покажчик на змінну pi */

int ***pppi = &ppi; /* pppi - покажчик на 'покажчик на 'покажчик на змінну ppi' */

Для звертання до значень за допомогою покажчиків можна прийняти наступне правило, що жорстко зв'язує форму звертання з оголошенням цих покажчиків:

• повна кількість зірочок непрямої адресації, рівна кількості зірочок при оголошенні покажчика, визначає значення змінної;

• зменшення кількості зірочок непрямої адресації додає до імені змінної слово "покажчик", причому цих слів може бути стільки, скільки може бути рівнів непрямої адресації для цих імен покажчиків, тобто стільки, скільки зірочок стоїть в оголошенні покажчика.

Наприклад, після оголошення:

int i, *pi=&i;

звертання у виді:

*pi - визначає значення змінної,

pi - покажчик на змінну i.

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

pi, ppi, pppi - 0-й рівень адресації, пряма адресація;

*pi, *ppi, *pppi - 1-й рівень непрямої адресації

**ppi, **pppi - 2-й рівень непрямої адресації

***pppi - 3-й рівень непрямої адресації

Таким чином, до покажчиків 1-го і вище рівнів непрямої адресації можливі звертання і з меншою кількістю зірочок непрямої адресації, аніж задано при оголошенні покажчика. Ці звертання визначають адреси, тобто значення покажчиків визначеного рівня адресації. Відповідність між кількістю зірочок при звертанні за допомогою покажчика і призначенням звертання за покажчиком для наведеного прикладу ілюструє таблиця 1.12 (де Р.н.а. - рівень непрямої адресації):

Таблиця 1.12. Відповідність між кількістю уточнень (*) і результатом звертання за допомогою покажчика

Звертання Результат звертання Р.н.а.
i значення змінної i  
*pi pi значення змінної, на яку вказує pi покажчик на змінну типу int, значення pi  
**ppi *ppi ppi значення змінної типу int покажчик на змінну типу int покажчик на "покажчик на змінну типу int', значення покажчика ppi  
***pppi **pppi *pppi pppi значення змінної типу int; покажчик на змінну типу int покажчик на 'покажчик на змінну типу int' покажчик на 'покажчик на 'покажчик на змінну типу int', значення покажчика pppi  

1.8.5 Операції над покажчиками

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

1. присвоїти покажчику значення адреси даних, або нуль;

2. збільшити (зменшити) значення покажчика;

3. додати або відняти від значення покажчика ціле число;

4. скласти або відняти значення одного покажчика від іншого;

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

Змінній-покажчику можна надати певне значення за допомогою одного із способів:

1. присвоїти покажчику адресу змінної, що має місце в ОП, або нуль, наприклад:

pi = &j;

pi = NULL;

2. оголосити покажчик поза функцією (у тому числі поза main ()) або у будь-якій функції, додавши до нього його інструкцію static; при цьому початковим значенням покажчика є нульова адреса (NULL);

3. присвоїти покажчику значення іншого покажчика, що до цього моменту вже має визначене значення; наприклад:

pi = pj; це - подвійна вказівка однієї і тієї ж змінної;

4. присвоїти змінній-покажчику значення за допомогою функцій calloc () або malloc () - функцій динамічного виділення ОП.

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

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

Наприклад, значення типу int займає 2 байти, а типу float - 4 байти. Додавання одиниці до покажчика додасть "квант пам'яті", тобто кількість байтів, що займає одне значення типу, що адресується. Для покажчика на елементи масиву це означає, що здійснюється перехід до адреси наступного елемента масиву, а не до наступного байта. Тобто значення покажчика при переході від елемента до елемента масиву цілих значень буде збільшуватися на 2, а типу float - на 4 байти. Результат обчислення покажчиків визначений у мові Сі як значення типу int.

Приклад програми зміни значення покажчика на 1 квант пам'яті за допомогою операції "++" і визначення результату обчислення покажчиків даний на такому прикладі:

#include<stdio.h>

void main ()

{

int a[] = { 100, 200, 300 };

int *ptr1, *ptr2;

ptr1=a; /*- ptrl одержує значення адреси а[0] */

ptr2 = &а[2]; /*- ptr2 одержує значення адреси а[2] */

ptr1++; /* збільшення значення ptrl на квант ОП:

ptr1 = &а[1]*/

ptr2++; /* збільшення значення ptr2 на квант ОП:

ptr2 = &а[3]*/

printf (" ptr2 - ptr1 = %d\n", ptr2 - ptr1);

}

Результат виконання програми:

ptr2 - ptr1 = 2

Результат 2 виконання операції віднімання визначає 2 кванти ОП для значень типу int:

ptr2 - ptr1 = &а[3] - &а[1] = (а + 3) - (а + 1) = 2;

У наступному Сі-фрагменті продемонстрований приклад програми для виведення значень номерів (індексів) елементів масивів, адрес першого байта ОП для їх розміщення та значень елементів масивів. Справа в тому, що в Сі є дуже важлива властивість - ім'я масиву еквівалентно адресу його нульового елемента: х == &х[0]. Покажчики pi і pf спочатку містять значення адрес нульових елементів масивів, а при виведенні складаються з i-номером елемента масиву, визначаючи адресу i-елемента масиву. Для одержання адрес елементів масивів у програмі використовується додавання покажчиків-констант х та у, та змінних-покажчиків pi і pf з цілим значенням змінної i. Зміна адрес у програмі дорівнює кванту ОП для даних відповідного типу: для цілих - 2 байти, для дійсних - 4 байти.

#include<stdio.h>

void main()

{

int x[4], *pi = х, i;

float y[4], *pf = y;

printf("\nномер елемента адреси елементів масивів:\n i pi+i х + i &x[i] pf+i у+i &y[i]\n");

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

printf(" %d: %6u %6u %6u %6u %6u %6u\n", i, pi + i, x + i, &x[i], pf + i, y + i, &y[i]);

}

Результати виконання програми:

номер елемента адреси елементів масивів:

i pi+i х+i &x[i] pf+i y+i &y[i]
0:            
1:            
2:            
3:            

Мовою Сі можна визначити адреси нульового елемента масиву х як х або &х[0]: х == &х[0]. Краще і стисло використовувати просто х - це базова адреса масиву. Ту саму адресу елемента масиву можна представити у вигляді: х + 2 == &х[2]; х + i == &x[i].

Те саме значення можна представити у вигляді:

*(х + 0) == *х == х[0] - значення нульового елемента масиву х;

*(х + 2) == x[2] - значення другого елемента масиву х;

*(х + i) == x[i] - значення i-го елемента масиву х.

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

*х + 2== х[0] +2; *(х + i) - 3 == x[i] - 3;

1.8.6 Проблеми, пов'язані з покажчиками

Проблеми, пов'язані з покажчиками, виникають при некоректному використанні покажчиків. Усі застереження щодо некоректного використання покажчиків відносяться до мови Сі так само, як і до багатьох інших низькорівневих мов програмування. Некоректним використанням покажчиків може бути:

• спроба працювати з неініціалізованим покажчиком, тобто з покажчиком, що не містить адреси ОП, що виділена змінній;

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

• незвільнення ОП, що виділена за допомогою функції malloc ();

• спроба повернути як результат роботи функції адресу локальної змінної класу auto (про функції та класи змінних йтиметься далі);

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

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

static int *pi, *pj; /* pi = NULL; pj= NULL; */

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

int *х; /* змінній-покажчику 'х' виділена ОП, але 'х' не містить значення адреси ОП для змінної */

*х = 123; /* - груба помилка! */

Таке присвоювання помилкове, тому що змінна-покажчик х не має значення адреси, за яким має бути розташоване значення змінної.

Компілятор видасть попередження:

Warning: Possible use of 'x' before definition

При цьому випадкове (непроініціалізоване) значення покажчика (сміття) може бути неприпустимим адресним значенням! Наприклад, воно може збігатися з адресами розміщення програми або даних користувача, або даних операційної системи. Запис цілого числа 123 за такою адресою може порушити працездатність програми користувача або самої OC. Компілятор не виявляє цю помилку, це повинен робити програміст!

Виправити ситуацію можна за допомогою функції malloc (). Форма звертання до функції malloc () наступна:

ім'я-покажчика = (тип-покажчика) malloc (об'єм -ОП);

де ім'я-покажчика - ім'я змінної-покажчика, тип-покажчика - тип значення, що повертається функцією malloc;

об'єм-ОП - кількість байтів ОП, що виділяються змінній, яка адресується.

Наприклад:

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

При цьому з купи виділяється 2 байти ОП для цілого значення, а отримана адреса його розміщення заноситься в змінну-покажчик х. Значення покажчика гарантовано не збігається з адресами, що використовуються іншими програмами, у тому числі програмами OС. Параметр функції malloc визначає об'єм ОП для цілого значення за допомогою функції sizeof(int). Запис (int *) означає, що адреса, що повертається функцією malloc (), буде розглядатися як покажчик на змінну цілого типу. Це операція приведення типів.

Таким чином, помилки не буде у випадку використання наступних операторів:

int *х; /* х - ім'я покажчика, він одержав ОП */

х = (int *) malloc (sizeof(int)); /* Виділена ОП цілому значенню, на яке вказує 'x' */

*х = 123; /* змінна, на яку вказує 'х', одержала значення 123*/

Повернення (звільнення) ОП у купі виконує функція free (). Її аргументом є ім'я покажчика, що посилається на пам'ять, що звільняється. Наприклад:

free (x);

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

Одна з можливих помилок - подвійна вказівка на дані, розташовані у купі, і зменшення об'єму доступної ОП через незвільнення отриманої ОП. Це може бути для будь-якого типу даних, у тому числі для скаляра або масиву. Розглянемо випадок для скаляра.

Приклад фрагмента програми з подвійною вказівкою і зменшенням об'єму доступної ОП через незвільнення ОП наведений нижче:

#include<alloc.h>

void main ()

{

/* Виділення ОП динамічним змінним х, у и z: */

int *х = (int *) malloc (sizeof(int)),

*у = (int *) malloc (sizeof(int)),

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

/* Ініціалізація значення покажчиків х, у, z;*/

*х = 14; *у = 15; *z = 17;

/*Динамічні змінні одержали конкретні цілі значення*/

y=x; /* груба помилка - втрата покажчика на динамічну змінну в без попереднього звільнення її ОП */

}

У наведеному вище прикладі немає оголошення імен змінних, є тільки покажчики на ці змінні. Після виконання оператора y = х; х та у є двома покажчиками на ту саму ОП змінної *х. Тобто *х = 14; і *у = 14. Крім того, 2 байти, виділені змінній, яку адресував y для розміщення цілого значення (*у), стають недоступними (загублені), тому що значення y, його адреса, замінені значенням х. А в купі ці 2 байти для *у вважаються зайнятими, тобто розмір купи зменшений на 2 байти. Відбулося зменшення доступної ОП. Цього слід уникати.

Щоб уникнути такої помилки треба попередньо звільнити ОП, виділену змінній *у, а потім виконати присвоювання значення змінній у. Наприклад:

free (у); /* звільнення ОП, виділеної змінної '*у' */

у = х; /* присвоювання нового значення змінній 'у' */

Чи можна змінній-покажчику присвоїти значення адреси в операторі оголошення? Наприклад:

int *x = 12345;

Тут константа 12345 цілого типу, а значенням покажчика х може бути тільки адресою, покажчиком на байт в ОП. Тому компілятор при цьому видасть повідомлення про помилку:

Error PR.CPP 3: Cannot convert 'int to 'int *'

Проте не викличе помилки наступне присвоювання:

int a[5], *х = а;

Використання покажчиків часто пов'язано з використанням масивів різних типів. Кожний з типів даних масивів має свої особливості. Тому далі розглянемо властивості покажчиків для роботи з масивами.

Масиви

1.9.1 Основні поняття

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

Як показує практика роботи на Сі, покажчики рідко використовуються зі скалярними змінними, а частіше - з масивами. Покажчики дають можливість застосовувати адреси приблизно так, як це робить ЕОМ на машинному рівні. Це дозволяє ефективно організувати роботу з масивами. Будь-яку серйозну програму, що використовує масиви, можна написати за допомогою покажчиків.

Для роботи з масивом необхідно:

1. визначити ім'я масиву, його розмірність (кількість вимірів) і розмір - кількість елементів масиву;

2. виділити ОП для його розміщення.

У мові Сі можна використовувати масиви даних будь-якого типу:

• статичні: з виділенням ОП до початку виконання функції; ОП виділяється в стеку або в ОП для статичних даних;

• динамічні: ОП виділяється з купи в процесі виконання програми, за допомогою функцій malloc() і calloc().

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

Розмір масиву визначається:

1. для статичних масивів при його оголошенні; ОП виділяється до початку виконання програми; ім'я масиву - покажчик-константа; кількість елементів масиву визначається:

a. явно; наприклад: int а[5];

b. неявно, при ініціалізації елементів масиву; наприклад:

int а[] = { 1, 2, 3 };

2. для динамічних масивів у процесі виконання програми; ОП для них запитується і виділяється динамічно, з купи; ім'я покажчика на масив - це змінна; масиви ці можуть бути:

a. одновимірні і багатовимірні; при цьому визначається кількість елементів усього масиву й ОП запитується для всього масиву;

b. вільні (спеціальні двовимірні); при цьому визначається кількість рядків і кількість елементів кожного рядка, і ОП запитується і виділяється для елементів кожного рядка масиву в процесі виконання програми; при використанні вільних масивів використовують масиви покажчиків;

Розмір масиву можна не вказувати. В цьому разі необхідно вказати порожні квадратні дужки:

1. якщо при оголошенні ініціалізується значення його елементів; наприклад:

static int а[] = {1, 2, 3};

char b[] = "Відповідь:";

2. для масивів - формальних параметрів функцій; наприклад:

int fun1(int a[], int n);

int fun2(int b[k][m][n]);

3. при посиланні на раніше оголошений зовнішній масив; наприклад:

int а[5]; /* оголошення зовнішнього масиву */

main ()

{

extern int а[]; /*посилання на зовнішній масив */

}

В усіх оголошеннях масиву ім'я масиву - це покажчик-константа! Для формування динамічного масиву може використовуватися тільки ім'я покажчика на масив - це покажчик-змінна. Наприклад:

int *m1 = (int *) malloc (100 * sizeof (int));

float *m2 = (float *) malloc (200 * sizeof (float));

де m1 - змінна-покажчик на масив 100 значень типу int;

m2 - змінна-покажчик на масив 200 значень типу float.

Звільнення виділеної ОП відбувається за допомогою функції:

free (покажчик-змінна);

Наприклад:

free(ml);

free(m2);

Звертання до елементів масивів m1 і m2 може виглядати так:

m1[i], m2[j].

Пересилання масивів у Сі немає. Але можна переслати масиви поелементно або сумістити масиви в ОП, давши їм практично те саме ім'я.

Наприклад:

int *m1 = (int *) malloc(100 * sizeof(int));

int *m2 = (int *) malloc(100 * sizeof(int));

Для пересилання елементів одного масиву в іншій можна використати оператор циклу:

for (i = 0; i < 100; i++) m2[i] = ml [i];

Замість m2[i] = m1 [i]; можна використовувати:

*m2++ = *ml++;

або: *(m2 + i) = *(ml + i);

За допомогою покажчиків можна сполучити обидва масиви й у такий спосіб:

free(m2);

m2 = ml;

Після цього обидва масиви займатимуть одну й ту саму область ОП, виділену для масиву m1. Однак це не завжди припустимо. Наприклад, коли масиви розташовані в різних типах ОП: один - у стеку, інший - у купі. Наприклад, у функції main () оголошені:

int *m1 = (int *) malloc(100* sizeof(int));

int m2[100];

У вищенаведеному прикладі m1 - пакажчик-змінна, і масив m1 розташований у купі, m2 - покажчик-константа, і масив m2 розташований у стеку. У цьому випадку помилковий оператор: m2 = m1; тому що m2 - це покажчик-константа. Але після free(m1) припустимим є оператор:

m1 = m2; /* оскільки m1 - покажчик-змінна */

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

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

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

1.9.2 Оголошення та звертання в одновимірних масивах

Форма оголошення одновимірного масиву з явною вказівкою кількості елементів масиву:

тип ім'я_масива [кількість-елементів-масива];

Звертання до елементів одновимірного масиву в загальному випадку можна представити індексуванням, тобто у вигляді

ім'я-масива [вираз];

де ім'я-масиву - покажчик-константа;

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

Елементи одновимірного масиву розташовуються в ОП підряд: нульовий, перший і т д. Приклад оголошення масиву:

int а[10];

іnt *p = а; /* - р одержує значення а */

При цьому компілятор виділяє масив в стеку ОП розміром (sizeof(Type) * розмір-масиву) байтів.

У вищенаведеному прикладі це 2 * 10 = 20 байтів. Причому а - покажчик-константа, адреса початку масиву, тобто його нульового елемента, р - змінна; змінній р можна присвоїти значення одним із способів:

р = а;

р = &а[0];

р = &a[i];

де &а[i] == (а + i) - адреса і-елемента масиву.

Відповідно до правил перетворення типів значення адреси i-елемента масиву на машинному рівні формується таким чином:

&а[i]= а + i * sizeof(int);

Справедливі також наступні співвідношення:

&a == a+0 == &a[0] - адреса а[0] - нульового елемента масиву;

а+2 == &а[2] - адреса а[2] - другого елементи масиву;

а+i == &a[i] - адреса a[i] - i-гo елемента масиву;

*а==*(а+0)==*(&а[0])==a[0] - значення 0-ого елемента масиву;

*(а + 2) == а[2] - значення а[2] - другого елементи масиву;

*(а + i) == а[i] - значення a[i] - i-гo елемента масиву;

*а + 2 == а[0] + 2 - сума значень а[0] і 2.

Якщо р - покажчик на елементи такого ж типу, які і елементи масиву a та p=а, то а та р взаємозамінні; при цьому:

p == &a[0] == a + 0;

p+2 == &a[2] == a + 2;

*(p + 2) == (&a[2]) == a[2] == p[2];

*(p + i) == (&a[i]) == a[i] == p[i];

Для a та p еквівалентні всі звертання до елементів a у вигляді:

a[i], *(a+i), *(i+a), i[a], та

p[i], *(p+i), *(i+p), i[p]

1.9.3 Оголошення та звертання до багатовимірних масивів

У даному розділі розглянемо оголошення і зв'язок покажчиків і елементів багатомірних масивів - що мають 2 та більше вимірів.

Багатомірний масив у мові Сі розглядається як сукупність масивів меншої розмірності. Наприклад, двовимірний масив - це сукупність одновимірних масивів (його рядків), тривимірний масив - це сукупність матриць, матриці - сукупності рядків, а рядок - сукупність елементів одновимірного масиву.

Елементи масивів розташовуються в ОП таким чином, що швидше змінюються самі праві індекси, тобто елементи одновимірного масиву розташовуються підряд, двовимірного - по рядках, тривимірного - по матрицях, а матриці - по рядках.

Для звертання до елементів багатомірного масиву можна використовувати нуль і більш індексів (індексних виразів):

ім'я-масиву [вираз1][вираз2]...

Наприклад, для звертання:

• до одновимірного масиву можна використовувати одно-індексний вираз (індекс);

• до двовимірного - 1 або 2 індексний вираз;

• до тривимірного - 1, 2 або 3 індексний вираз і т.д.

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

Приклад оголошення двовимірного масиву значень типу int:

int а[m][n];

Цей масив складається з m одновимірних масивів (рядків), у кожному з яких утримується n елементів (стовпців). При роботі з цим двовимірним масивом можна використовувати одно або 2 індексний вираз. Наприклад:

а[i][j]- містить 2 індекси; використовується для звертання до елемента i -рядка, j -стовпця масиву; обчислюються індексні вирази, визначається адреса елемента масиву і вилучається його значення;

a[i] - містить 1 індекс; визначає адресу одновимірного масиву: адреса початку i-рядка масиву;

а - не містить індексу і визначає адресу масиву, його нульового елемента.

Таким чином, звертання до двовимірних масивів за допомогою імені і тільки одного індексу визначає покажчик на початок відповідного рядка масиву (адреса його нульового елемента). Наприклад:

а[0] == &a[0][0] == a+0*n*sizeof(int);

а[1] == &а[1][0] == a+1*n*sizeof(int);

a[i] == &a[i][0] == a+i*n*sizeof(int);

Приклад оголошення тривимірного масиву:

int а[k][m][n];

де:

• k- кількість матриць з m рядками і n стовпцями;

• m - кількість рядків (одновимірних масивів) у матриці;

• n - кількість стовпців (елементів у рядку) матриці.

Цей масив складається з k матриць, кожна з яких складається з m одновимірних масивів (рядків) по n елементів (стовпців). При звертанні до цього масиву можна використовувати імена:

a[l][i][j] - містить 3 індекси; використовується для звертання до елемента l -матриці, i -рядка. j -стовпця масиву; обчислюються індексні вирази, визначається адреса елемента масиву і вилучається його значення;

a[k][i] - визначає одновимірний масив - адреса початку i -рядка; k - матриці;

a[k] - визначає двовимірний масив - адреса початку k - матриці, тобто нульового елемента його нульового рядка;

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

Наприклад:

int b[3][4][5];

int i, *ip, *ipp;

i = b[0][0][1];

ip = b[2][0];

ipp = b[2];

де: ip, ipp - покажчики на значення типу int.

Після ip = b[2][0]; ip є покажчиком на елемент 0-рядка 0-го стовпця 2-й матриці масиву, тобто b[2][0][0].

Після ipp = b[2]; ipp адресує 0-й рядок 2-ї матриці масиву, тобто містить адреса b[2][0][0].

Звертання до елементів багатомірного масиву більш детально розглянемо на прикладі двовимірного масиву. Наприклад:

int а[3][4]; /* а - покажчик-константа */

int *р = а; /* р - покажчик-змінна */

Після цього покажчик р можна використовувати замість покажчика а для звертання до рядків або елементів масиву а у вигляді: ім'я покажчика і зсув елемента щодо адреси початку масиву а.

В ОП елементи масиву а розташовуються таким чином, що швидше всіх змінюється самий правий індекс, тобто в послідовності:

а[0][0] а[0][1] а[0][2] а[0][3] а[1][0]... а[2][2] а[2][3].

При цьому для звертання до масиву а можна використовувати імена:

&a == а == &а[0][0] == *а - адреса а[0][0] - елемента 0-ого рядка 0-ого стовпця масиву а;

**а == *(&а[0][0]) == а[0][0] - значення елемента нульового рядка нульового стовпця масиву а;

a[i] == (а + i) == *(а + i) == &а[i][0] - адреса елемента i -рядка 0-стовпця;

*a[i] == **(а + i) == *(&а[i]) == a[i][0] - значення 0-го елемента i -рядка;

a[i][j] == *(*(а + i) + j) == *(a[i] + j) == a[i][j] - значення елемента i -рядка j -стовпця масиву а;

де:

(а + i) == *(а + i) == a[i] - адреса 0-го елемента i -рядка == &a[i][0];

(*(а + i) + j)- адреса j -елемента i -рядка = &a[i][j];

*(*(а + i) + j)- значення j -елемента i-рядка = a[i][j].

Значення адреси початку i -рядка (адреси 0-елемента i -рядка) на машинному рівні формується у виді:

a[i] = а + i == (a+i*n*sizeof(int)), де n - кількість значень в одному рядку.

Таким чином, адреса (i+1)-рядка відстоїть від i -рядка на (n*sizeof(int)) байтів, тобто на відстань одного рядка масиву.

Вираз a[i][j] компілятор Сі переводить в еквівалентний вираз:

*(*а + i) + j). Зрозуміло, запис a[i][j] більш традиційний у математиці і більш наочний.

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

int а[m][n], *р = а;

*(p+i*n+j) - значення j - елемента i-рядка;

де: n - кількість елементів у рядку;

i*n + j - змішання а[i][j]- елемента відносно початку масиву а.

1.10 Масиви покажчиків

За допомогою масивів покажчиків можна формувати великі масиви і вільні масиви - колекції масивів будь-яких типів.





Дата публикования: 2015-01-23; Прочитано: 603 | Нарушение авторского права страницы | Мы поможем в написании вашей работы!



studopedia.org - Студопедия.Орг - 2014-2024 год. Студопедия не является автором материалов, которые размещены. Но предоставляет возможность бесплатного использования (0.094 с)...