
Зубенко, Омельчук - Програмування. Поглиблений курс
.pdf
Розділ ІІІ. МОВИ ПРОГРАМУВАННЯ С ТА С++
даного підрозділу. Вона створює змінну для забезпечення доступу до об'єкта, на який посилається покажчик. У наведеному вище прикладі операція розіменування повертає змінну з ім'ям *p, яка є синонімом змінної n. Звернення n та *p до цілого об'єкта за адресою 255 у всіх припустимих контекстах є еквівалентними (доти, доки покажчик зберігатиме значення 255).
Приклад 3.40. Операції взяття адреси й розіменування:
Вираз |
Значення |
|
p=NULL; |
Обнулення покажчика |
|
p= =NULL |
Порівняння p із нулем після обнулення поверне 1 |
|
!p |
Заперечення p після обнуління поверне 1 |
|
*p<0; |
Порівняння n із нулем |
|
printf("%d\n", *p); |
Виведення значення n ( =150) |
|
printf("%p\n", p); |
Виведення значення покажчика ( =0x00FF ) |
|
*p=0; |
Обнулення змінної n |
|
n=*p+2; |
Збільшення n на 2 |
■
Покажчик можна зводити до іншого типу. Перетворення здійсню- ються за участю безтипових покажчиків і без них. У мові С допуска- ється присвоювання значень безтипового покажчика покажчику до- вільного типу (і навпаки) без явного перетворення типу. До безтипо- вих покажчиків звертаються, якщо тип об'єкта невідомий. Вони до- зволяють посилатися на довільну ділянку пам'яті, незалежно від роз- міщених там об'єктів. Їхнє використання як параметрів функції до- зволяє передавати у функцію покажчик на об'єкт довільного типу.
Перетворення інших типів покажчиків мають бути завжди явними (тобто за допомогою операції зведення типів). Однак варто врахову- вати, що перетворення одного типу покажчика до іншого може ви- кликати непередбачувану поведінку програми.
Приклад 3.41. Некоректне перетворення:
double d=10.5; int *ip;
ip=(int*) &d; /*синтаксично вірне перетворення, але призводить до втрати інформації*/ ■
Розмір покажчиків часто не збігається з розмірами типів int і long. У С99 тип intptr_t є цілочисловим і достатнім для збереження пока- жчика на об'єкт. Покажчики на функції й дані можуть мати суттєво різні подання й розміри.
341

ПРОГРАМУВАННЯ
Адресна арифметика. Над покажчиками можна виконувати унарні операції збільшення ++ і зменшення -- у префіксному й постфіксному варіантах. При цьому значення покажчика після цих операцій збільшу- ється або зменшується на довжину об'єкта, зв'язаного з покажчиком. Не- хай Val(е) – значення виразу е. Тоді для покажчика р типу Т:
Val(p++)=Val(p--)=Val(p), Val(++p)=Val(p)+sizeof(Т), Val(--p)=Val(--p)= Val(p)-sizeof(Т).
У бінарних операціях додавання й віднімання можуть брати участь покажчик і ціле. При цьому результатом операції буде знов покажчик того самого типу: p ±i = p ±(i * sizeof(T)).
В операції віднімання можуть брати участь два покажчики одного типу. Результат такої операції має цілий тип і дорівнює кількості об'- єктів між зменшуваним і від'ємником. Результат буде від'ємним, як- що перша адреса менша ніж друга.
Два покажчики одного типу можна порівнювати в операціях ==, !=, <, <=, >, >=. Значення покажчиків розглядаються як цілі числа.
Приклад 3.42. Адресна арифметика:
іnt i=3;
іnt *ptr, *ptr1, *ptr2, v[20];
ptr1=ptr=&v[4]; |
/*=&v[4]*/ |
Зн(ptr++) |
/*=&v[4]*/ |
Зн(ptr--) |
/*=&v[4]*/ |
Зн(--ptr) |
/*=&v[3]*/ |
ptr2=ptr1+(і+4); |
/*=&v[11]*/ |
ptr1=ptr2-і; |
/*=&v[8]*/ |
i=ptr1-ptr2; |
/*=-3*/ |
i=ptr2-ptr1; |
/*=3*/ |
(prt1>=ptr2)?1:2 |
/*=2*/ |
■
3.5.3. ІНДЕКСАЦІЯ ПОКАЖЧИКІВ І МАСИВИ
Операція індексації покажчику р типу Т і цілому і ставить у відпо- відність змінну з індексом p[i] = *(p +i) . Якщо пригадати, що ім'я ма-
сиву є покажчиком (посилається на перший його компонент з індек- сом 0), то стане зрозумілим, що змінні з індексом є не чим іншим, як результатом індексації імені масиву відповідними індексами.
Отже, існують два методи звернення до компонентів масиву: змін- ні з індексами та з використанням адресної арифметики. Останній є
342

Розділ ІІІ. МОВИ ПРОГРАМУВАННЯ С ТА С++
більш універсальним і саме він часто використовується у функціях обробки символьних масивів-рядків.
У наступному прикладі наведені різні версії стандартної функції strlen для підрахунку довжини рядка з використанням адресної ари- фметики та індексації масиву.
Приклад 3.43. Функція strlen:
//strlen: версія 1 int strlen (char *s) {int i;
for (i=0; *s!='\0'; s++) i++; return i;
}
//strlen: версія 2 int strlen (char *s) {int i;
for (i=0; *s; s++) i++; /*вираз *s трактується як булевий*/ return i;
}
//strlen: версія 3 int strlen (char s[]) {int i;
for (i=0; s[i]!='\0'; i++); return i;
}
■
У табл. 3.12 наведені деякі стандартні функції для роботи з рядка- ми. Їхні прототипи містяться в бібліотеці <string.h>.
Таблиця 3.12. Стандартні функції для роботи з рядками
Функція |
Прототип |
|
Семантика |
strcpy |
char *strcpy(char |
Копіювання s2 у s1 |
|
*s1,const char *s2); |
|||
strcat |
char *strcat(char |
Конкатенація s1 та s2 |
|
*s1,const char *s2); |
|||
strlen(s1) |
long strlen(char *s1); |
Повертає довжину рядка s1 |
|
|
int strcmp(const char |
Повертає 0, якщо s1 =s2, |
|
|
|||
strcmp(s1,s2) |
від'ємне значення, якщо |
||
*s1,const char *s2); |
s1 |
<s2, і додатне, якщо |
|
|
|
s1 |
>s2 |
343

ПРОГРАМУВАННЯ |
Закінчення табл. 3.12. |
Функція Прототип
strchr(s1,ch) char *strchr(const char *s1, int c);
strstr(s1,s2) int strstr(const char s1,const char *s2);
strtok(s1,s2) char* strtok(char s1, const char *s2);
Семантика
Повертає покажчик на перше входження симво- лу c у рядок s1
Повертає покажчик на перше входження рядка s2 у рядок s1
Повертає покажчик на першу лексему в рядку s1, обмежену символами рядка s2. Щоб знайти другу й наступні лексеми в рядку s1, необхідно викли-
кати strtok(Null,s2);
Така програма ілюструє роботу наведених функцій:
int main(void)
{
char word[80],word_1[80]; gets(word); gets(word_1);
printf("Length: %d %d\n", strlen(word),strlen(word_1)); if(!strcmp(word,word_1)) printf("Strings is the same\n"); strcat(word,word_1);
printf("%s\n",word);
strcpy(word,"Checking.\n");
printf(word);
if(strchr("Hello", 'h')) printf("Hello contain h\n"); if(strstr("Hello", 'he')) printf("Hello contain he"); printf(strtok("Hello contain\the"," \n\t");/*виведе Hello*/
printf(strtok(null, " \n\t"); /*виведе contain*/ printf(strtok(null, " \n\t"); /*виведе he*/
return 0;
}
3.5.4. МАСИВИ ПОКАЖЧИКІВ
Як і об'єкти будь-яких інших типів, покажчики можуть бути зібрані в масив. Нижченаведені оператори описують масиви з покажчиками на об'єкти типу іnt та float:
int |
var; |
/*звичайнa ціла змінна*/ |
int |
array[10]; |
/*звичайний масив*/ |
344

|
|
Розділ ІІІ. МОВИ ПРОГРАМУВАННЯ С ТА С++ |
|
int *v[20]; |
/*масив |
покажчиків*/ |
|
int *pd[]={&array[0],&array[2],&array[4]}/*ініціалізований |
|||
v[2]=&var; |
масив покажчиків*/ |
|
|
/*присвоює |
адресу цілої змінної var третьому |
||
|
елементу масиву |
v*/ |
|
float *w[20]; /*масив |
дійсних |
покажчиків*/ |
При передаванні масиву покажчиків у функцію передаються його ім'я й довжина. Наприклад, щоб вивести вектор дійсних чисел, який подається масивом покажчиків, можна застосувати функцію
voіd printArray(int n, float *q[])/*q – масив довжиною n дійс-
них покажчиків*/
{
for(int i=0; i<n; i++) prіntf("%f", *q[i]);
}
Виклик printArray(20, w); виведе значення, що подає масив по- кажчиків w.
Є задачі, в яких необхідно мати справу відразу з кількома різними порядками елементів масиву (напр., задачі пошуку інформації в таб- лицях тощо), а зберігати одночасно кілька впорядкованих по-різному копій масиву обтяжливо або взагалі неможливо. Універсальним вихо- дом із такої ситуації є зберігання не впорядкованих копій масиву, а тільки масивів покажчиків (індексів) елементів, які відповідають роз- ташуванню елементів масиву в тому чи іншому порядку.
Приклад 3.44. Одночасне сортування числового масиву за зрос- танням і спаданням:
#define n 6 int main()
{
float array[n]={5.0, 2.0, 1.0, 3.0, 6.0, 4.0};
float *pmin[n], /*масив pmin[n]: буде задавати порядок у масиві array за зростанням*/
*pmax[n], /*масив pmах[n]: буде задавати порядок у масиві array за спаданням*/
*temp;
int i, j;
for (i=0; i<n; i++) pmin[i]=pmax[i]=&array[i]; for (i=0; i<n-1; i++)
for (j=i+1; j<n; j++)
{
if (*pmin[i]<*pmin[j])
345

ПРОГРАМУВАННЯ
{
temp=pmin[i];
pmin[i]=pmin[j];
pmin[j]=temp;
}
if (*pmax[i]>*pmax[j])
{
temp=pmax[i];
pmax[i]=pmax[j];
pmax[j]=temp;
}
}
printf("\n за спаданням: \n"); printArray(n, pmin);
printf("\n за зростанням: \n"); printArray (n, pmax);
return 0;
}
■
Масиви покажчиків використовуються при роботі з рядками. Останні зазвичай ініціалізуються літералами – константами типу си- мвольних покажчиків. Наприклад, така функція виводить повідом- лення про помилку з номером num:
voіd syntaxError(іnt num)
{
statіc char *err[]={
"Не можна відкрити файл\n", "Помилка при читанні\n", "Помилка при записі\n",
};
prіntf("%s", err[num]);
}
3.5.5. ФУНКЦІЇ ВИДІЛЕННЯ ПАМ'ЯТІ
Прототипи функцій для роботи з пам'яттю містяться в бібліотеці <stdlib.h>. Для виділення пам'яті служать функції void
*malloc(size_t n), void *calloc(size_t n, size_t m) та void *realloc(void* ptr, size_t n), де size_t – цілий тип, розмір якого залежить від компілятора. Перша виділяє в купі поле пам'яті, достат- нє для розміщення об'єкта довжиною n байтів, і повертає його безти-
346

Розділ ІІІ. МОВИ ПРОГРАМУВАННЯ С ТА С++
пову адресу, яка може бути явно перетворена до будь-якого потрібно- го типу з урахуванням вирівнювання. Якщо в купі недостатньо місця, то функція повертає значення NULL. Якщо параметр n = 0 , то функція повертає значення NULL або інший безтиповий покажчик.
Друга функція calloc виділяє в купі поле пам'яті, достатнє для ро- зміщення масиву довжиною n із компонентами довжиною m байтів, і повертає його безтипову адресу, яка може бути явно перетворена до типу компонент з урахуванням вирівнювання. При виділенні пам'яті її комірки обнулюються. Якщо в купі недостатньо місця, то функція повертає NULL.
Третя – realloc – змінює розмір поля, виділеного раніше однією зі стандартних функцій зі збереженням даних. За необхідності дані ко- піюються в нову область пам'яті. Якщо операцію здійснити неможли- во, то функція повертає NULL, а пам'ять залишається без змін. Для ptr=NULL функція працює як malloc(0). Виклик realloc(ptr,0) повер-
тає безтиповий покажчик і звільняє заняту пам'ять. Якщо обсяг нової пам'яті менший ніж попередньої, то зайві байти втрачаються.
Функція void free(char *ptr) звільняє область купи, розподілену ра- ніше функціями malloc та realloc. Аргументом її є покажчик, поверну- тий однією із цих функцій. Виклик free(NULL) не здійснює жодних дій.
Функція void сfree(char *ptr) робить те саме, але для покажчиків, повернутих функцією calloc.
3.5.6. МОДЕЛЮВАННЯ ДИНАМІЧНИХ МАСИВІВ
У підрозд. 3.4.2 динамічні масиви були визначені як масиви зі змінною довжиною. Пам'ять під такі масиви виділяється не під час компіляції, а в процесі виконання програми, коли з'являється конкре- тна довжина масиву.
Завдяки операції індексування динамічні масиви легко можна промоделювати неявно. Для цього за допомогою функцій calloc або malloc виділяється поле в купі з необхідною кількістю елементів дано- го типу Т і присвоюється покажчику типу Т, який потім шляхом інде- ксування забезпечує прямий доступ до відповідних ділянок динаміч- ного масиву 31. Це також відкриває шлях до використання надвели- ких масивів, розмір яких може становити кілька сегментів ОП. Спра- ва в тім, що вся пам'ять, виділена автоматичним даним під час ви-
31 Іноді кажуть, що ці функції породжують масив елементів у динамічній пам’яті.
347

ПРОГРАМУВАННЯ
клику функції, обмежена зазвичай одним сегментом, розмір якого
залежить від реалізації мови (часто це 216 = 65536 байтів). Динамічний одновимірний масив a[n] із n компонентами типу
float можна промоделювати таким чином:
int n; float *a;
scanf("%d", &n); a=(float*)(calloc(n,sіzeof(float));
Для n = 30000 та sizeof(float)=4 розмір масиву a становитиме
30000 × 4 =120000 байтів.
Наступні два приклади містять зразки обробки неявних динаміч- них масивів.
Приклад 3.45. Функція in_out вводить динамічний масив v, а по- тім виводить його значення на екран у зворотному порядку:
double * in_out(float *v, unsigned long n)
{
for (int i=0; i<n; i++)
{printf("v[%d]=", i); scanf("%f", &v[i]);
}
for (i=n-1; i>=0; i--)
printf("\t v[%d]=%f", i, v[i]); return v;
}
Функція Re_allocArray збільшує граничний розмір динамічного ма- сиву v і додає до нього новий елемент:
#defne ARR_INCREMENT 128 /*кількість компонентів, на яку збільшується масив v*/
int Re_allocArray (float *v, double new_el, unsigned long *lim, unsigned long *count)
/* *lim – гранична кількість компонентів динамічного масиву*/ /* *count – поточна кількість компонентів динамічного масиву*/
{if (*count<*lim) v[*count++]=new_el; else {
int new_lim=*lim+ ARR_INCREMENT;
if (double *new_arr=realloc(v, new_lim*sizeof(double)))==NULL) {printf("No memory");}
348

Розділ ІІІ. МОВИ ПРОГРАМУВАННЯ С ТА С++
else {
v=new_arr; *lim=new_lim; v[*count++]=new_el;
}
}
return *count;
}
Виклик функції Re_allocArray (a, 3.1428, &n, &n) додасть до ма-
сиву a число 3.1428 ■
Для створення двовимірного динамічного масиву спочатку потріб- но виділити пам'ять для масиву покажчиків на одновимірні масиви, а потім – під самі масиви.
Приклад 3.46. Створення двовимірного динамічного масиву v[n][m]:
{double **v; int n, m;
scanf("%d %d", &n, &m); v=(double**)calloc(m, sizeof(double*));
for (i=0; i<n; i++)
v[i]=(double*)calloc(m, sizeof(double));
…
}■
Якщо виникає необхідність працювати з надвеликими одновимір- ними масивами з кількістю елементів більше sizeof(size_t), то для їх моделювання застосовують саме двовимірні динамічні масиви.
*Література для CР: покажчики – [55, 102, 132, 133]; надве- ликі масиви – [55, 102, 122].
Контрольні запитання та вправи
1.Що таке покажчик і тип покажчиків?
2.Які бувають покажчики?
3.Навести визначення операції взяття адреси та розімену- вання покажчика.
4.Що таке константа NULL?
5.Як зводяться покажчики?
6.Що таке індексація покажчика?
7.Який існує зв'язок між масивами й покажчиками?
8.Що таке збільшення та зменшення покажчиків?
9.Сформулювати операції адресної арифметики.
10. Як описується масив покажчиків?
349

ПРОГРАМУВАННЯ
11.Що таке динамічний масив?
12.Як описується динамічний масив?
13.Дати визначення функцій динамічного виділення пам'яті.
14.Як моделюються динамічні масиви за допомогою покаж- чиків?
15.Що таке надвеликий масив?
16.Як моделюються надвеликі масиви?
17.Якого значення набуде змінна p після виконання оператора
{int p=3, *a=&p, q=2; *a*=p-=q+=2;}?
18.Написати варіанти бібліотечних (<string.h>) функцій strlen, strcpy, strcat, strncpy, strncat, ctrncmp,
використовуючи: а) індексацію; б) розіменування покаж- чиків.
19.Написати функції int atoi(char *p), itoa(int n, char *p) та long htol(char *p), які перетворюють, відпові- дно: а) послідовність десяткових цифр зі знаком або без нього на ціле типу int; б) ціле типу int на послідовність
символів – знака й значущих десяткових цифр; в) послідовність шістнадцяткових цифр із префіксом 0х або 0Х (якому може передувати знак) на ціле типу long.
20. Написати функцію int getint(int *pn), що здійснює введення з клавіатури у вільному форматі цілого числа й подання його цілим типу int. Функція повертає прочита- не ціле через свій параметр. Вона має сигналізувати про успішність (неуспішність) введення числа, а також про досягнення кінця вхідного потоку.
21.Написати функцію int getdouble(double *pa) – аналог функції getint – для введення дійсних чисел.
22.Що виведуть фрагменти програм:
char *ch[]={"12345", "ABCDE", "67800", "3401"};
а) main()
{char **chp[]={ch+2,ch,ch+1}; char ***chpp=chp;
printf ("%s", **chpp); printf ("%s", **++chpp+2);
}
б) main()
{char **chp[]={ch+2,ch,ch+1,ch+3}; char ***chpp=chp;
350