Смекни!
smekni.com

Особливості мови програмування С (стр. 2 из 6)

Наприклад, якщо один змінна містить адреса інший змінної, говорять, що перша змінна посилається на другу

Показник ідентифікує змінну, говорячи не про її ім'я, а про те, де вона перебуває в пам'яті.

Оголошення показника складається з імені базового типу, символу * й імені змінної [3].

2.1.1.2 Вказівники в С++

Програми на C++ зберігають змінні в пам'яті. Вказівником є адреса пам'яті, яка вказує (або посилається) на певну ділянку. Для зміни параметра усередині функції програма повинна передати адресу параметра (вказівник) у функцію. Далі функція у свою чергу використовує змінну-вказівник для звернення до ділянки пам'яті. Деякі програми використовують вказівники на параметри. Аналогічно цьому, коли програми працюють з символьними рядками і масивами, вони зазвичай використовують вказівники, щоб оперувати елементами масиву. Для простоти (для зменшення коду) багато програм трактують символьний рядок як вказівник і маніпулюють вмістом рядка, використовуючи операції з вказівниками.

Коли збільшують змінну-вказівник (змінну, яка зберігає адресу), C++ автоматично збільшує адресу на необхідну величину (на 1 байт для char, на 2 байти для int, на 4 байти для float і так далі).

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

2.1.1.3 Оператор розіменування

Для доступу до об'єкту через вказівник використовується оператор розіменування *, що здійснює так звану непряму адресацію: *p означає «об'єкт, на який вказує p». По суті *p є ссилкою на об'єкт.

Спроба розіменування нульового вказівника приводить до помилки при виконанні програми, спроба ж розіменування вказівника 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++)