Як і багато інших мов, C++ має вказівники. Ми вже знаємо, що посилання та вказівники схожі, але сприймаймо їх як ще один рівень ближче до внутрішньої будови компʼютера. Вказівники - це змінні, які зберігають адреси обʼєктів. Їх використовують, щоб безпосередньо взаємодіяти з обʼєктами, що уможливлює динамічне виділення памʼяті та ефективну роботу з даними в C++.
Якщо ми тільки починаємо знайомитися зі вказівниками, вони можуть здаватися трохи загадковими, але щойно ми до них звикнемо, вони стануть цілком зрозумілими.
Це надзвичайно важлива частина C++, тож приділімо їм трохи часу, щоб справді їх зрозуміти. Найпростіший варіант, про який ідеться в цьому концепті, також називають дурним вказівником або сирим вказівником. У сучасному C++ є також розумні вказівники, а базовий тип зовсім не розумний, і всю роботу доводиться виконувати вручну.
Перш ніж заглиблюватися в деталі, варто зрозуміти, для чого потрібні вказівники. Вказівники - це спосіб поділитися адресою обʼєкта з іншими частинами нашої програми, і це корисно з трьох основних причин:
Оголошення вказівника в C++ передбачає вказівку типу даних, на який вказує вказівник, а потім зірочку (*) та імʼя вказівника.
Коли вказівники оголошують, їх не ініціалізують автоматично.
Без явного присвоєння вказівник зазвичай містить невизначене значення, яке часто називають «сміттєвою адресою».
Хоча деякі компілятори можуть ініціалізувати вказівники значенням nullptr, така поведінка не гарантована для всіх компіляторів, тож покладатися на неї не можна.
Найкраща практика - явно ініціалізувати сирі вказівники й перевіряти, що вони не є нульовими, перш ніж ними користуватися, щоб уникнути можливих проблем.
int* ptr{nullptr}; // Declares a pointer and makes sure it is not invalid
Щоб присвоїти вказівнику адресу змінної, використовують оператор взяття адреси (&).
Розіменування вказівника виконують за допомогою оператора розіменування (*).
std::string opponent{"Solomon Lane"};
// 'ethan' points to the address of the string opponent
std::string* ethan{&opponent};
// Instead of ethan's, the opponent's name address is given to the passPort
std::string passportName{*ethan};
Звернімо увагу: розіменування потрібно виконувати явно, тоді як посилання просто працюють як псевдонім.
Арифметика вказівників дає змогу виконувати арифметичні операції над вказівниками, що особливо корисно під час роботи з масивами. Додавання цілого числа до вказівника змушує його вказувати на інший елемент.
// Stargate Coordinate Code
int gateCode[] = {462, 753, 218, 611, 977};
// 'ptr' points to the first element of 'gateCode'
int* ptr{&gateCode[0]};
// Accesses the third Stargate address through pointer arithmetic
int dialedAddress{*(ptr + 2)};
// Chevron encoded! Dialing Stargate address:
openStarGate(dialedAddress);
Арифметика вказівників у C++ може легко призвести до невизначеної поведінки, якщо поводитися з нею необережно. Невизначена поведінка може проявлятися як несподівані результати роботи програми, збої чи навіть уразливості безпеки. Один сумнозвісний приклад наслідків невизначеної поведінки стався 1996 року: вибух ракети «Аріан-5», де програмний виняток, спричинений перетворенням 64-бітного числа з плаваючою комою на 16-бітне ціле число зі знаком, призвів до катастрофічної відмови.
У C++ оператор -> використовують для доступу до членів обʼєкта через вказівник на цей обʼєкт.
Це скорочений запис, який спрощує доступ до членів обʼєктів, на які вказують вказівники.
Наприклад, якщо ptr - вказівник на обʼєкт із змінною-членом x, то замість (*ptr).x можна одразу скористатися ptr->x.
Цей оператор підвищує читабельність коду й позбавляє зайвої багатослівності під час роботи з вказівниками на обʼєкти.
Ось короткий приклад зі struct Superhero, яка має змінну-член superpower.
Головна функція створює вказівник dianaPrince на обʼєкт Superhero (який уособлює Диво-жінку).
Оператор -> використовують для доступу до змінної-члена superpower, демонструючи культове «Lasso of Truth» Диво-жінки.
struct Superhero {
std::string superpower;
};
Superhero wonder_woman{};
Superhero* dianaPrince = &wonder_woman;
dianaPrince->superpower = "Lasso of Truth";
// Using the -> operator to access member variable superpower:
std::cout << "Wonder Woman, possesses the mighty " << dianaPrince->superpower;
// Memory cleanup:
delete dianaPrince;
І вказівники, і посилання дають змогу непрямо звертатися до обʼєктів, але вони відрізняються своїми можливостями та міркуваннями безпеки. Вказівники дають змогу змінювати обʼєкт, на який вони вказують, і їм можна присвоїти null. Однак така гнучкість породжує ризики, як-от розіменування нульових вказівників або створення висячих вказівників. Посилання ж не можуть бути нульовими й привʼязані до дійсних обʼєктів уже під час створення, що дає змогу уникнути цих ризиків. Оскільки посилання безпечніші, їм варто віддавати перевагу над вказівниками, якщо тільки не потрібні додаткові можливості, які дають вказівники.
У C++ new використовують, щоб динамічно виділяти памʼять для обʼєктів у купі під час виконання програми.
Він повертає вказівник на виділену адресу памʼяті, завдяки чому час життя обʼєктів не привʼязаний до конкретної області видимості.
Однак важливо завжди поєднувати new з delete, щоб належно звільняти памʼять.
До динамічного виділення памʼяті варто ставитися обережно, щоб не допустити поширених проблем, як-от витоків памʼяті та звернення до вже звільненої памʼяті.
Вказівнику також можна надати особливе значення під назвою нульовий вказівник, яке означає, що він не вказує на дійсну адресу. Нульові вказівники часто використовують, щоб ініціалізувати вказівники, перш ніж присвоїти їм дійсні значення.
// There is a dark mark bearer, but who is it?
std::string* darkMarkBearer{nullptr};
// It is Bellatrix!
darkMarkBearer = new std::string{"Bellatrix Lestrange"};
// She has been defeated
delete darkMarkBearer;
// Reset pointer to null, symbolizing there is no more darkMarkBearer at the moment
darkMarkBearer = nullptr;
Вказівник має або зберігати адресу дійсного обʼєкта, або бути встановленим у nullptr.
Тоді достатньо перевіряти вказівники на null і можна вважати будь-який ненульовий вказівник дійсним.
Коли обʼєкт знищено, усі вказівники на нього залишаються висячими (їх не буде автоматично встановлено в nullptr).
Наша відповідальність - виявляти такі випадки й дбати про те, щоб ці вказівники згодом було встановлено в nullptr.
У старішому коді можна натрапити на дві альтернативи nullptr.
По-перше, літерал 0 спеціально інтерпретують як нульове значення для вказівників, хоча це єдиний випадок, коли цілочисельний літерал можна присвоїти вказівнику.
По-друге, preprocessor macro NULL, успадкований із C і визначений у заголовку <cstddef>, - це ще одне представлення нульового вказівника, хоча в сучасному коді C++ його використовують рідше.