Klasa void pointer, znany również jako wskaźnik ogólny, to specjalny typ wskaźnika, który można wskazać na obiekty zawierające dowolne dane wpisz! Wskaźnik void deklaruje się jak zwykły wskaźnik, używając słowa kluczowego void jako typu wskaźnika:
void* ptr {}; // ptr jest wskaźnikiem pustkiWskaźnik void może wskazywać na obiekty dowolnego typu danych:
int nValue {};
float fValue {};
struct Something
{
int n;
float f;
};
Something sValue {};
void* ptr {};
ptr = &nValue; // valid
ptr = &fValue; // valid
ptr = &sValue; // validJednakże, ponieważ wskaźnik void nie wie, na jaki typ obiektu wskazuje, dereferencja wskaźnika void jest niedozwolona. Zamiast tego wskaźnik void musi najpierw zostać rzutowany na inny typ wskaźnika, zanim będzie można wykonać dereferencję.
int value{ 5 };
void* voidPtr{ &value };
// std::cout << *voidPtr << '\n'; // nielegalne: dereferencja wskaźnika pustki
int* intPtr{ static_cast<int*>(voidPtr) }; // jednak jeśli rzucimy nasz wskaźnik void na wskaźnik int...
std::cout << *intPtr << '\n'; // wtedy możemy wyrejestrować wynikWypisuje:
5
Następne oczywiste pytanie brzmi: Jeśli wskaźnik void nie wie, na co wskazuje, skąd mamy wiedzieć, na co go rzucić? Ostatecznie to zależy od Ciebie.
Oto przykład używanego wskaźnika pustki:
#include <cassert>
#include <iostream>
enum class Type
{
tInt, // uwaga: nie możemy tutaj użyć „int”, ponieważ jest to słowo kluczowe, więc zamiast tego użyjemy „tInt”
tFloat,
tCString
};
void printValue(void* ptr, Type type)
{
switch (type)
{
case Type::tInt:
std::cout << *static_cast<int*>(ptr) << '\n'; // rzutuj na int wskaźnik i wykonaj pośrednictwo
break;
case Type::tFloat:
std::cout << *static_cast<float*>(ptr) << '\n'; // rzut na float wskaźnik i wykonaj pośredniość
break;
case Type::tCString:
std::cout << static_cast<char*>(ptr) << '\n'; // rzut na wskaźnik char (bez pośredniości)
// std::cout będzie traktował znak* jako ciąg w stylu C
// jeśli mielibyśmy wykonać pośrednie przez wyniku, to po prostu wypiszemy pojedynczy znak, na który wskazuje ptr
break;
default:
std::cerr << "printValue(): invalid type provided\n";
assert(false && "type not found");
break;
}
}
int main()
{
int nValue{ 5 };
float fValue{ 7.5f };
char szValue[]{ "Mollie" };
printValue(&nValue, Type::tInt);
printValue(&fValue, Type::tFloat);
printValue(szValue, Type::tCString);
return 0;
}Ten program wypisuje:
5 7.5 Mollie
Różne wskaźniki pustki
Wskaźniki pustki można ustawić na wartość null:
void* ptr{ nullptr }; // ptr jest wskaźnikiem pustki, który obecnie jest wskaźnikiem zerowymPonieważ wskaźnik pustki nie wie, jaki typ obiektu wskazuje, usunięcie wskaźnika pustki spowoduje niezdefiniowane zachowanie. Jeśli chcesz usunąć wskaźnik pustki, static_cast przywróć najpierw odpowiedni typ.
Nie jest możliwe wykonanie arytmetyki wskaźnika na wskaźniku pustki. Dzieje się tak, ponieważ arytmetyka wskaźników wymaga, aby wskaźnik wiedział, jaki rozmiar obiektu wskazuje, dzięki czemu może odpowiednio zwiększać lub zmniejszać wskaźnik.
Zauważ, że nie ma czegoś takiego jak odwołanie do pustki. Dzieje się tak dlatego, że odwołanie do void byłoby typu void & i nie wiedziałoby, do jakiego typu wartości się odwołuje.
Wnioski
Ogólnie rzecz biorąc, dobrym pomysłem jest unikanie używania wskaźników void, chyba że jest to absolutnie konieczne, ponieważ skutecznie pozwalają one uniknąć sprawdzania typu. Dzięki temu możesz niechcący zrobić rzeczy, które nie mają sensu, a kompilator nie będzie na to narzekał. Na przykład poprawne byłoby następujące działanie:
int nValue{ 5 };
printValue(&nValue, Type::tCString);Ale kto wie, jaki byłby faktycznie wynik!
Chociaż powyższa funkcja wydaje się dobrym sposobem na sprawienie, aby pojedyncza funkcja obsługiwała wiele typów danych, w rzeczywistości C++ oferuje znacznie lepszy sposób na zrobienie tego samego (poprzez przeciążenie funkcji), który zachowuje sprawdzanie typu, aby zapobiec nadużyciom. W wielu innych miejscach, w których kiedyś używano wskaźników void do obsługi wielu typów danych, obecnie lepiej jest to zrobić przy użyciu szablonów, które również oferują silne sprawdzanie typu.
Jednak bardzo rzadko wskaźnik void może nadal znaleźć rozsądne zastosowanie. Tylko upewnij się, że nie ma lepszego (bezpieczniejszego) sposobu na wykonanie tego samego, używając najpierw innych mechanizmów językowych!
Czas quizu
Pytanie nr 1
Jaka jest różnica między wskaźnikiem void a wskaźnikiem zerowym?

