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'; // wydrukuj wartość zmiennej x

    int* ptr{ &x }; // ptr przechowuje adres x
    std::cout << *ptr << '\n'; // użyj operatora dereferencji, aby wydrukować wartość obiektu pod adresem trzymanym przez ptr (który jest adresem x) adres)

    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. 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 jest teraz wskaźnikiem zerowym i nie jest trzymający adres
 
    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 jest wskaźnikiem zerowym i nie przechowuje adresu

    int x { 5 };
    ptr = &x; // ptr wskazuje teraz na obiekt x (nie jest już wskaźnikiem zerowym)

    std::cout << *ptr << '\n'; // wypisz wartość x przez wyłuskany 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 }; // może użyć nullptr do zainicjowania wskaźnika jako wskaźnik zerowy

    int value { 5 };
    int* ptr2 { &value }; // ptr2 jest prawidłowym wskaźnikiem
    ptr2 = nullptr; // Można przypisać nullptr, aby wskaźnik był wskaźnikiem zerowym

    someFunction(nullptr); // możemy także przekazać nullptr do funkcji, która ma parametr wskaźnika

    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 {}; // Utwórz wskaźnik zerowy
    std::cout << *ptr << '\n'; // Usuń wskaźnik zerowy

    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) // jawna test równoważności
        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"); // jawna test równoważności

    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 };

    // wskaźniki są konwertowane na wartość Boolean false, jeśli mają wartość null, i wartość Boolean true, jeśli są różne od wartości null
    if (ptr) // ukryta konwersja na wartość logiczną
        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"); // ukryta konwersja na wartość logiczną

    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:

// Załóż, że ptr jest wskaźnikiem, który może być wskaźnikiem zerowym lub nie
if (ptr) // jeśli ptr nie jest wskaźnikiem zerowym
    std::cout << *ptr << '\n'; // w porządku wyłuskanie
else
    // zrób coś innego, co nie wymaga wyłuskiwania ptr (drukuj komunikat o błędzie, w ogóle nic nie rób itp...)

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 jest teraz wskaźnikiem zerowym (tylko na przykład, nie rób tego)

    float* ptr2; // ptr2 jest niezainicjowany
    ptr2 = 0; // ptr2 jest teraz wskaźnikiem zerowym (tylko na przykład, nie rób tego 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> // dla NULL

int main()
{
    double* ptr { NULL }; // ptr jest wskaźnikiem zerowym

    double* ptr2; // ptr2 jest niezainicjowany
    ptr2 = NULL; // ptr2 jest teraz wskaźnikiem zerowym

    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; // przypisz wskaźnik do obiektu, który zostanie zniszczony (nie jest to możliwe w przypadku referencji)
    } // ptr zwisa teraz i wskazuje na nieprawidłowy obiekt

    if (ptr) // warunek ma wartość true, ponieważ ptr nie ma wartości 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