Методи доступу до елементів масивів
У мові Сі між покажчиками і масивами існує тісний зв’язок. Наприклад, коли оголошується масив у вигляді int array[25]
, то цим визначається не тільки виділення пам’яті для двадцяти п’яти елементів масиву, але і для покажчика з ім’ям array, значення якого рівне адресі першого по рахунку (нульового) елементу масиву, тобто сам масив залишається безіменним, а доступ до елементів масиву здійснюється через покажчик з ім’ям array. З погляду синтаксису мови покажчик array є константою, значення якої можна використовувати у виразах, але змінити це значення не можна.
Оскільки ім’я масиву є покажчиком допустимо, наприклад, таке присвоєння:
1int array[25];
2int *ptr;
3ptr=array;
Тут покажчик ptr
встановлюється на адресу першого елементу масиву, причому присвоєння ptr=array
можна записати в еквівалентній формі ptr=&array[0]
.
Для доступу до елементів масиву існує два різні способи. Перший спосіб зв’язаний з використанням звичних індексних виразів в квадратних дужках, наприклад, array[16]=3
або array[i+2]=7
. При такому способі доступу записуються два вирази, причому другий вираз полягає в квадратні дужки. Один з цих виразів повинен бути покажчиком, а друге - виразом цілого типу. Послідовність запису цих виразів може бути будь-якою, але в квадратних дужках записується вираз наступний другим. Тому записи array[16]
і 16[array]
будуть еквівалентними і позначають елемент масиву з номером шістнадцять. Покажчик використовуваний в індексному виразі не обов’язково повинен бути константою, вказуючою на який-небудь масив, це може бути і змінна. Зокрема після виконання присвоєння ptr=array
доступ до шістнадцятого елементу масиву можна дістати за допомогою покажчика ptr у формі ptr[16]
або 16[ptr]
.
Другий спосіб доступу до елементів масиву зв’язаний з використанням адресних виразів і операції розадресації у формі *(array+16)=3
або *(array+i+2)=7
. При такому способі доступу адресний вираз рівний адресі шістнадцятого елементу масиву теж може бути записане різними способами *(array+16)
або *(16+array)
.
При реалізації на комп’ютері перший спосіб приводиться до другого, тобто індексний вираз перетвориться до адресного. Для приведених прикладів array[16]
і 16[array]
перетворяться в *(array+16).
Для доступу до початкового елементу масиву (тобто до елементу з нульовим індексом) можна використовувати просто значення покажчика array або ptr. Будь-яке з присвоєнь
1*array = 2;
2array[0]= 2;
3*(array+0) = 2;
4*ptr = 2;
5ptr[0]= 2;
6*(ptr+0) = 2;
присвоює початковому елементу масиву значення 2, але найшвидше виконаються присвоєння *array=2
і *ptr=2
, оскільки в них не вимагається виконувати операції складання.
Покажчики на багатовимірні масиви
Покажчики на багатовимірні масиви в мові Сі - це масиви масивів, тобто такі масиви, елементами яких є масиви. При оголошенні таких масивів в пам’яті комп’ютера створюється декілька різних об’єктів. Наприклад при виконанні оголошення двовимірного масиву int arr2[4][3]
в пам’яті виділяється ділянка для зберігання значення змінної arr, яка є покажчиком на масив з чотирьох покажчиків. Для цього масиву з чотирьох покажчиків теж виділяється пам’ять. Кожний з цих чотирьох покажчиків містить адресу масиву з трьох елементів типу int, і, отже, в пам’яті комп’ютера виділяється чотири ділянки для зберігання чотирьох масивів чисел типу int, кожний з яких складається з трьох елементів. Таке виділення пам’яті показане на схемі на рис.3.
Рис.3. Розподіл пам’яті для двовимірного масиву.
Таким чином, оголошення arr2[4][3]
породжує в програмі три різні об’єкти: покажчик з ідентифікатором arr, безіменний масив з чотирьох покажчиків і безіменний масив з дванадцяти чисел типу int. Для доступу до безіменних масивів використовуються адресні вирази з покажчиком arr. Доступ до елементів масиву покажчиків здійснюється з вказівкою одного індексного виразу у формі arr2[2]
або *(arr2+2)
. Для доступу до елементів двовимірного масиву чисел типу int повинні бути використані два індексні вирази у формі arr2[1][2]
або еквівалентних їй *(*(arr2+1)+2)
і (*(arr2+1))[2]
. Слід враховувати, що з погляду синтаксису мови Сі покажчик arr і покажчики arr[0]
, arr[1]
, arr[2]
, arr[3]
є константами і їх значення не можна змінювати під час виконання програми.
Розміщення тривимірного масиву відбувається аналогічно і оголошення float arr3[3][4][5]
породжує в програмі окрім самого тривимірного масиву з шістдесяти чисел типу float
масив з чотирьох покажчиків на тип float
, масив з трьох покажчиків на масив покажчиків на float
, і покажчик на масив масивів покажчиків на float
.
При розміщенні елементів багатовимірних масивів вони розташовуються в пам’яті підряд по рядках, тобто найшвидше змінюється останній індекс, а повільніше - перший. Такий порядок дає можливість звертатися до будь-якого елементу багатовимірного масиву, використовуючи адресу його початкового елементу і лише один індексний вираз.
Наприклад, звернення до елементу arr2[1][2]
можна здійснити за допомогою покажчика ptr2, оголошеного у формі int *ptr2=arr2[0]
як звернення ptr2[1*4+2]
(тут 1 і 2 це індекси використовуваного елементу, а 4 це число елементів в рядку) або як ptr2[6]
. Помітимо, що зовні схоже звернення arr2[6]
виконати неможливо оскільки покажчика з індексом 6 не існує.
Для звернення до елементу arr3[2][3][4]
з тривимірного масиву теж можна використовувати покажчик, описаний як float *ptr3=arr3[0][0]
з одним індексним виразом у формі ptr3[3*2+4*3+4]
або ptr3[22]
.
Далі приведена функція, що дозволяє знайти мінімальний елемент в тривимірному масиві. У функцію передається адреса початкового елементу і розміри масиву, значення, що повертається, - покажчик на структуру, що містить індекси мінімального елементу.
1struct INDEX {
2 int i,
3 int j,
4 int k
5} min_index ;
6
7struct INDEX *find_min (int *ptr1, int l, int m int n)
8{
9 int min, i, j, k, ind;
10 min=*ptr1;
11 min_index.i=min_index.j=min_index.k=0;
12 for (i=0; i*(ptr1+ind)
13 {
14 min=*(ptr1+ind);
15 min_index.i=i;
16 min_index.j=j;
17 min_index.k=k;
18 }
19 }
20 return min_index;
21}
Операції з покажчиками
Над покажчиками можна виконувати унарні операції: інкремент і декремент. При виконанні операцій ++
і --
значення покажчика збільшується або зменшується на довжину типу, на який посилається використовуваний покажчик.
Приклад:
1int *ptr, а[10];
2ptr=&a[5];
3ptr++; /* дорівнює адресі елементу а[6]*/
4ptr--; /* дорівнює адресі елементу а[5]*/
У бінарних операціях складання і віднімання можуть брати участь покажчик і величина типу int. При цьому результатом операції буде покажчик на початковий тип, а його значення буде на вказане число елементів більше або менше початкового.
Приклад:
1int *ptr1, *ptr2, а[10];
2int i=2;
3ptr1=a+(i+4); /* дорівнює адресі елементу а[6]*/
4ptr2=ptr1-i; /* дорівнює адресі елементу а[4]*/
У операції віднімання можуть брати участь два покажчики на один і той же тип. Результат такої операції має тип int і рівний числу елементів початкового типу між зменшуваним і віднімається, причому якщо перша адреса молодше, то результат має негативне значення.
Приклад:
1int *ptr1, *ptr2, а[10];
2int i;
3
4ptr1=a+4;
5ptr2=a+9;
6i=ptr1-ptr2; /* дорівнює 5 */
7i=ptr2-ptr1; /* дорівнює -5 */
Значення двох покажчиків на однакові типи можна порівнювати в операціях ==, !=, <, <=",">, >= при цьому значення покажчиків розглядаються просто як цілі числа, а результат порівняння рівний 0 (фальш) або 1 (істина).
Приклад:
1int *ptr1, *ptr2, а[10];
2ptr1=a+5;
3ptr2=a+7;
4if (prt1>ptr2) а[3]=4;
У даному прикладі значення ptr1 менше значення ptr2 і тому оператора а[3]=4 не буде виконаний.
Масиви покажчиків
У мові Сі елементи масивів можуть мати будь-який тип, і, зокрема, можуть бути покажчиками на будь-який тип. Розглянемо декілька прикладів з використанням покажчиків.
Наступні оголошення змінних
int a[]={10,11,12,13,14,};
int *p[]={а, a+1, a+2, a+2, a+3, a+4};
int **pp=p;
породжують програмні об’єкти, представлені на схемі на рис.4.
Рис.4. Схема розміщення змінних при оголошенні.
При виконанні операції pp-p набудемо нульове значення, оскільки посилання pp і p рівні і указують на початковий елемент масиву покажчиків, пов’язаного з покажчиком p ( на елемент p[0]).
Після виконання операції pp+=2 схема зміниться і матиме вигляд, зображений на рис.5.
Рис.5. Схема розміщення змінних після виконання операції pp+=2.
Результатом виконання віднімання pp-p
буде 2, оскільки значення pp
є адреса третього елементу масиву p
. Посилання *pp-a
теж дає значення 2, оскільки звертання *pp
є адреса третього елементу масиву а, а звернення а є адреса початкового елементу масиву а. При звертанні за допомогою посилання **pp одержимо 12 - це значення третього елементу масиву а. Посилання *pp++
дасть значення четвертого елементу масиву p тобто адреса четвертого елементу масиву а.
Якщо вважати, що pp=p, то звернення *++pp
це значення першого елементу масиву а (тобто значення 11), операція ++*pp
змінить вміст покажчика p[0]
, таким чином, що він стане рівним значенню адреси елементу а[1]
.
Складне посилання розкривається зсередини. Наприклад звернення *(++(*pp))
можна розбити на наступні дії: *pp
дає значення початкового елементу масиву p[0]
, далі це значення інкремінуєтся ‘++(*p)’ внаслідок чого покажчик ‘p[0]’ стане рівний значенню адреси елементу ‘а[1]’, і остання дія це вибірка значення за одержаною адресою, тобто значення 11
.
У попередніх прикладах був використаний одновимірний масив, розглянемо тепер приклад з багатовимірним масивом і покажчиками. Наступні оголошення змінних
1int а[3][3]={ { 11,12,13 },
2 { 21,22,23 },
3 { 31,32,33 } };
4
5int *pa[3]={ а, а[1],a[2]};
6int *p=a[0];
породжують в програмі об’єкти представлені на схемі на рис.6.
Рис.6. Схема розміщення покажчиків на двовимірний масив.
Згідно цій схемі доступ до елементу а[0][0]
одержати по покажчиках а
, p
, pa
за допомогою наступних посилань: а[0][0]
, *a
, **a[0]
, *p
, **pa
, *p[0]
.
Розглянемо тепер приклад з використанням рядків символів. Оголошення змінних
1char *c[]={ "abs", "dx", "yes", "no" };
2char **cp[]={ c+3, c+2, c+1, з };
3char ***cpp=cp;
можна зобразити схемою представленої на рис.7.
Рис.7. Схема розміщення покажчиків на рядки.
Динамічне розміщення масивів
При динамічному розподілі пам’яті для масивів слід описати відповідний покажчик і привласнювати йому значення за допомогою функції calloc. Одновимірний масив а[10]
з елементів типу float можна створити таким чином
1float *a;
2a=(float*)(calloc(10,sizeof(float)));
Для створення двовимірного масиву спочатку потрібно розподілити пам’ять для масиву покажчиків на одновимірні масиви, а потім розподіляти пам’ять для одновимірних масивів. Хай, наприклад, вимагається створити масив а[n][m]
, це можна зробити за допомогою наступного фрагмента програми:
1#include
2main ()
3{
4 double **a;
5 int n,m,i;
6 scanf("%d %d",&n,&m);
7 a=(double **) calloc(m,sizeof(double *));
8 for (i=0; i<=m; i++)
9 а[i]=(double *)calloc(n,sizeof(double));
10 /*. . . . . . . . . . . .*/
11}
Аналогічним чином можна розподілити пам’ять і для тривимірного масиву розміром n,m,l. Слід тільки пам’ятати, що непотрібну для подальшого виконання програми пам’ять слід звільняти за допомогою функції free.
1#include
2main ()
3{
4 long ***a;
5 int n,m,l,i,j;
6 scanf("%d %d %d",&n,&m,&l);
7 /* -------- розподіл пам'яті -------- */
8 a=(long ***) calloc(m,sizeof(long **));
9 for (i=0; i<=m; i++)
10 {
11 а[i]=(long **)calloc(n,sizeof(long *));
12 for (j=0; i<=l; j++) а[i][j]=(long *)calloc(l,sizeof(long));
13 }
14
15 //. . . . . . . . . . . .
16 /* звільнення пам'яті */
17 for (i=0; i<=m; i++)
18 {
19 for (j=0; j<=l; j++)
20 free (а[i][j]);
21 free (а[i]);
22 }
23 free (a);
24}
Розглянемо ще один цікавий приклад, в якому пам’ять для масивів розподіляється у функції, що викликається, а використовується в тій, що викликає. У такому разі у функцію, що викликається, вимагається передавати покажчики, яким будуть привласнені адреси пам’яті, що виділяється для масивів.
Приклад:
1// Online C compiler to run C program online
2#include <stdio.h>
3#include <stdlib.h>
4
5int input_data(double ***, long **);
6
7int main()
8{
9 double **a; /* покажчик для масиву а[n][m] */
10 long *b; /* покажчик для масиву b[n] */
11 input_data (&a,&b);
12 /* у функцію input_data передаються адреси покажчиків, */
13 /* а не їх значення */
14}
15
16int input_data(double ***a, long **b)
17{
18 int n,m,i,j;
19 a[n][m];
20
21 scanf ("%d %d",&n,&m);
22
23 *a=(double **) calloc(n,sizeof(double *));
24 *b=(long *) calloc(n,sizeof(long));
25
26 for (i=0; i<=n; i++)
27 *a[i]=(double *) calloc(m,sizeof(double));
28 /*.....*/
29}
Відзначимо також ту обставину, що покажчик на масив не обов’язково повинен показувати на початковий елемент деякого масиву. Він може бути зсунутий так, що початковий елемент матиме індекс відмінний від нуля, причому він може бути як позитивним так і негативним.
Приклад:
1#include
2
3int main()
4{
5 float *q, **b;
6 int i, j, до, n, m;
7 scanf("%d %d",&n,&m);
8 q=(float *) calloc(m,sizeof(float));
9 /* зараз покажчик q показує на початок масиву */
10 q[0]=22.3;
11 q-=5;
12 /* тепер початковий елемент масиву має індекс 5, */
13 /* а кінцевий елемент індекс n-5 */
14 q[5]=1.5;
15
16 /* Зміщення індексу не приводить до перерозподілу */
17 /* масиву в пам'яті і зміниться початковий елемент */
18 q[6]=2.5; /* - це другий елемент */
19 q[7]=3.5; /* - це третій елемент */
20 q+=5;
21 /* тепер початковий елемент знов має індекс 0, */
22 /* а значення елементів q[0], q[1], q[2] рівні */
23 /* відповідно 1.5, 2.5, 3.5 */
24 q+=2;
25 /* тепер початковий елемент має індекс -2, */
26 /* наступний -1, потім 0 і т.д. по порядку */
27
28 q[-2]=8.2;
29 q[-1]=4.5;
30 q-=2;
31
32 /* повертаємо початкову індексацію, три перших */
33 /* елементу масиву q[0], q[1], q[2], мають */
34 /* значення 8.2, 4.5, 3.5 */
35 q--;
36 /* знов змінимо індексацію . */
37 /* Для звільнення області пам'яті в якій розміщений */
38 /* масив q використовується функція free(q), але оскільки */
39 /* значення покажчика q зміщене, те виконання */
40 /* функції free(q) приведе до непередбачуваних наслідків. */
41 /* Для правильного виконання цієї функції */
42 /* покажчик q повинен бути повернений в первинне */
43 /* положення */
44
45 free(++q);
46
47 /* Розглянемо можливість зміни індексації і */
48 /* звільнення пам'яті для двовимірного масиву */
49 b=(float **) calloc(m,sizeof(float *));
50 for (i=0; i < m; i++)
51 b[i]=(float *)calloc(n,sizeof(float));
52 /* Після розподілу пам'яті початковим елементом */
53 /* масиву буде елемент b[0][0] */
54 /* Здійсниме Зміщення індексів так, щоб початковим */
55 /* елементом став елемент b[1][1] */
56
57 for (i=0; i < m ; i++)
58 --b[i];
59
60 b--;
61
62 /* Тепер привласнимо кожному елементу масиву суму його */
63 /* індексів */
64
65 for (i=1; i<=m; i++)
66 for (j=1;j<=n; j++)
67 b[i][j]=(float)(i+j);
68 /* Зверніть увагу на початкові значення лічильників */
69 /* циклів i і j, він починаються з 1 а не з 0 */
70 /* Повернемося до колишньої індексації */
71 for (i=1; i<=m; i++)
72 ++b[i];
73 b++;
74 /* Виконаємо звільнення пам'яті */
75 for (i="0;" i < m; i++)
76 free(b[i]);
77 free(b);
78 /*... ...*/
79 return 0;
80 }
Як останній приклад розглянемо динамічний розподіл пам’яті для масиву покажчиків на функції, що мають один вхідний параметр типу double і повертають значення типу double.
Приклад:
1#include <stdio.h>
2#include <stdlib.h>
3#include <math.h>
4
5double cos(double);
6double sin(double);
7double tan(double);
8
9int main()
10{
11 double (*(*masfun))(double);
12 double x=0.5, y;
13 int i;
14 masfun=(double(*(*))(double))
15 calloc(3,sizeof(double(*(*))(double)));
16
17 masfun[0]=cos;
18 masfun[1]=sin;
19 masfun[2]=tan;
20
21 for (i=0; i<3; i++);
22 {
23 y=masfun[i](x);
24 printf("\n x=%g y=%g",x,y);
25 }
26 return 0;
27}
Коментарі