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

Пособие

.pdf
Скачиваний:
66
Добавлен:
22.03.2015
Размер:
1.35 Mб
Скачать

9ПОКАЖЧИКИ

9.1Загальна інформація про покажчики

Коли компілятор опрацьовує оператор визначення змінної, наприклад, int i=10, він виділяє пам’ять відповідно до типу int і ініціалізує її вказаним значенням 10. Усі звернення в програмі до змінної по її імені i замінюються компілятором на адресу області пам’яті, в якій зберігається значення змінної. Програміст може визначити власні змінні для зберігання адрес областей пам’яті. Такі змінні називаються покажчиками. Покажчики призначені для зберігання адрес областей пам’яті.

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

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

тип * ім’я;

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

Зірочка відноситься безпосередньо до імені, тому для того, щоб оголосити кілька покажчиків, потрібно ставити її перед ім’ям кожного з них. Наприклад, в операторі:

int *d, *h, с;

описуються два покажчика на ціле з іменами d і h, а також ціла змінна c.

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

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

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

71

int l; // ціла змінна

const int ci = 1; // ціла константа int *pi; // покажчик на цілу змінну

const int *pci; // покажчик на цілу константу int *const ср = &i; // покажчик-константа на

цілу змінну

const int *const СРС = &ci; // покажчик-

константа на цілу константу

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

Для ініціалізації покажчиків використана операція отримання адреси &.

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

9.2Ініціалізація покажчиків

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

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

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

Існують наступні способи ініціалізації покажчика: 1. Присвоєння вказівником адреси існуючого об’єкта:

– за допомогою операції одержання адреси:

int а=5; //ціла змінна

int *р=&а; //в покажчик записується адреса а int *р(&а): //те ж саме іншим способом

72

за допомогою значення іншого ініціалізувати покажчик: int *r = р;

за допомогою імені масиву або функції, що трактуються як ад-

реса:

int *t = b; // присвоювання адреси початку масиву

void f (int а) {/ * ... * /} // Визначення фу-

нкції

void (*pf) (int); // покажчик на функцію pf = f; // присвоювання адреси функції

2. Присвоєння вказівником адреси області пам’яті в явному вигляді:

char *vp = (char *) 0хВ8000000;

Тут 0хВ8000000 – шістнадцяткова константа, (char *) – операція приведення типу: константа перетворюється до типу «вказів-

ник на char».

3. Присвоєння порожнього значення:

int *SUXX = NULL; int *rulez = 0;

У першому рядку використовується константа NULL, визначена в деяких заголовків файлах С як покажчик, рівний нулю. Рекомендується використовувати просто 0, так як це значення типу int буде правильно перетворено стандартними способами відповідно до контексту. Оскільки гарантується, що об’єктів з нульовою адресою немає, порожній покажчик можна використовувати для перевірки, чи посилається вказівник на конкретний об’єкт.

4. Виділення ділянки динамічної пам’яті і присвоювання її адреси вказівнику за допомогою функції malloc:

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

Функція malloc виконує виділення достатнього для розміщення величини типу int ділянки динамічної пам’яті і записує адресу початку цієї ділянки в змінну u. Пам’ять під саму змінну n (розміру, достатнього для розміщення покажчика) виділяється на етапі компіляції. В функцію передається один параметр – кількість виділеної

73

пам’яті в байтах. Конструкція int* використовується для приведення типу покажчика, що повертається функцією до необхідного типу. Якщо пам’ять виділити не вдалося, функція повертає 0.

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

<malloc.h>.

При цьому змінна-вказівник зберігається і може ініціалізуватись повторно.

Наведені вище динамічні змінні знищуються наступним чином: free (u);

9.3Операції з покажчиками

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

або непряме звернення до об’єкта (*), присвоєння, додавання з константою, віднімання, інкремент (+), декремент (-), порівняння, приведення типів. При роботі з покажчиками часто використовується операція отримання адреси (&).

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

char а; // змінна типу char

char *р=(char*)malloc(sizeof(char)); /*виділення пам’яті під покажчик і під динамі-

чну змінну типу char*/ *р='0'; а=*р;

// присвоювання значення обом змінним

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

# include <stdio.h>

74

#include <conio.h> int main ()

{

unsigned long int A = 0Xcc77ffaa; unsigned short int

*pint = (unsigned short int *) & A; unsigned char

*pchar = (unsigned char *) & A; printf(" | % x | % x | % x | ",

A, * pint, * pchar); _getch();

return 0;

}

виведе на екран рядок:

| cc77ffaa | ffaa | аа |

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

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

9.4 Арифметичні операції з покажчиками

Арифметичні операції з покажчиками (додавання з константою, віднімання, інкремент і декремент) автоматично розраховують розмір типу величин, що адресуються покажчиками.

Інкремент переміщає покажчик до наступного елементу масиву, декремент – до попереднього. Фактично значення покажчика змінюється на величину sizeof (тип). Якщо покажчик на певний тип збільшується або зменшується на константу, його значення змінюється на величину цієї константи, помноженої на розмір об’єкта даного типу, наприклад:

short *р = (short *)malloc(sizeof(short)*5);

р++; // значення р збільшується на 2

75

long *q = (long *)malloc(sizeof(long)*5); q++; // значення q збільшується на 4

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

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

*р++=10;

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

*р=10; р++;

Вираз (*р)++, навпаки, інкрементується значенням, на яке посилається покажчик.

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

9.5Посилання

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

Формат оголошення посилання:

тип &ім’я;

де тип – це тип величини, на яку вказує посилання, & – оператор посилання, означає, що наступне за ним ім’я є ім’ям змінної типу посилання, наприклад:

int коl;

int &pal = kol; // посилання pal - альтернати-

вне ім’я для коl

76

const char & CR = '\n': // посилання на конс-

танту

Запам’ятайте наступні правила:

після ініціалізації посилання не може бути присвоєна інша змінна;

тип посилання повинен збігатися з типом величини, на яку вона посилається;

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

9.6Динамічні масиви

Перевага динамічних масивів полягає в тому, що розмірність може бути змінною, тобто об’єм пам’яті, що виокремлюється під масив, визначається на етапі виконання програми [17]. Динамічні масиви неможна інціалізувати при створенні та вони не обнуляються. Доступ до елементів динамічного масиву здійснюється так само, як і до статичних, наприклад, до елементу з номером 5 можна звернутись як p[5] або *(p+5).

У мові С динамічні масиви створюються за допомогою функції malloc.

int n=100;

float *q=(float*)malloc(n*sizeof(float));

Наведемо спосіб виділення пам’яті під двомірний масив, коли обидві його розмірності задаються на етапі виконання програми:

int n,j,i;

float **matr; // 1

printf("Введіть розмір матриці n: "); scanf("%d",&n);

matr=(float **)malloc(sizeof(float *)*n); //виділяємо пам’ять під масив покажчиків if (matr==NULL)

{

puts("не створено динамічний масив!"); return 0;

}

for (i=0;i<n;i++) //2

{

77

//3

matr[i]=(float*)malloc(sizeof(float)*n); //виділяємо пам’ять під i-й рядок

}

if (matr[i]==NULL)

{

puts("не створено динамічний масив!"); return 0;

}

В операторі 1 оголошується змінна типу «покажчик на покажчик на float» і виділяється пам’ять під масив покажчиків на рядки масиву (кількість рядків – n). В операторі 2 організовується цикл для виділення пам’яті під кожен рядок масиву. В операторі 3 кожному елементу масиву покажчиків на рядка присвоюється адреса початку ділянки пам’яті, виділеної під рядок двовимірного масиву.

Кожен рядок складається з n елементів типу float.

 

 

 

 

float matr[n][n]

 

 

 

float *matr[n]

0

1

n-1

float **matr

matr

0

 

...

 

 

 

 

 

 

1

 

...

 

 

 

 

 

 

 

 

...

 

...

 

 

 

 

 

 

 

 

n-1

 

 

 

 

 

 

 

n

 

Рис. 9.1 Виділення пам’яті під двомірний масив

Звільнення пам’яті з-під масиву з довільною кількістю вимірів за допомогою операції free(matr).

78

10 ОСНОВНІ АЛГОРИТМИ СОРТУВАННЯ МАСИВІВ

Алгоритм сортування – це алгоритм для впорядкування елементів у списку. У випадку, коли елемент списку має декілька полів, поле, що є критерієм порядку, називають ключем сортування [4].

10.1 Метод бульбашкового сортування

Метод «бульбашкового сортування» ґрунтується на перестановці сусідніх елементів. Для впорядкування елементів масиву здійснюються повторні проходи по масиву.

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

Урезультаті першого проходу найбільше число буде поставлено

вкінець масиву. У другому проході такі операції виконуються над

елементами з першого до (N-1)-ого, у третьому – від першого до (N-2)-ого і т.д. Впорядкування масиву буде закінчено, якщо при проході масиву не виконається жодної перестановки елементів масиву. Факт перестановки фіксується за допомогою деякої змінної (у наступному прикладі – is), що на початку має значення 0 і набуває значення 1 тоді, коли виконається перестановка в якій-небудь парі

(табл. 10.1).

Таблиця 10.1.

Приклад сортування масиву методом «бульбашкового сортування»

Масив до впо-

22

20

-1

-40

88

-75

-22

рядкування

 

 

 

 

 

 

 

І крок

20

-1

-40

22

-75

-22

88

ІІ крок

-1

-40

20

-75

-22

22

88

ІІІ крок

-40

-1

-75

-22

20

22

88

ІV крок

-40

-75

-22

-1

20

22

88

V крок

-75

-40

-22

-1

20

22

88

Реалізувати сортування масиву методом «бульбашкового сортування» можна так:

const n=10;

int a[n], i, c, is; /* … */

do {

79

is=0;

for (i=1;i<n;i++) if (a[i-1]>a[i])

{

c=a[i]; a[i]=a[i-1]; a[i-1]=c; is=1;

}

}while (is);

10.2 Сортування методом вибору

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

Даний процес виконується до тих пір, поки не буде поставлено на місце N-1 елемент (табл. 10.2).

Таблиця 10.2.

Приклад сортування масиву методом вибору

Масив до впоряд-

22

20

-1

-40

88

-75

-22

кування

 

 

 

 

 

 

 

І крок

-75

20

-1

-40

88

22

-22

ІІ крок

-75

-40

-1

20

88

22

-22

ІІІ крок

-75

-40

-22ё

20

88

22

-1

ІV крок

-75

-40

-22

-1

88

22

20

V крок

-75

-40

-22

-1

20

22

88

VІ крок

-75

-40

-22

-1

20

22

88

Реалізувати сортування масиву методом вибору можна так:

const int n=20; int b[n];

int imin, i, j, a; /* … */

for (i=0;i<n-1;i++)

{

imin=i;

for (j=i+1;j<n;j++)

if (b[j]<b[imin]) imin=j;

80