12.8 -- Wskaźniki zerowe

W poprzedniej lekcji (12.7 -- Wprowadzenie do wskaźników), omówiliśmy podstawy wskaźników, które są obiektami przechowującymi adres innego obiektu. Adres ten można wyłuskać za pomocą operatora dereferencji (*), aby uzyskać obiekt pod tym adresem:

#include <iostream>

int main()
{
    int x{ 5 };
    std::cout << x << '\n'; // print the value of variable x

    int* ptr{ &x }; // ptr holds the address of x
    std::cout << *ptr << '\n'; // use dereference operator to print the value of the object at the address that ptr is holding (which is x's address)

    return 0;
}

Powyższy przykład wypisuje:

5
5

W poprzedniej lekcji zauważyliśmy również, że wskaźniki nie muszą na nic wskazywać. W tej lekcji przyjrzymy się bliżej takim wskaźnikom (i różnym konsekwencjom wskazywania niczego).

Wskaźniki zerowe

Oprócz adresu pamięci, istnieje jeszcze jedna dodatkowa wartość, którą wskaźnik może przechowywać: wartość null. A wartość null (często skracana do null) to wartość specjalna, która oznacza, że ​​coś nie ma wartości. Gdy wskaźnik przechowuje wartość null, oznacza to, że wskaźnik na nic nie wskazuje. Taki wskaźnik nazywany jest wskaźnikiem zerowym.

Najłatwiejszym sposobem utworzenia wskaźnika zerowego jest użycie inicjalizacji wartości:

int main()
{
    int* ptr {}; // ptr is now a null pointer, and is not holding an address
 
    return 0;
}

Najlepsza praktyka

Wartość inicjuje wskaźniki (aby były wskaźnikami zerowymi), jeśli nie inicjujesz ich adresem prawidłowego obiektu.

Ponieważ możemy użyć przypisania, aby zmienić to, na co wskazuje wskaźnik, wskaźnik, który początkowo ustawione na null, można później zmienić tak, aby wskazywało prawidłowy obiekt:

#include <iostream>

int main()
{
    int* ptr {}; // ptr is a null pointer, and is not holding an address

    int x { 5 };
    ptr = &x; // ptr now pointing at object x (no longer a null pointer)

    std::cout << *ptr << '\n'; // print value of x through dereferenced ptr
 
    return 0;
}

Słowo kluczowe nullptr

Podobnie jak słowa kluczowe true i false reprezentują wartości literału logicznego, nullptr reprezentuje literał wskaźnika zerowego. Możemy użyć nullptr do jawnej inicjalizacji lub przypisania wskaźnikowi wartości null.

int main()
{
    int* ptr { nullptr }; // can use nullptr to initialize a pointer to be a null pointer

    int value { 5 };
    int* ptr2 { &value }; // ptr2 is a valid pointer
    ptr2 = nullptr; // Can assign nullptr to make the pointer a null pointer

    someFunction(nullptr); // we can also pass nullptr to a function that has a pointer parameter

    return 0;
}

W powyższym przykładzie używamy przypisania do ustawienia wartości ptr2 Do nullptr, tworząc ptr2 wartość null wskaźnik.

Najlepsza praktyka

Użyj nullptr kiedy potrzebujesz literału wskaźnika zerowego do inicjalizacji, przypisania lub przekazania wskaźnika zerowego do funkcji.

Wyłuskiwanie wskaźnika zerowego powoduje niezdefiniowane zachowanie

Podobnie jak wyłuskiwanie wskaźnika wiszącego (lub dzikiego) prowadzi do niezdefiniowanego zachowania, wyłuskiwanie wskaźnika zerowego również prowadzi do niezdefiniowanego zachowania. W większości przypadków spowoduje to awarię aplikacji.

Poniższy program ilustruje to i prawdopodobnie spowoduje awarię lub nienormalne zamknięcie aplikacji po jej uruchomieniu (śmiało, spróbuj, nie zaszkodzisz swojej maszynie):

#include <iostream>

int main()
{
    int* ptr {}; // Create a null pointer
    std::cout << *ptr << '\n'; // Dereference the null pointer

    return 0;
}

Pod względem koncepcyjnym ma to sens. Dereferencja wskaźnika oznacza „przejdź do adresu, na który wskazuje wskaźnik i uzyskaj dostęp do znajdującej się tam wartości”. Wskaźnik zerowy przechowuje wartość zerową, co semantycznie oznacza, że ​​wskaźnik na nic nie wskazuje. Zatem do jakiej wartości miałby dostęp?

Przypadkowe wyłuskiwanie wskaźników zerowych i wiszących jest jednym z najczęstszych błędów popełnianych przez programistów C++ i prawdopodobnie najczęstszą przyczyną awarii programów w C++ w praktyce.

Ostrzeżenie

Za każdym razem, gdy używasz wskaźników, musisz zachować szczególną ostrożność, aby Twój kod nie wyłuskiwał wskaźników zerowych lub wiszących, ponieważ spowoduje to niezdefiniowane zachowanie (prawdopodobnie aplikacja awaria).

Sprawdzanie wskaźników zerowych

Podobnie jak możemy użyć warunku do przetestowania wartości logicznych dla true lub false, możemy użyć warunku do sprawdzenia, czy wskaźnik ma wartość nullptr czy nie:

#include <iostream>

int main()
{
    int x { 5 };
    int* ptr { &x };

    if (ptr == nullptr) // explicit test for equivalence
        std::cout << "ptr is null\n";
    else
        std::cout << "ptr is non-null\n";

    int* nullPtr {};
    std::cout << "nullPtr is " << (nullPtr==nullptr ? "null\n" : "non-null\n"); // explicit test for equivalence

    return 0;
}

Powyższy program wypisuje:

ptr is non-null
nullPtr is null

W lekcji 4.9 -- Wartości logiczne, zauważyliśmy, że wartości całkowite zostaną niejawnie zamienione na wartości logiczne: wartość całkowita of 0 konwertuje na wartość logiczną false, a każda inna wartość całkowita jest konwertowana na wartość logiczną true.

Podobnie wskaźniki również będą niejawnie konwertowane na wartości logiczne: wskaźnik zerowy jest konwertowany na wartość logiczną false, a wskaźnik inny niż null jest konwertowany na wartość logiczną true. Dzięki temu możemy pominąć jawne testowanie nullptr i po prostu użyć niejawnej konwersji na wartość logiczną, aby sprawdzić, czy wskaźnik jest wskaźnikiem zerowym. Poniższy program jest odpowiednikiem poprzedniego:

#include <iostream>

int main()
{
    int x { 5 };
    int* ptr { &x };

    // pointers convert to Boolean false if they are null, and Boolean true if they are non-null
    if (ptr) // implicit conversion to Boolean
        std::cout << "ptr is non-null\n";
    else
        std::cout << "ptr is null\n";

    int* nullPtr {};
    std::cout << "nullPtr is " << (nullPtr ? "non-null\n" : "null\n"); // implicit conversion to Boolean

    return 0;
}

Ostrzeżenie

Warunków warunkowych można używać tylko do odróżniania wskaźników zerowych od wskaźników innych niż null. Nie ma wygodnego sposobu na określenie, czy wskaźnik inny niż null wskazuje na prawidłowy obiekt, czy jest zwisający (wskazuje nieprawidłowy obiekt).

Użyj nullptr, aby uniknąć zwisających wskaźników

Powyżej wspomnieliśmy, że wyłuskiwanie wskaźnika, który ma wartość null lub jest zwisający, spowoduje niezdefiniowane zachowanie. Dlatego musimy się upewnić, że nasz kod nie wykonuje żadnej z tych rzeczy.

Możemy łatwo uniknąć wyłuskiwania wskaźnika zerowego, używając warunku, aby upewnić się, że wskaźnik nie ma wartości null przed próbą wyłuskania:

// Assume ptr is some pointer that may or may not be a null pointer
if (ptr) // if ptr is not a null pointer
    std::cout << *ptr << '\n'; // okay to dereference
else
    // do something else that doesn't involve dereferencing ptr (print an error message, do nothing at all, etc...)

Ale co ze zwisającymi wskaźnikami? Ponieważ nie ma możliwości wykrycia, czy wskaźnik jest wiszący, musimy przede wszystkim unikać umieszczania w naszym programie jakichkolwiek wskaźników wiszących. Robimy to poprzez upewnienie się, że każdy wskaźnik, który nie wskazuje na prawidłowy obiekt, ma ustawioną wartość nullptr.

W ten sposób przed wyłuskaniem wskaźnika musimy jedynie sprawdzić, czy ma on wartość null — jeśli nie jest null, zakładamy, że wskaźnik nie wisi.

Najlepsza praktyka

Wskaźnik powinien albo zawierać adres prawidłowego obiektu, albo mieć ustawioną wartość nullptr. W ten sposób musimy jedynie przetestować wskaźniki pod kątem wartości null i możemy założyć, że każdy wskaźnik inny niż null jest prawidłowy.

Niestety unikanie zwisających wskaźników nie zawsze jest łatwe: gdy obiekt zostanie zniszczony, wszelkie wskaźniki do tego obiektu pozostaną w zawieszeniu. Takie wskaźniki są nie automatycznie kasowane! Obowiązkiem programisty jest upewnienie się, że wszystkie wskaźniki do obiektu, który właśnie został zniszczony, są prawidłowo ustawione na nullptr.

Ostrzeżenie

Gdy obiekt zostanie zniszczony, wszelkie wskaźniki do zniszczonego obiektu pozostaną w zawieszeniu (nie zostaną automatycznie ustawione na nullptr). Twoim obowiązkiem jest wykrycie takich przypadków i upewnienie się, że te wskaźniki zostaną następnie ustawione na nullptr.

Starsze literały wskaźników zerowych: 0 i NULL

W starszym kodzie możesz zobaczyć dwie inne wartości literałów zamiast nullptr.

Pierwsza to literał 0. W kontekście wskaźnika literał 0 jest specjalnie zdefiniowany jako oznaczający wartość null i jest to jedyny przypadek, w którym można przypisać do wskaźnika literał całkowity.

int main()
{
    float* ptr { 0 };  // ptr is now a null pointer (for example only, don't do this)

    float* ptr2; // ptr2 is uninitialized
    ptr2 = 0; // ptr2 is now a null pointer (for example only, don't do this)

    return 0;
}

Na marginesie…

W nowoczesnych architekturach adres 0 jest zwykle używany do reprezentowania wskaźnika zerowego. Jednak ta wartość nie jest gwarantowana przez standard C++, a niektóre architektury używają innych wartości. Literal 0, użyty w kontekście wskaźnika zerowego, zostanie przetłumaczony na dowolny adres używany przez architekturę do reprezentowania wskaźnika zerowego.

Dodatkowo istnieje makro preprocesora o nazwie NULL (zdefiniowane w nagłówku <cstddef>). To makro jest dziedziczone z C, gdzie jest powszechnie używane do wskazywania wskaźnika zerowego.

#include <cstddef> // for NULL

int main()
{
    double* ptr { NULL }; // ptr is a null pointer

    double* ptr2; // ptr2 is uninitialized
    ptr2 = NULL; // ptr2 is now a null pointer

    return 0;
}

Oba 0 i NULL należy unikać we współczesnym C++ (zamiast tego użyj nullptr ). Omawiamy to na lekcji 12.11 — Przekazywanie adresu (część 2).

Jeśli to możliwe, preferuj referencje zamiast wskaźników

Zarówno wskaźniki, jak i referencje dają nam możliwość pośredniego dostępu do innych obiektów.

Wskaźniki mają dodatkową możliwość zmiany tego, na co wskazują, i wskazywania na wartość null. Jednak te możliwości wskaźników są również z natury niebezpieczne: wskaźnik zerowy wiąże się z ryzykiem wyłuskania, a możliwość zmiany tego, na co wskazuje wskaźnik, może ułatwić tworzenie wskaźników wiszących:

int main()
{
    int* ptr { };
    
    {
        int x{ 5 };
        ptr = &x; // assign the pointer to an object that will be destroyed (not possible with a reference)
    } // ptr is now dangling and pointing to invalid object

    if (ptr) // condition evaluates to true because ptr is not nullptr
        std::cout << *ptr; // undefined behavior

    return 0;
}

Ponieważ referencji nie można powiązać z wartością null, nie musimy się martwić referencjami zerowymi. A ponieważ odniesienia muszą być powiązane z prawidłowym obiektem podczas tworzenia, a następnie nie mogą być ponownie umieszczone, odniesienia wiszące są trudniejsze do utworzenia.

Ponieważ są bezpieczniejsze, referencje powinny być preferowane zamiast wskaźników, chyba że wymagane są dodatkowe możliwości zapewniane przez wskaźniki.

Najlepsza praktyka

Preferuj odniesienia zamiast wskaźników, chyba że potrzebne są dodatkowe możliwości zapewniane przez wskaźniki.

A żart

Słyszałeś dowcip o wskaźniku zerowym?

W porządku, nie doszłoby do dereferencji.

Czas quizu

Pytanie nr 1

1a) Czy możemy określić, czy wskaźnik jest wskaźnikiem zerowym, czy nie? Jeśli tak, to w jaki sposób?

Pokaż rozwiązanie

1b) Czy możemy określić, czy wskaźnik o wartości innej niż null jest prawidłowy, czy nieaktualny? Jeśli tak, to w jaki sposób?

Pokaż rozwiązanie

Pytanie nr 2

Dla każdego podelementu odpowiedz, czy opisane działanie spowoduje zachowanie: przewidywalne, niezdefiniowane lub prawdopodobnie niezdefiniowane. Jeżeli odpowiedź brzmi „prawdopodobnie niezdefiniowany”, wyjaśnij kiedy.

Załóż, że wszystkie wymienione obiekty są typu, na który może wskazywać wskaźnik.

2a) Przypisanie adresu obiektu do niestałego wskaźnika

Pokaż rozwiązanie

2b) Przypisanie nullptr do wskaźnika

Pokaż rozwiązanie

2c) Wyłuskanie wskaźnika do prawidłowego obiektu

Pokaż rozwiązanie

2d) Wyłuskanie odwołania do wiszącego obiektu wskaźnik

Pokaż rozwiązanie

2e) Wyłuskiwanie wskaźnika zerowego

Pokaż rozwiązanie

2f) Wyłuskiwanie wskaźnika innego niż zerowy

Pokaż rozwiązanie

Pytanie nr 3

Dlaczego powinniśmy ustawiać wskaźniki, które nie wskazują na poprawny obiekt na „nullptr”?

Pokaż rozwiązanie

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
233 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze