Wskaźnik vs. odniesienie


256

Jaka byłaby lepsza praktyka przy nadawaniu funkcji oryginalnej zmiennej do pracy z:

unsigned long x = 4;

void func1(unsigned long& val) {
     val = 5;            
}
func1(x);

lub:

void func2(unsigned long* val) {
     *val = 5;
}
func2(&x);

IOW: Czy jest jakiś powód, aby wybierać między sobą?


1
Referencje są oczywiście cenne, ale pochodzę z C, gdzie wskaźniki są wszędzie. Aby zrozumieć wartość referencji, trzeba biegle posługiwać się wskaźnikami.
Jay D,

Jak to pasuje do celu, takiego jak przejrzystość referencyjna od programowania funkcjonalnego? Co jeśli zawsze chcesz, aby funkcje zwracały nowe obiekty i nigdy nie mutowały wewnętrznie stanu, zwłaszcza nie zmiennych przekazywanych do funkcji. Czy istnieje sposób, w jaki ta koncepcja jest nadal używana ze wskaźnikami i referencjami w języku takim jak C ++. (Uwaga: zakładam, że ktoś ma już na celu przejrzystość referencyjną. Nie chcę rozmawiać o tym, czy jest to dobry cel.)
ely

Wolę referencje. Wskaźniki użytkownika, gdy nie masz wyboru.
Ferruccio,

Odpowiedzi:


285

Moja ogólna zasada brzmi:

Użyj wskaźników, jeśli chcesz wykonywać z nimi arytmetykę wskaźnika (np. Zwiększając adres wskaźnika, aby przejść przez tablicę) lub jeśli kiedykolwiek będziesz musiał przekazać wskaźnik NULL.

W przeciwnym razie użyj referencji.


10
Doskonały punkt dotyczący wskaźnika o wartości NULL. Jeśli masz parametr wskaźnika, musisz jawnie sprawdzić, czy nie ma on wartości NULL, lub przeszukać wszystkie zastosowania funkcji, aby upewnić się, że nigdy nie ma wartości NULL. Ten wysiłek nie jest wymagany w przypadku referencji.
Richard Corden,

26
Wyjaśnij, co masz na myśli przez arytmetykę. Nowy użytkownik może nie rozumieć, że chcesz dostosować to, na co wskazuje wskaźnik.
Martin York,

7
Martin, Przez arytmetykę rozumiem, że przekazujesz wskaźnik do struktury, ale wiesz, że nie jest to prosta struktura, ale jej tablica. W takim przypadku możesz albo zindeksować go za pomocą [], albo wykonać arytmetykę za pomocą ++ / - na wskaźniku. To jest różnica w pigułce.
Nils Pipenbrinck

2
Martin, Możesz to zrobić tylko za pomocą wskaźników. Nie z odniesieniami. Pewnie, że możesz wziąć wskaźnik do referencji i zrobić to samo w praktyce, ale jeśli to zrobisz, skończysz z bardzo brudnym kodem.
Nils Pipenbrinck

1
Co z polimorfizmem (np. Base* b = new Derived())? To wygląda na przypadek, którego nie da się załatwić bez wskaźników.
Chris Redford,

72

Naprawdę myślę, że skorzystasz na ustanowieniu następujących wskazówek dotyczących kodowania wywoływania funkcji:

  1. Jak we wszystkich innych miejscach, zawsze bądź constpoprawny.

    • Uwaga: Oznacza to między innymi, że tylko wartości poza (patrz pozycja 3) i wartości przekazywane przez wartość (patrz pozycja 4) mogą nie mieć constspecyfikatora.
  2. Przekaż wartość przez wskaźnik tylko wtedy, gdy wartość 0 / NULL jest prawidłowym wejściem w bieżącym kontekście.

    • Uzasadnienie 1: Jako osoba dzwoniąca widzisz, że cokolwiek przekazujesz, musi być w stanie użytecznym.

    • Uzasadnienie 2: Jak nazywamy , wiesz, że cokolwiek przychodzi, jest w stanie użytecznym. Dlatego dla tej wartości nie trzeba wykonywać kontroli NULL ani obsługi błędów.

    • Uzasadnienie 3: Wymogi 1 i 2 zostaną wymuszone przez kompilator . Zawsze wychwytuj błędy w czasie kompilacji, jeśli możesz.

  3. Jeśli argument funkcji jest wartością out, przekaż go przez odwołanie.

    • Uzasadnienie: Nie chcemy łamać przedmiotu 2 ...
  4. Wybierz „przekaż przez wartość” zamiast „przekaż przez stałą odniesienia” tylko wtedy, gdy wartość jest POD ( zwykła stara struktura danych ) lub wystarczająco mała (pod względem pamięci) lub w inny sposób wystarczająco tania (pod względem czasu), aby skopiować.

    • Uzasadnienie: Unikaj niepotrzebnych kopii.
    • Uwaga: wystarczająco małe i wystarczająco tanie nie są absolutnie mierzalne.

Brakuje wytycznej, gdy: ... „kiedy należy użyć const &” ... Wytyczną 2 należy napisać „dla wartości [in], przekazać wskaźnik tylko, jeśli wartość NULL jest prawidłowa. W przeciwnym razie użyj const referen (lub dla„ małe „obiekty, skopiuj) lub odwołanie, jeśli jest to wartość [
wyjściowa

Punkt 1 obejmuje opisany przypadek.
Johann Gerell

Trudno jest przekazać parametr out przez referencję, jeśli nie można go domyślnie zbudować. Jest to dość powszechne w moim kodzie - jedynym powodem, dla którego funkcja tworzy ten obiekt zewnętrzny, jest to, że nie jest on trywialny.
MSalters

@MSalters: Jeśli zamierzasz przydzielić pamięć wewnątrz funkcji (co myślę, co masz na myśli), to dlaczego nie po prostu zwrócić wskaźnik do przydzielonej pamięci?
Kleist

@Kleist: W imieniu @MSalters istnieje wiele możliwych przyczyn. Jednym z nich jest to, że już masz przydzieloną pamięć do wypełnienia, tak jak w przypadku wcześniejszych rozmiarów std::vector<>.
Johann Gerell

24

Ostatecznie jest to subiektywne. Dotychczasowa dyskusja jest przydatna, ale nie sądzę, aby na to pytanie była poprawna lub zdecydowana odpowiedź. Wiele zależy od wytycznych stylu i twoich potrzeb w danym czasie.

Chociaż istnieją pewne różne możliwości (niezależnie od tego, czy coś może mieć wartość NULL) ze wskaźnikiem, największą praktyczną różnicą dla parametru wyjściowego jest czysto składnia. Przewodnik po stylu C ++ firmy Google ( https://google.github.io/styleguide/cppguide.html#Reference_Arguments ), na przykład, nakazuje tylko wskaźniki parametrów wyjściowych i dopuszcza tylko odniesienia, które są stałymi. Rozumowanie dotyczy czytelności: coś ze składnią wartości nie powinno mieć semantycznego znaczenia wskaźnika. Nie sugeruję, że to koniecznie jest dobre lub złe, ale myślę, że chodzi tutaj o to, że jest to kwestia stylu, a nie poprawności.


Co to znaczy, że odwołania mają składnię wartości, ale znaczenie semantyczne wskaźnika?
Eric Andrew Lewis,

Wygląda na to, że przekazujesz kopię, ponieważ część „pass by reference” wynika tylko z definicji funciton (składnia wartości), ale nie kopiujesz przekazywanej wartości, w zasadzie podajesz wskaźnik pod maską, co pozwala funkcja modyfikacji wartości.
phant0m

Nie należy zapominać, że przewodnik po stylu Google C ++ jest bardzo obrzydliwy.
Deduplicator,

7

Powinieneś przekazać wskaźnik, jeśli zamierzasz zmodyfikować wartość zmiennej. Chociaż technicznie przekazanie odwołania lub wskaźnika jest takie samo, przekazanie wskaźnika w twoim przypadku użycia jest bardziej czytelne, ponieważ „reklamuje” fakt, że wartość zostanie zmieniona przez funkcję.


2
Jeśli postępujesz zgodnie ze wskazówkami Johanna Gerella, odwołanie nie będące const również reklamuje zmienną, więc wskaźnik nie ma tutaj tej przewagi.
Alexander Kondratskiy,

4
@AlexanderKondratskiy: brakuje Ci punktu ... nie możesz natychmiast zobaczyć na stronie wywołania, czy wywoływana funkcja akceptuje parametr jako odniesienie, constczy nie const, ale możesz sprawdzić, czy parametr przeszedł ala &xvs. x, i użyj to przekonanie do kodowania, czy parametr może zostać zmodyfikowany. (To powiedziawszy, są chwile, kiedy będziesz chciał przekazać constwskaźnik, więc konwój jest tylko wskazówką. Argumentowanie, że podejrzenie czegoś może być zmodyfikowane, gdy nie będzie, jest mniej niebezpieczne niż myślenie, że nie będzie, kiedy to będzie ....)
Tony Delroy,

5

Jeśli masz parametr, w którym może być konieczne wskazanie braku wartości, powszechną praktyką jest ustawianie parametru jako wartości wskaźnika i przekazywanie wartości NULL.

Lepszym rozwiązaniem w większości przypadków (z punktu widzenia bezpieczeństwa) jest użycie boost :: opcjonalne . Umożliwia to przekazywanie opcjonalnych wartości przez odniesienie, a także jako wartość zwracaną.

// Sample method using optional as input parameter
void PrintOptional(const boost::optional<std::string>& optional_str)
{
    if (optional_str)
    {
       cout << *optional_str << std::endl;
    }
    else
    {
       cout << "(no string)" << std::endl;
    }
}

// Sample method using optional as return value
boost::optional<int> ReturnOptional(bool return_nothing)
{
    if (return_nothing)
    {
       return boost::optional<int>();
    }

    return boost::optional<int>(42);
}


4

Wskaźniki

  • Wskaźnik to zmienna przechowująca adres pamięci.
  • Deklaracja wskaźnika składa się z typu podstawowego, * i nazwy zmiennej.
  • Wskaźnik może wskazywać dowolną liczbę zmiennych w ciągu całego życia
  • Wskaźnik, który obecnie nie wskazuje prawidłowej lokalizacji w pamięci, otrzymuje wartość null (czyli zero)

    BaseType* ptrBaseType;
    BaseType objBaseType;
    ptrBaseType = &objBaseType;
  • & Jest jednoargumentowym operatorem, który zwraca adres pamięci swojego operandu.

  • Operator dereferencji (*) służy do uzyskania dostępu do wartości przechowywanej w zmiennej, na którą wskazuje wskaźnik.

       int nVar = 7;
       int* ptrVar = &nVar;
       int nVar2 = *ptrVar;

Odniesienie

  • Odwołanie (&) jest jak alias do istniejącej zmiennej.

  • Odwołanie (&) jest jak stały wskaźnik, który jest automatycznie usuwany z odniesienia.

  • Jest zwykle używany do list argumentów funkcji i zwracanych wartości funkcji.

  • Odwołanie musi zostać zainicjowane podczas jego tworzenia.

  • Po zainicjowaniu odwołania do obiektu nie można go zmienić, aby odwoływał się do innego obiektu.

  • Nie możesz mieć referencji NULL.

  • Odwołanie do stałej może odnosić się do stałej. Odbywa się to za pomocą zmiennej tymczasowej o wartości stałej

    int i = 3;    //integer declaration
    int * pi = &i;    //pi points to the integer i
    int& ri = i;    //ri is refers to integer i – creation of reference and initialization

wprowadź opis zdjęcia tutaj

wprowadź opis zdjęcia tutaj


3

Odwołanie jest domyślnym wskaźnikiem. Zasadniczo możesz zmienić wartość, na którą wskazują punkty odniesienia, ale nie możesz zmienić odniesienia, aby wskazywało na coś innego. Więc moje 2 centy są takie, że jeśli chcesz tylko zmienić wartość parametru, przekaż go jako odniesienie, ale jeśli musisz zmienić parametr, aby wskazywał na inny obiekt, przekaż go za pomocą wskaźnika.


3

Zastanów się nad słowem kluczowym C #. Kompilator wymaga, aby wywołujący metodę zastosował słowo kluczowe out do wszystkich argumentów out, nawet jeśli już wie, czy są. Ma to na celu zwiększenie czytelności. Chociaż w przypadku nowoczesnych IDE skłonny jestem myśleć, że jest to zadanie do podświetlania składni (lub semantycznych).


literówka: semantyczna, nie symantyczna; +1 Zgadzam się na możliwość podświetlenia zamiast wypisywania (C #) lub & (w przypadku C, brak referencji)
peenut

2

Przekaż const const, chyba że istnieje powód, dla którego chcesz zmienić / zachować przekazywaną zawartość.

W większości przypadków będzie to najbardziej wydajna metoda.

Upewnij się, że używasz const dla każdego parametru, którego nie chcesz zmieniać, ponieważ to nie tylko chroni cię przed zrobieniem czegoś głupiego w funkcji, ale daje dobre wskazanie innym użytkownikom, co funkcja robi z przekazanymi wartościami. Obejmuje to utworzenie wskaźnika stałej, gdy chcesz tylko zmienić to, na co wskazywałeś ...


2

Wskaźniki:

  • Można przypisać nullptr(lub NULL).
  • W witrynie wywoływania należy użyć, &jeśli typ nie jest samym wskaźnikiem, co wyraźnie modyfikuje obiekt.
  • Wskaźniki mogą być odbijane.

Bibliografia:

  • Nie może być zero.
  • Raz związany nie może się zmienić.
  • Dzwoniący nie muszą jawnie używać &. Jest to czasami uważane za złe, ponieważ musisz przejść do implementacji funkcji, aby sprawdzić, czy parametr został zmodyfikowany.

Mały punkt dla tych, którzy nie wiedzą: nullptr lub NULL to po prostu 0. stackoverflow.com/questions/462165/…
Serguei Fedorov

2
nullptr to nie to samo, co 0. Spróbuj int a = nullptr; stackoverflow.com/questions/1282295/what-exactly-is-nullptr
Johan Lundberg

0

Odwołanie jest podobne do wskaźnika, z tym wyjątkiem, że nie trzeba używać prefiksu ∗, aby uzyskać dostęp do wartości, do której odwołuje się odwołanie. Ponadto nie można utworzyć odwołania do innego obiektu po jego inicjalizacji.

Odnośniki są szczególnie przydatne do określania argumentów funkcji.

Aby uzyskać więcej informacji, zobacz „A Tour of C ++” autorstwa „Bjarne Stroustrup” (2014) strony 11–12

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.