← Повернутися до лекцій

Лекція 8: Вказівники, Посилання та Динамічна пам'ять

Досі ми оголошували масиви так: int arr[10];. Але що, якщо ми не знаємо заздалегідь, скільки елементів нам знадобиться? Якщо ми виділимо arr[1000], а використаємо 5 — це марнотратство. Якщо виділимо arr[5], а треба 6 — програма не спрацює.

Рішення — динамічна пам'ять. Але щоб з нею працювати, спершу треба зрозуміти, що таке адреса.

1. Вказівники (Pointers)

Кожна змінна в комп'ютері живе за певною адресою в оперативній пам'яті (наприклад, 0x7ffee4). Вказівник — це змінна, яка зберігає не саме число (наприклад, 50), а адресу, де це число лежить.

Два головні оператори:

  • & (Амперсанд) — Взяття адреси. "Де ти живеш?"
  • * (Зірочка) — Розіменування. "Піди за цією адресою і дай мені те, що там лежить".

Приклад:

#include <iostream>
using namespace std;

int main() {
    int number = 42;       // Звичайна змінна
    int* ptr = &number;    // Вказівник 'ptr' тепер зберігає адресу 'number'

    cout << "Змінна: " << number << endl;       // Виведе 42
    cout << "Адреса: " << ptr << endl;          // Виведе щось типу 0x61ff0c
    cout << "Значення через адресу: " << *ptr;  // Виведе 42 (пішли за адресою)

    *ptr = 100; // Змінюємо значення за адресою
    cout << "\nНове значення number: " << number; // Виведе 100!

    return 0;
}

2. Посилання (References)

Вказівники — потужні, але небезпечні (можна випадково звернутися до чужої пам'яті). Тому в C++ додали посилання.

Посилання — це псевдонім (альтернативне ім'я) для вже існуючої змінної. Це як "Сашко" і "Олександр" — імена різні, а людина одна.

#include <iostream>
using namespace std;

int main() {
    int original = 10;
    int& ref = original; // ref — це посилання на original

    ref = 20; // Змінюємо ref, а змінюється original!
    cout << "original: " << original << endl; // Виведе 20
    
    return 0;
}

Різниця:

  • Вказівник (*) може бути пустим (nullptr) і його можна перенаправити на іншу змінну.
  • Посилання (&) має бути прив'язане одразу і назавжди.

3. Динамічна пам'ять (new та delete)

Пам'ять, яку ми використовували раніше (Стек), очищається автоматично. Динамічна пам'ять (Куча / Heap) — це "ручне керування".

  • new — виділяє пам'ять і повертає адресу (вказівник).
  • delete — звільняє пам'ять.

⚠️ Золоте правило C++:

На кожен new має бути свій delete. Якщо ви не звільните пам'ять, вона залишиться зайнятою до перезавантаження програми. Це називається витік пам'яті (memory leak).

Робота з однією змінною:

#include <iostream>
using namespace std;

int main() {
    int* p = new int; // Просимо систему виділити місце під одне ціле число
    *p = 55;          // Записуємо туди значення
    cout << *p;       // Користуємося
    delete p;         // Повертаємо пам'ять системі!
    p = nullptr;      // Хороший тон: занулити вказівник, щоб випадково не використати його знову
    
    return 0;
}

4. Динамічні масиви

Це найкорисніша частина. Ми можемо створити масив будь-якої довжини прямо під час виконання програми.

Синтаксис: тип* ім'я = new тип[розмір];

Звільнення масиву вимагає квадратних дужок: delete[] ім'я;.

Приклад: Створення списку студентів

#include <iostream>
using namespace std;

int main() {
    int size;
    cout << "Скільки студентів у групі? ";
    cin >> size;

    // Виділяємо пам'ять рівно під потрібну кількість
    int* grades = new int[size]; 

    // Працюємо як зі звичайним масивом
    for (int i = 0; i < size; i++) {
        cout << "Оцінка студента " << i + 1 << ": ";
        cin >> grades[i];
    }

    cout << "Перша оцінка: " << grades[0] << endl;

    // ОБОВ'ЯЗКОВО звільняємо пам'ять
    delete[] grades; 
    
    return 0;
}

Практичні завдання до Лекції 8

Виконайте ці завдання в своєму середовищі розробки.

Завдання 1: Магія вказівників

  1. Створіть три змінні: a, b, c (цілі числа).
  2. Створіть вказівник ptr.
  3. За допомогою ptr по черзі змініть значення кожної змінної на 10, 20 та 30.
  4. Направте ptr на a, змініть значення через *ptr.
  5. Потім направте ptr на b... і так далі.
  6. Виведіть a, b, c, щоб переконатися, що вони змінилися.

Завдання 2: Динамічний розворот

  1. Запитайте у користувача: "Якої довжини масив створити?".
  2. Створіть динамічний масив (new) введеного розміру.
  3. Заповніть його числами, які вводить користувач.
  4. Виведіть цей масив у зворотному порядку.
  5. Не забудьте очистити пам'ять (delete[]).

Завдання 3: Парні та Непарні (Складніше)

  1. Створіть динамічний масив на 10 випадкових чисел.
  2. Порахуйте, скільки там парних і скільки непарних чисел.
  3. Створіть два нових динамічних масиви:
    • Один розміром точно під кількість парних чисел.
    • Другий — під кількість непарних.
  4. Перепишіть числа з головного масиву у відповідні маленькі масиви.
  5. Виведіть обидва нові масиви.
  6. Очистіть пам'ять для всіх трьох масивів.