Funkcja może wykonać obliczenia, zmienić stan obiektu albo wypisać komunikat, nie zwracając żadnego wyniku. Właśnie do takich sytuacji służy void w C++, ale to słowo kluczowe ma więcej niż jedno zastosowanie. Wyjaśnię, jak działają funkcje typu void, czym różni się void* od zwykłego wskaźnika, jakie błędy popełniają początkujący oraz kiedy we współczesnym C++ lepiej użyć bezpieczniejszej alternatywy.
Najważniejsze informacje o void w C++ w jednym miejscu
- Funkcja void wykonuje operacje, ale nie zwraca wartości do miejsca wywołania.
- W takiej funkcji można użyć return;, lecz nie
return 5;. - void* jest wskaźnikiem bez określonego typu danych i wymaga rzutowania przed odczytem.
- void nie jest tym samym co nullptr, pusty wskaźnik ani wartość zero.
- W standardowym C++ funkcja main() powinna zwracać
int, a nievoid.
Co oznacza void w języku C++
void oznacza brak konkretnej wartości albo brak określonego typu. Najczęściej spotkasz je jako typ zwracany przez funkcję, ale może też wystąpić w zapisie wskaźnika, na przykład void*. To ważne rozróżnienie, bo funkcja void i wskaźnik void rozwiązują zupełnie inne problemy.
Typ void jest typem niekompletnym. Nie można utworzyć zwykłej zmiennej void, pobrać rozmiaru takiej zmiennej przez sizeof ani bezpośrednio odczytać jej wartości. Kompilator wie natomiast, że funkcja lub wskaźnik korzysta z konstrukcji, dla której nie ma konkretnego typu danych.
void komunikat()
{
std::cout << "Operacja zakonczona";
}
W tym przykładzie funkcja wykonuje efekt uboczny, czyli wyświetla tekst, ale nie przekazuje wyniku wywołującemu. Z mojego doświadczenia wynika, że początkujący często próbują traktować void jak specjalną wartość. Lepiej myśleć o nim jako o informacji, że wartości do odebrania po prostu nie ma.
Jak działają funkcje zwracające void
Funkcję typu void wywołuje się tak samo jak każdą inną funkcję. Różnica polega na tym, że nie można przypisać jej wyniku do zmiennej ani wykorzystać go w obliczeniu.
#include
void wyswietlSume(int a, int b)
{
std::cout << a + b << '\n';
}
int main()
{
wyswietlSume(4, 6);
}
Poprawne jest samo wywołanie wyswietlSume(4, 6);. Taki zapis byłby błędny:
int wynik = wyswietlSume(4, 6);
Nie istnieje bowiem wartość, którą można przypisać do zmiennej wynik. Jeśli funkcja ma zarówno wykonać operację, jak i przekazać rezultat, trzeba nadać jej konkretny typ, na przykład int, double albo własny typ strukturalny.
Instrukcja return w funkcji void
W funkcji zwracającej void można użyć instrukcji return; bez wyrażenia. Służy ona do wcześniejszego zakończenia działania funkcji, na przykład po wykryciu nieprawidłowych danych.
void pokazWynik(int wynik)
{
if (wynik < 0)
{
std::cout << "Brak wyniku\n";
return;
}
std::cout << "Wynik: " << wynik << '\n';
}
Nie wolno natomiast pisać return 10; w funkcji typu void. Jeżeli potrzebujesz zwrócić informację o powodzeniu operacji, rozważ bool, kod błędu, wyjątek albo typ wynikowy, zależnie od sytuacji. Samo void dobrze opisuje operacje, których rezultatem jest działanie, a nie wartość.
Parametr void i pusta lista argumentów
W C++ funkcję bez parametrów zapisuje się jako void funkcja(). Dawny zapis void funkcja(void) również może się pojawić, szczególnie w kodzie odziedziczonym po języku C, ale we współczesnym C++ najczytelniejsze jest puste ().
Nie należy mylić tego zapisu z funkcją, która przyjmuje argument typu void. Taki argument nie ma praktycznego sensu. Nawiasy informują tutaj po prostu, że funkcja nie przyjmuje żadnych parametrów.
Void pointer czyli wskaźnik void*
void* to wskaźnik, który może przechowywać adres obiektu bez deklarowania jego konkretnego typu. Dzięki temu może wskazywać zarówno na int, jak i na double czy obiekt własnej klasy. Ta elastyczność ma jednak cenę, ponieważ kompilator nie wie, jak interpretować dane pod wskazanym adresem.
int liczba = 42;
void* adres = &liczba;
int* wskaznikNaLiczbe = static_cast(adres);
std::cout << *wskaznikNaLiczbe;
Najpierw przechowujemy adres liczby w void*, a później jawnie konwertujemy go z powrotem do int*. Dopiero po takim rzutowaniu można bezpiecznie użyć operatora dereferencji. Jeśli wybierzesz zły typ podczas rzutowania, program może odczytać pamięć w błędny sposób, a problem często ujawni się dopiero w czasie działania.
Ograniczenia wskaźnika void*
Wskaźnik void* nie może być bezpośrednio dereferencjonowany, bo kompilator nie zna rozmiaru ani struktury danych. Nie można też wykonywać na nim typowej arytmetyki wskaźników, takiej jak adres + 1, ponieważ nie wiadomo, o ile bajtów przesunąć adres.
W praktycznym kodzie trzeba pamiętać o kilku zasadach:
-
Nie odczytuj
void*bez wcześniejszego rzutowania na właściwy typ. - Nie zakładaj, że wskaźnik wskazuje na poprawny obiekt tylko dlatego, że nie jest równy
nullptr. - Uważaj na czas życia obiektu, ponieważ sam adres nie przedłuża jego istnienia.
- Do funkcji wskaźnikowych używaj określonych typów funkcji, a nie
void*.
Wskaźniki bez typu były popularne w interfejsach C, bibliotekach systemowych i kodzie niskopoziomowym. W nowym C++ sięgam po nie ostrożnie. Jeśli interfejs można zaprojektować za pomocą szablonu, std::variant, std::any albo konkretnej klasy bazowej, zwykle otrzymujemy czytelniejszy i bezpieczniejszy kod.
Void a nullptr, NULL i typy zwracane przez funkcję
Jednym z częstszych nieporozumień jest utożsamianie void z pustym wskaźnikiem. To różne pojęcia. void opisuje brak typu lub brak wartości zwracanej, natomiast nullptr oznacza, że wskaźnik nie wskazuje na żaden obiekt.
| Konstrukcja | Znaczenie | Przykład |
|---|---|---|
void |
Brak konkretnej wartości lub typu | void zapisz(); |
void* |
Wskaźnik na obiekt bez znanego typu | void* dane; |
nullptr |
Jawna wartość pustego wskaźnika | int* p = nullptr; |
NULL |
Starszy zapis pustego wskaźnika | int* p = NULL; |
W kodzie korzystającym ze standardu C++11 lub nowszego preferuję nullptr zamiast NULL i zera. Jest jednoznaczny typowo i nie powoduje części niejasności przy przeciążaniu funkcji.
void wybierz(int);
void wybierz(int*);
wybierz(nullptr);
W tym przypadku kompilator wie, że chodzi o przeciążenie przyjmujące wskaźnik. Zapis wybierz(0) może prowadzić do innego wyboru lub niejednoznaczności, ponieważ zero jest również liczbą całkowitą. To drobna różnica w składni, ale w większym projekcie potrafi oszczędzić sporo czasu podczas diagnozowania błędu.
Najczęstsze błędy związane z void w C++
Próba zwrócenia wartości
Najprostszy błąd wygląda tak:
void pobierzLiczbe()
{
return 7;
}
Jeśli funkcja ma zwracać liczbę, jej deklaracja powinna zawierać odpowiedni typ:
int pobierzLiczbe()
{
return 7;
}
Użycie void main()
W standardowym C++ funkcja main powinna zwracać int. Poprawna wersja to int main() albo int main(int argc, char* argv[]). Zwrócenie zera informuje system, że program zakończył się prawidłowo, a inna wartość może sygnalizować błąd.
int main()
{
return 0;
}
Niektóre środowiska mogą akceptować void main() jako rozszerzenie, ale nie jest to przenośny, standardowy C++. W ćwiczeniach i projektach, które mają działać na różnych kompilatorach, taka oszczędność nie ma sensu.
Przeczytaj również: Zapis pliku w C# - Wybierz najlepszą metodę (WriteAllText, JSON)
Rzutowanie dowolnego adresu bez sprawdzenia typu
Sam fakt, że da się przekonwertować void* na int*, nie oznacza, że pod adresem znajduje się liczba typu int. Błędne założenie może prowadzić do niezdefiniowanego działania, uszkodzenia danych albo trudnych do odtworzenia awarii.
Jeżeli użycie void* jest konieczne, warto przekazywać razem z adresem informację o typie, rozmiarze lub sposobie obsługi danych. Jeszcze lepiej ograniczyć taki interfejs do małego fragmentu kodu, zamiast rozprowadzać wskaźnik bez typu po całej aplikacji.
Kiedy używać void, a kiedy wybrać inną konstrukcję
Typ zwracany void dobrze pasuje do funkcji, która ma wykonać polecenie, zmienić obiekt albo wysłać dane do określonego miejsca. Przykładami są zapis pliku, wyświetlenie komunikatu, aktualizacja stanu formularza lub uruchomienie procedury obsługi zdarzenia.
Nie używałbym go jednak wtedy, gdy wywołujący musi wiedzieć, czy operacja się udała. W takiej sytuacji lepszy może być bool, std::optional, std::expected albo wyjątek. Wybór zależy od tego, czy brak wyniku jest normalną cechą operacji, czy ukrywa ważną informację o jej przebiegu.
| Potrzeba | Lepszy wybór | Dlaczego |
|---|---|---|
| Operacja bez wyniku | void |
Wywołujący nie musi niczego odbierać |
| Prosta informacja powodzenie lub błąd | bool |
Wynik jest łatwy do sprawdzenia |
| Wynik może nie istnieć | std::optional |
Brak wartości jest jawnie opisany w typie |
| Wynik albo informacja o błędzie | std::expected |
Typ przechowuje rezultat lub przyczynę niepowodzenia |
| Dane różnych typów | Szablon, std::variant lub std::any
|
Ogranicza ryzyko błędnego rzutowania |
W aplikacjach webowych i narzędziach backendowych taka decyzja ma praktyczne znaczenie. Funkcja zapisująca konfigurację może być typu void, jeśli błąd kończy działanie przez wyjątek. Jeżeli jednak kod wyżej ma sam zdecydować, co zrobić z błędem, zwrócenie bool lub obiektu opisującego rezultat będzie zwykle bardziej przejrzyste.
Jak zapamiętać różnicę między void i void*
Najprostsza reguła jest taka: void mówi o braku wartości, a void* przechowuje adres. Pierwsze użycie spotkasz głównie przy funkcjach. Drugie dotyczy zarządzania adresami obiektów, gdy konkretny typ nie jest znany w miejscu deklaracji.
Można zapamiętać to na dwóch krótkich przykładach:
void uruchom(); // funkcja nie zwraca wartości
void* dane = nullptr; // wskaźnik nie wskazuje obecnie na obiekt
Jeśli dopiero uczysz się C++, zacznij od funkcji zwracających void, bo są naturalnym elementem codziennego kodu. Do void* wracaj wtedy, gdy naprawdę pracujesz z niskopoziomowym interfejsem, biblioteką C albo mechanizmem, którego nie da się sensownie opisać typami C++.
Od prostego void do świadomego projektowania funkcji
Najważniejsza lekcja nie dotyczy samej składni. void warto stosować wtedy, gdy brak wartości jest czytelną informacją projektową, a nie sposobem na ukrycie tego, co funkcja faktycznie robi.
Wskaźniki void* nadal mają swoje miejsce w programowaniu systemowym, lecz w typowym nowym kodzie powinny być wyjątkiem. Im dokładniej opiszesz typ danych i rezultat operacji, tym więcej błędów wykryje kompilator, zanim program trafi do użytkownika.
Jeżeli zapamiętasz trzy rzeczy, niech będą to te: funkcja void nie zwraca wyniku, void* wymaga ostrożnego rzutowania, a nullptr oznacza pusty wskaźnik i nie ma związku z typem void. To wystarczy, by poprawnie czytać większość podstawowych przykładów i podejmować rozsądne decyzje w dalszej nauce C++.