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

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

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

 

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

char * р = new char; /* виділення пам'яті під покажчик і під динамічну змінну типу char */

*р = 'А'; а = *р; // привласнення значення обом змінним

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

 

#include <stdio.h>

int main()

{

unsigned long int A = 0Xcc77ffaa;

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

unsigned char* pchar = (unsigned char *) &A;

printf(" | %x | Xx | Xx |", A *pint, *pchar);

return 0;

}

 

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

| cc77ffaa | ffaa | аа |

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

При змішуванні у виразі покажчиків різних типів явне перетворення типів потрібне для всіх покажчиків, окрім void*. Покажчик може неявно перетворюватися в значення типу bool (наприклад, у виразі умовного оператора), при цьому ненульовий покажчик перетвориться в true, а нульовий в false. Привласнення без явного приведення типів допускається в двох випадках:

покажчикам типу void*;

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

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

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

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

 

short * р = new short [5]:

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

long * q = new long [5];

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

 

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

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

 

*р++ = 10;

 

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

 

*р = 10; р++;

 

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

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

Ідентифікатор масиву є константним покажчиком на його нульовий елемент. Наприклад, для масиву з попереднього лістингу ім'я b - це те ж саме, що &b[0], а до i-му елементу масиву можна звернутися, використовуючи вираз *(b+i). Можна описати покажчик, привласнити йому адресу почала масиву і працювати з масивом через покажчик. Наступний фрагмент програми копіює всі елементи масиву а в масив b:

 

Int a[100], b[100];

int *pa = а;

int *pb = b:

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

*pb++ = *pa++; // або pb[i]= pa[i];

 

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

 

int n = 100;

float *р = new float [n];

 

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

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

Альтернативний спосіб створення динамічного масиву - використання функції malloc бібліотеки C:

 

int n = 100:

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

 

Операція перетворення типу, записана перед зверненням до функції malloc, потрібна тому, що функція повертає значення покажчика типу void*, а ініціалізувався покажчик на float.

Пам'ять, зарезервована під динамічний масив за допомогою new [], повинна звільнятися операцією delete []. а пам'ять, виділена функцією mallос - за допомогою функції free, наприклад:

 

delete [] p; free (q);

 

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

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

 

int matr [5][6];

 

задає опис двовимірного масиву з 5 рядків і 6 стовпців. У пам'яті такий масив розташовується в послідовних осередках відрядковий. Багатовимірні масиви розміщуються так, що при переході до наступного елементу найшвидше змінюється останній індекс. Для доступу до елементу багатовимірного масиву указуються всі його індекси, наприклад, matr[i][j], або більш екзотичним способом: *(matr[i]+j) або *(*(matr+i )+j). Це можливо, оскільки matr[i] є адресою почала i-й рядки масиву.

Для створення динамічного багатовимірного масиву необхідно вказати в операції new все його розмірності (найлівіша розмірність може бути змінною) наприклад:

 

int nstr = 5;

int ** m = (int **) new int [nstr][10];

 

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

 

int nstr, nstb;

cout << " Введіть кількість рядків і стовпців :";
cin >> nstr , nstb;
int **a = new int *[nstr]; // 1
for(int i = 0; i<nstr; i++) // 2
а[i] = new int [nstb]; // 3

 

У операторі 1 оголошується змінна типу покажчик на покажчик на int і виделяєтся пам'ять під масив покажчиків на рядки масиву (кількість рядків nstr). У операторі 2 організовується цикл для виділення пам'яті під кожною строь масиву. У операторі 3 кожному елементу масиву покажчиків на рядки привласнюється адреса почала ділянки пам'яті, виділеного під рядок двовимірного масиву. Кожен рядок складається з nstb елементів типу int.

Звільнення пам'яті з-під масиву з будь-якою кількістю вимірювань виконується за допомогою операції delete []. Покажчик на константу видалити не можна.