Наприклад, якщо один змінна містить адреса інший змінної, говорять, що перша змінна посилається на другу
Показник ідентифікує змінну, говорячи не про її ім'я, а про те, де вона перебуває в пам'яті.
Оголошення показника складається з імені базового типу, символу * й імені змінної [3].
2.1.1.2 Вказівники в С++
Програми на C++ зберігають змінні в пам'яті. Вказівником є адреса пам'яті, яка вказує (або посилається) на певну ділянку. Для зміни параметра усередині функції програма повинна передати адресу параметра (вказівник) у функцію. Далі функція у свою чергу використовує змінну-вказівник для звернення до ділянки пам'яті. Деякі програми використовують вказівники на параметри. Аналогічно цьому, коли програми працюють з символьними рядками і масивами, вони зазвичай використовують вказівники, щоб оперувати елементами масиву. Для простоти (для зменшення коду) багато програм трактують символьний рядок як вказівник і маніпулюють вмістом рядка, використовуючи операції з вказівниками.
Коли збільшують змінну-вказівник (змінну, яка зберігає адресу), C++ автоматично збільшує адресу на необхідну величину (на 1 байт для char, на 2 байти для int, на 4 байти для float і так далі).
Програми можуть використовувати вказівники для роботи з масивами цілочисельних значень або значень з плаваючою крапкою.
2.1.1.3 Оператор розіменування
Спроба розіменування нульового вказівника приводить до помилки при виконанні програми, спроба ж розіменування вказівника void*(див. розділ 2.4) викличе помилку на етапі компіляції.
Однією з поширених помилок є розіменування неініціалізованих вказівників. Наприклад, в будь-якому з наступних випадків результат роботи програми непередбачуваний:
char* s;
*s=’a’; // помилка!
cin>>s; // помилка!
Відзначимо, що в останньому випадку, де виводиться рядок, на який вказує s, розіменування присутнє неявно і відбувається усередині оператора введення з потоку [3].
2.1.1.4 Перетворення типів
Показник одного типу не можна привласнити показнику іншого типу без явного перетворення типів. Виняток становить показник void*(див. розділ 2.4), який трактується як показник на деяку ділянку пам'яті. Він називається родовим показником і може отримувати як значення показник на будь-якого іншого типа без явного перетворення.
int i;
void* pi=&i;
Явні перетворення типів в більшості випадків є потенційно небезпечними і повинні застосовуватися з крайньою обережністю. Так, в наступному прикладі
double d=*static_cast<double*>pi;
вміст змінної d непередбачувано.
Властивість показників void*(див. розділ 2.4) зберігати дані різнорідних типів використовується при створенні так званих родових (generic) масивів, тобто масивів, що зберігають різнотипні об'єкти. Наприклад:
int i=5;
char c='u';
double d[2]={3,6};
void* generic[]={&i,&c,&d};
cout<<*static_cast<int*>generic[0]<<’ ’
<<*static_cast<char*>generic[1]<<’ ’
<<(static_cast<double*>generic[2])[1];[4].
2.1.1.5 Вказівники і передача параметрів у функції
void swap(int* pa, int* pb)
{ int t=*pa; *pa=*pb; *pb=t;}
У функціях стандартної бібліотеки можна зустріти безліч прикладів передачі параметрів з використанням вказівників. Така, наприклад, функція memcpy, призначена для копіювання даних з однієї області пам'яті в іншу. Вона має наступний прототип, оголошений в заголовному файлі <string.h> (або за стандартом 1998 р. в <cstring>):
void *memcpy(void *dest, const void *src, size_t n);
і забезпечує копіювання n байтів даних з src в dest. Параметр src
оголошений Вказівником на константу, що свідчить про те, що дані, на які він вказує, не можуть бути змінені функцією memcpy. Оскільки як тип фігурує void*, то замість src і dest при виклику функції memcpy можна підставляти вказівник на будь-якого типа. Нарешті, стандартний тип size_t використовується для вказівки того, що дана змінна зберігає розмір і є беззнакове ціле (зазвичай unsigned int або unsigned long).
Повернення функцією вказівника на локальну змінну є грубою помилкою. Наприклад, в ситуації
int* f()
{
int i=5;
return &i;
}змінна i руйнується після виходу з функції, тому результат роботи програми непередбачуваний.
2.1.1.6 Арифметичні дії над вказівниками
Над вказівниками можна здійснювати ряд арифметичних дій. При цьому передбачається, що якщо вказівник p відноситься до типа T*, то p вказує на елемент деякого масиву типа T. Тоді р+1 є Вказівником на наступний елемент цього масиву, а р-1 - Вказівником на попередній елемент. Аналогічно визначаються вирази р+n, n+p і р-n, а також дії p++, p--, ++p, --p, p+=n, p-=n, де n - ціле число. Поважно відзначити, що арифметичні дії з вказівниками виконуються в одиницях того типа, до якого відноситься вказівник. Тобто р+n, перетворене до цілого типа, містить на sizeof(T)*n більше значення, чим р.
З рівності p+n==p1 виходить, що p1-p==n. Саме так вводиться оператор різниці двох вказівників: його значенням є ціле, рівне кількості елементів масиву від p до p1. Відзначимо, що це - єдиний випадок в мові, коли результат бінарного оператора з операндами одного типа належить до принципово іншого типу.
Сума двох вказівників не має сенсу і тому не визначена. Не визначені також арифметичні дії над вказівниками void*, що не типізуються.
Нарешті, всі вказівники, у тому числі і що не типізуються, можна порівнювати, використовуючи операторів відношення >, <, >=, <= ==, != [4].
2.1.1.7 Вказівники і масиви
Вказівники і масиви тісно взаємозв'язані. Ім'я масиву може бути неявно перетворене до константного вказівника на перший елемент цього масиву. Так &a[0] рівноцінно а. Взагалі, вірна формула
&а[n]== a+n
тобто адреса n-того елементу масиву є збільшений на n елементів вказівник на початок масиву. Розийменовуя ліву і праву частини, отримуємо основну формулу, що зв'язує масиви і вказівники:
а[n]== *(a+n)
Дана формула, не дивлячись на простоту, вимагає декількох пояснень. По-перше, компілятор будь-який запис вигляду а[n] інтерпретує як *(a+n). По-друге, формула (*) пояснює, чому в C++ масиви індексуються з нуля і чому немає контролю виходу за кордони діапазону. Нарешті, використовуючи (*), ми можемо записати наступний ланцюжок рівності:
а[n]== *(a+n) == *(n+a) == n[a]
Таким чином, елемент масиву а з індексом 2 можна позначити не лише як а, але і як 2[a]
Із зв'язку масивів і вказівників витікає спосіб передачі масивів у функції - за допомогою вказівника на перший елемент[5].
2.1.1.8 Масиви вказівників на масиви
Двовимірні масиви можна створювати також за допомогою масивів вказівників, ініціалізувавши їх елементи адресами одновимірних масивів. Наприклад:
int b0[4]={1,2,3,4},
b1[4]={5,6,7,8},
b2[4]={9,0,1,2};
int* а[3]={b0,b1,b2};
В цьому випадку вираження а[1][2] розшифровується як *(а[1]+2), що у свою чергу є *(b1+2), або b1[2]. Аналогічного ефекту можна добитися, ініціалізувавши масив а динамічними одновимірними масивами:
int* а[3];
for (int i=0; i<3; i++)
а[i]=new int[4];
Сам масив вказівників також можна створити в динамічній пам'яті. Він контролюватиметься покажчиком на типа int*, тобто змінній типа int**. В результаті ми отримаємо двовимірний динамічний масив, розмірності якого можна задавати при виділенні пам'яті в процесі роботи програми:
int **a,n,m;
n=3; m=4;
a=new int*[n];
for (int i=0; i<n; i++)
а[i]=new int[m];
При звільненні пам'яті, займаної таким масивом, треба діяти в зворотному порядку, спочатку визволяючи рядки, а потім - сам одновимірний масив вказівників.
for (int i=0; i<n; i++)
delete[] а[i];
delete[] а;
До елементів нашого двовимірного динамічного масиву можна звертатися звичайним способом: а[1][2].
Масив вказівників на масиви. Розподіл пам'яті.
По формулі (*) а[1][2]==*(*(a+1)+2). Але, на відміну від звичайного двовимірного масиву, а є покажчиком не на int[4], а на int*. Тому a+1 вказує на наступний елемент типа int* в одновимірному масиві а, тобто на а[1]. Нарешті, оскільки а[1] має типа int*, то а[1]+2 вказує на елемент а[1][2].
Відзначимо, що, на відміну від звичайного двовимірного масиву, рядки нашого динамічного масиву не обов'язково розташовуються в пам'яті послідовно. Саме завдяки цьому структура двовимірного динамічного масиву є надзвичайно гнучкою. Зокрема, для перестановки його рядків досить поміняти місцями покажчики в одновимірному масиві:
int* v=a[1]; а[1]=a[2]; а[2]=v;
Двовимірний динамічний масив дозволяє також зберігати рядки різної довжини. Наприклад, для створення нижнетреугольной матриці можна використовувати наступний фрагмент:
a=new int*[n];
for (int i=0; i<n; i++)
а[i]=new int[i+1];
Одновимірний масив вказівників може також зберігати C-строки, відводячи під них стільки місця, скільки вони займають. Ініціалізація такого масиву при введенні рядків із стандартного потоку cin приводиться нижче:
char* s[10];
char buf[80];
for (int i=0; i<10; i++)
{
cin.getline(buf,80);
s[i]=new char(strlen(buf)+1);
strcpy(s[i],buf);
}
Відзначимо, що масив C-строк також відноситься до двовимірних динамічних масивів з рядками змінної довжини. Зокрема, в алгоритмі сортування при перестановці рядків потрібно міняти місцями лише вказівники [6].
2.1.1.9 Двовимірні масиви як параметри функції
Розглянемо простий випадок:
void print(int а[3][4])
{
for (int i=0; i<3; i++)
{
for (int j=0; j<4; j++)
cout<<a[i][j];
cout<<endl;
}
}
int b[3][4];
...
print(b);
Подібна функція працює лише для масивів 3 4. Згадуючи, що при передачі у функцію інформація про розмір одновимірного масиву втрачається, ми можемо модифікувати попередній приклад для передачі масивів, що мають змінний перший розмір:
void print(int a[][4], int n)
{
for (int i=0; i<n; i++)