Вказівник на вказівник — це саме те, про що ви подумали: вказівник, який містить адресу іншого вказівника.
Вказівники на вказівники
Звичайний вказівник типу int оголошується з використанням однієї зірочки:
|
1 |
int *ptr; // вказівник типу int, одна зірочка |
Вказівник на вказівник типу int оголошується з використанням двох зірочок:
|
1 |
int **ptrptr; // вказівник на вказівник типу int (дві зірочки) |
Вказівник на вказівник працює як звичайний вказівник: ви можете його розіменувати для отримання значення, на яке він вказує. І, оскільки цим значенням є інший вказівник, для отримання початкового значення вам потрібно буде виконати операцію розіменування ще раз. Їх слід виконувати послідовно:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> int main() { int value = 7; int *ptr = &value; std::cout << *ptr << std::endl; // розіменовуємо вказівник, щоб отримати значення типу int int **ptrptr = &ptr; std::cout << **ptrptr << std::endl; return 0; } |
Результат виконання програми:
7
7
Зверніть увагу, ви не можете ініціалізувати вказівник на вказівник безпосередньо значенням:
|
1 2 |
int value = 7; int **ptrptr = &&value; // заборонено |
Це пов’язано з тим, що оператор адреси (&) вимагає l-value, але &value — це r-value. Однак, вказівнику на вказівник можна вказати значення nullptr:
|
1 |
int **ptrptr = nullptr; // використовуйте 0, якщо не підтримується C++11 |
Масиви вказівників
Вказівники на вказівники мають кілька застосувань. Найбільш використовуваним є динамічне виділення масиву вказівників:
|
1 |
int **array = new int*[20]; // виділяємо масив із 20 вказівників типу int |
Це звичайний динамічно виділений масив, за винятком того, що його елементами є вказівники на тип int, а не значення типу int.
Двовимірні динамічно виділені масиви
Іншим поширеним застосуванням вказівників на вказівники є динамічне виділення багатовимірних масивів. На відміну від двовимірного фіксованого масиву, який можна легко оголосити наступним чином:
|
1 |
int array[15][7]; |
Динамічне виділення двовимірного масиву трохи відрізняється. У вас може виникнути спокуса написати щось на кшталт такого:
|
1 |
int **array = new int[15][7]; // не працюватиме! |
Тут ви отримаєте помилку. Є два можливих рішення. Якщо правий індекс є константою типу compile-time, то ви можете зробити наступне:
|
1 |
int (*array)[7] = new int[15][7]; |
Дужки тут потрібні для дотримання пріоритету. У C++11 хорошою ідеєю буде використовувати ключове слово auto для автоматичного визначення типу даних:
|
1 |
auto array = new int[15][7]; // набагато простіше! |
На жаль, це відносно просте рішення не працює, якщо правий індекс не є константою типу compile-time. В такому випадку все трохи ускладнюється. Спочатку ми виділяємо масив вказівників (як у вищенаведеному прикладі), а потім перебираємо кожен елемент масиву вказівників і виділяємо динамічний масив для кожного елементу цього масиву. В підсумку, наш динамічний двовимірний масив — це динамічний одновимірний масив динамічних одновимірних масивів!
|
1 2 3 |
int **array = new int*[15]; // виділяємо масив із 15 вказівників типу int — це наші рядки for (int count = 0; count < 15; ++count) array[count] = new int[7]; // а це наші стовпці |
Доступ до елементів масиву виконується як зазвичай:
|
1 |
array[8][3] = 4; // це те ж саме, що і (array[8])[3] = 4; |
Цим методом, оскільки кожен стовпець масиву незалежно динамічно виділяється, можна зробити динамічно виділені двовимірні масиви, які не є прямокутними. Наприклад, ми можемо створити масив трикутної форми:
|
1 2 3 |
int **array = new int*[15]; // виділяємо масив із 15 вказівників типу int — це наші рядки for (int count = 0; count < 15; ++count) array[count] = new int[count+1]; // а це наші стовпці |
У вищенаведеному прикладі, array[0] — це масив довжиною 1, а array[1] — масив довжиною 2 і т.д.
Для звільнення пам’яті динамічно виділеного двовимірного масиву (який створювався за допомогою цього способу) також потрібен буде цикл:
|
1 2 3 |
for (int count = 0; count < 15; ++count) delete[] array[count]; delete[] array; // це слід виконувати в кінці |
Зверніть увагу, ми видаляємо масив в порядку, протилежному його створення. Якщо ми видалимо масив перед видаленням елементів масиву, то нам доведеться отримувати доступ до звільненої пам’яті для видалення елементів масиву. А це, в свою чергу, призведе до несподіваних результатів.
Оскільки процес виділення і звільнення двовимірних масивів є трохи заплутаним (можна легко наробити помилок), то часто простіше «сплюснути» двовимірний масив в одновимірний масив:
|
1 2 3 4 5 6 7 |
// Замість наступного: int **array = new int*[15]; // виділяємо масив із 15 вказівників типу int — це наші рядки for (int count = 0; count < 15; ++count) array[count] = new int[7]; // а це наші стовпці // Робимо наступне: int *array = new int[105]; // двовимірний масив 15x7 "сплющений" в одновимірний масив |
Проста математика використовується для конвертації індексів рядка і стовпця прямокутного двовимірного масиву в один індекс одновимірного масиву:
|
1 2 3 4 5 6 7 |
int getSingleIndex(int row, int col, int numberOfColumnsInArray) { return (row * numberOfColumnsInArray) + col; } // Присвоюємо array[9,4] значення 3, використовуючи наш "сплющений" масив array[getSingleIndex(9, 4, 5)] = 3; |
Вказівник на вказівник на вказівник і т.д.
Також можна оголосити вказівник на вказівник на вказівник:
|
1 |
int ***ptrx3; |
Вони можуть використовуватися для динамічного виділення тривимірного масиву. Тим не менш, для цього потрібен буде цикл всередині циклу і надзвичайна акуратність і обережність, щоб не наробити помилок. Ви навіть можете оголосити вказівник на вказівник на вказівник на вказівник:
|
1 |
int ****ptrx4; |
Або зробити ще більшу вкладеність, якщо захочете. Однак на практиці такі вказівники рідко коли використовуються.
Висновки
Рекомендується застосовувати вказівники на вказівники тільки в крайніх випадках, так як вони складні у використанні і потенційно небезпечні. Досить легко розіменувати нульовий або висячий вказівник в ситуаціях з використанням звичайних вказівників, вдвічі легше це зробити в ситуаціях з вказівником на вказівник, оскільки для отримання початкового значення потрібно виконати подвійне розіменування!

В цьому прикладі є така фраза Присвоюємо array[9,4] значення 3, використовуючи наш “сплющений” масив
Тобто чіткий натяк на сплющений масив 105 елементів, що був отриманий з двохвимірного масиву 15×7. А далі іде опис підходу, який дозволяє адресуватись до одномірного масиву в 105 елементів як до двохмірного 15×7 за допомогою getSingleIndex(…).
Скоріше за все просто тут помилка в кількості колонок в масиві, третій вхідний параметр функції має бути 7.