C++/Основи/Вказівники

Матеріал з Вікіпідручника
< C++

Вказівники[ред.]

Вказівник це змінна, значенням якої є адреса комірки пам’яті. Тобто вказівник посилається на блок даних із області оперативної пам’яті, при чому в самий його початок. Вказівник може посилатися на змінну або функцію. Для того, щоб дізнатися адресу змінної в C++ існує унарна операція взяття адреси “&”. Така операція повертає адресу об’явленої змінної, для того, щоб присвоїти її вказівнику.

Вказівник об’являється за тим самим принципом що і змінна, тільки перед ім’ям змінної ставиться символ *:

тип_даних * імя_змінної;

Наприклад:

int *ptr_a; // вказівник;
int b = 10; // проста змінна
ptr_a = &b; // взяття адреси і присвоєння її вказівнику.

В програмуванні прийнято додавати до імені вказівника приставку ptr, таким чином, не дозволяючи плутати прості змінні і змінні вказівників.

Вказівники на вказівники[ред.]

Вказівники можуть посилатися на інші вказівники. При цьому в комірці пам'яті, на котру буде посилатися перший вказівник, буде міститися не значення, а адреса іншого вказівника. Кількість символів “*” при об’явленні вказівника показує порядок вказівника. Щоб отримати доступ до значення, на яке посилається вказівник його слід розіменувати декілька разів.

int a = 7;                 
int *ptr_1 = &a;                           // вказівник на адрес змінної "а"
int **ptr_2 = &ptr_1;                      // вказівник на адрес вказівника ptr_1 (ptr_1 вказує на змінну "а")

cout << "*ptr_1  = " <<  *ptr_1  << endl;
cout << "**ptr_2 = " << **ptr_2  << endl;  // розіменування вказівника на вказівник " ** "

cout << "*ptr_2  = " << *ptr_2   << endl;  // так виведеться, лише адрес, на який вказує вказівник "ptr_2"
	                                       // тобто адрес вказівника "ptr_1"

Вказівники на функції[ред.]

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

тип_даних (* імя_вказівника)(аргумент_1[, аргумент_2, ...]);

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

Динамічне виділення пам'яті[ред.]

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

Основні відмінності між статичними і динамічними об'єктами такі:

  • статичні об'єкти визначаються іменованими змінними, і дії над цими змінними проводяться напряму, з використанням їх імен. Динамічні об'єкти не мають власних імен і дії над ними виконуються за допомогою вказівників.
  • виділення і звільнення пам'яті під статичними об'єктами виконується компілятором автоматично. Виділення і звільнення пам'яті під динамічні об'єкти цілком залежить від програміста. Ця задача досить складна і під час цього виникають помилки.

Для динамічного виділення пам'яті існують оператори new і delete. Оператор new має дві форми. Перша форма виділяє пам'ять під динамічний об'єкт одиничного типу:

int *ptr_int = new int(1024);

Тут оператор new виділяє пам'ять під безіменний об'єкт типу int і ініціалізує його значенням 1024 і повертає адресу створеного об'єкта. Ця адреса використовується для ініціалізації вказівника ptr_int. Всі дії над таким об'єктом виконуються шляхом розіменування даного вказівника, оскільки явно проводити операції з динамічним об'єктом не можна.

Друга форма оператору new виділяє пам'ять під масив заданого розміру, який складається з елементів заданого типу:

int *ptr_array = new int[4];

В цьому прикладі відбувається виділення пам'яті під масив із чотирьох елементів типу int. Така форма оператора new не дозволяє ініціалізувати елементи масиву, там будуть випадкові значення. Деяку плутанину вносить те, що обидві форми оператору new повертають однаковий вказівник. В прикладі вказівник на ціле число і на масив оголошені однаково. .Коли динамічний об'єкт більше не потрібен, мі повинні явним способом звільнити відведену під нього пам'ять. Це робиться за допомогою оператору delete, який як і new також має дві форми – для одиничної змінної і для масиву:

delete[] ptr_array;

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

Приклад:

int *pi = new int(0);
int *p_array = new int[10];
while ( *pi < 10 ) {
  p_array[*pi] = *pi;
  *pi = *pi + 1;
}
delete pi;