Вказівник типу void (або «загальний вказівник») — це спеціальний тип вказівника, який може вказувати на об’єкти будь-якого типу даних! Оголошується він як звичайний вказівник, тільки замість типу даних використовується ключове слово void:
1 |
void *ptr; // ptr - це вказівник типу void |
Вказівник типу void може вказувати на об’єкти будь-якого типу даних:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
int nResult; float fResult; struct Something { int n; float f; }; Something sResult; void *ptr; ptr = &nResult; // допускається ptr = &fResult; // допускається ptr = &sResult; // допускається |
Оскільки вказівник типу void сам не знає, на об’єкт якого типу він буде вказувати, то розіменувати його напряму не вийде! Вам спочатку потрібно буде явно перетворити вказівник типу void за допомогою оператора static_cast в інший тип даних, а потім вже виконати операцію розіменування:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> int main() { int value = 7; void *voidPtr = &value; //std::cout << *voidPtr << std::endl; // заборонено: не можна розіменовувати вказівники типу void int *intPtr = static_cast<int*>(voidPtr); // проте, якщо ми конвертуємо наш вказівник типу void у вказівник типу int, std::cout << *intPtr << std::endl; // то ми зможемо його розіменувати, наче це звичайний вказівник return 0; } |
Результат виконання програми:
7
Виникає питання: «Якщо вказівник типу void сам не знає, на що він вказує, то як ми тоді можемо знати, в який тип даних його слід явно конвертувати за допомогою оператора static_cast?». Ніяк ми не можемо знати, це вже залишається на ваш власний розсуд. Вам самим доведеться вибрати потрібний тип даних. Наприклад:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#include <iostream> enum Type { INT, DOUBLE, CSTRING }; void printValue(void *ptr, Type type) { switch (type) { case INT: std::cout << *static_cast<int*>(ptr) << '\n'; // конвертуємо у вказівник типу int і виконуємо розіменування break; case DOUBLE: std::cout << *static_cast<double*>(ptr) << '\n'; // конвертуємо у вказівник типу double і виконуємо розіменування break; case CSTRING: std::cout << static_cast<char*>(ptr) << '\n'; // конвертуємо у вказівник типу char (без розіменування) // std::cout знає, що char* слід обробляти як рядок C-style. // Якщо б ми розіменували результат (цілий вираз), то тоді вивівся би просто перший символ з масиву букв, на який вказує ptr break; } } int main() { int nValue = 7; double dValue = 9.3; char szValue[] = "Jackie"; printValue(&nValue, INT); printValue(&dValue, DOUBLE); printValue(szValue, CSTRING); return 0; } |
Результат виконання програми:
7
9.3
Jackie
Вказівникам типу void можна присвоїти нульове значення:
1 |
void *ptr = 0; // ptr - це вказівник типу void, який зараз є нульовим |
Хоча деякі компілятори дозволяють видаляти вказівники типу void, які вказують на динамічно виділену пам’ять, робити це не рекомендується, так як результати можуть бути несподіваними.
Також не вийде виконати адресну арифметику з вказівниками типу void, так як для цього потрібно, щоб вказівник знав розмір об’єкта, на який він вказує (для виконання коректного інкременту/декременту). Також немає такого поняття, як посилання на void.
Висновки
Загалом, використовувати вказівники типу void рекомендується тільки в крайніх випадках, коли без цього не обійтися, так як з їх використанням перевірку типів даних ні вам, ні компілятору виконати не вдасться. А це, в свою чергу, дозволить вам випадково зробити те, що не має сенсу, і компілятор на це скаржитися не буде. Наприклад:
1 2 |
int nResult = 7; printResult(&nResult, CSTRING); |
Тут компілятор промовчить. Але що буде в результаті? Незрозуміло!
Хоча код, наведений вище, здається акуратним способом змусити одну функцію обробляти кілька типів даних, в мові C++ є набагато кращий спосіб зробити те ж саме (через перевантаження функцій), в якому зберігається перевірка типів для запобігання неправильного використання. Також для обробки декількох типів даних можна використовувати шаблони, які забезпечують хорошу перевірку типів (але про це вже поговоримо на наступних уроках).
Якщо вам все-таки доведеться використовувати вказівник типу void, то переконайтеся, що немає кращого (більш безпечного) способу зробити те ж саме, але з використанням інших механізмів мови C++!
Тест
У чому різниця між нульовим вказівником і вказівником типу void?
Відповідь
Вказівник типу void — це вказівник, який може вказувати на об’єкт будь-якого типу даних, але він сам не знає, який це буде тип. Для розіменування вказівника типу void, його потрібно явно конвертувати за допомогою оператора static_cast в інший тип даних. Нульовий вказівник — це вказівник, який не вказує на адресу. Вказівник типу void може бути нульовим вказівником.