Void w C++ - funkcje, void* i najczęstsze błędy

Błąd kompilatora C++: błąd AddressSanitizer: użycie pamięci stosu poza zakresem.

Autor

Tymoteusz Sobczak

Opublikowano

23 paź 2026

Spis treści

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 nie void.

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++.

FAQ - Najczęstsze pytania

Tak, ale tylko w postaci return;, aby wcześniej zakończyć działanie funkcji. Zapis return 5; jest błędny, ponieważ funkcja typu void nie zwraca wartości.

void* przechowuje adres obiektu bez określenia jego typu. Przed dereferencją trzeba jawnie rzutować go na właściwy typ, na przykład z void* na int*; błędne rzutowanie może prowadzić do niezdefiniowanego działania.

W standardowym C++ funkcja main() powinna zwracać int, czyli mieć postać int main(). Zwrócenie zera oznacza prawidłowe zakończenie programu, a inna wartość może sygnalizować błąd.

void pasuje do operacji, których efektem jest samo działanie, na przykład zapis pliku lub zmiana stanu obiektu. Jeśli wywołujący musi poznać powodzenie operacji, wybierz bool; gdy wynik może nie istnieć, std::optional; a gdy trzeba zwrócić rezultat lub informację o błędzie, std::expected.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi:

wskaźniki void nullptr rzutowanie

Udostępnij artykuł

Tymoteusz Sobczak

Tymoteusz Sobczak

Nazywam się Tymoteusz Sobczak i od 7 lat zajmuję się programowaniem webowym. Moje zainteresowanie tą dziedziną zaczęło się od prostych projektów, które realizowałem w wolnym czasie. Z czasem odkryłem, jak fascynujące jest tworzenie aplikacji, które mogą ułatwiać życie innym. Chętnie dzielę się swoją wiedzą, pomagając czytelnikom zrozumieć złożone zagadnienia związane z programowaniem, od podstawowych technik po bardziej zaawansowane rozwiązania. Pisząc na temat programowania, staram się dostarczać informacje, które są nie tylko użyteczne, ale także zrozumiałe. Zawsze dokładam starań, aby moje źródła były wiarygodne, a treści aktualne. Lubię upraszczać trudne tematy i organizować wiedzę w sposób, który ułatwia naukę. Wierzę, że każdy, kto ma chęci, może stać się dobrym programistą, a ja jestem tutaj, aby wskazać drogę.

Napisz komentarz