Kiedy używać referencji a wskaźników?


381

Rozumiem składnię i ogólną semantykę wskaźników w porównaniu z referencjami, ale jak mam zdecydować, kiedy bardziej lub mniej właściwe jest używanie referencji lub wskaźników w interfejsie API?

Oczywiście niektóre sytuacje wymagają jednej lub drugiej ( operator++wymaga argumentu referencyjnego), ale ogólnie uważam, że wolę używać wskaźników (i wskaźników stałych), ponieważ składnia jest jasna, że ​​zmienne są przekazywane destrukcyjnie.

Np. W następującym kodzie:

void add_one(int& n) { n += 1; }
void add_one(int* const n) { *n += 1; }
int main() {
  int a = 0;
  add_one(a); // Not clear that a may be modified
  add_one(&a); // 'a' is clearly being passed destructively
}

Dzięki wskaźnikowi zawsze (bardziej) oczywiste jest, co się dzieje, więc w przypadku interfejsów API i tym podobnych, w których istotną kwestią jest przejrzystość, wskaźniki nie są bardziej odpowiednie niż odniesienia? Czy to oznacza, że ​​odniesienia należy stosować tylko wtedy, gdy jest to konieczne (np. operator++)? Czy są jakieś problemy z wydajnością jednego lub drugiego?

EDYCJA (NIEAKTUALIZOWANE):

Poza dopuszczeniem wartości NULL i radzeniem sobie z surowymi tablicami wydaje się, że wybór sprowadza się do osobistych preferencji. Zaakceptowałem odpowiedź poniżej, która odwołuje się do Przewodnika po stylu Google C ++ , ponieważ prezentują pogląd, że „Odnośniki mogą być mylące, ponieważ mają składnię wartości, ale semantykę wskaźnika”.

Z powodu dodatkowej pracy wymaganej do dezynfekcji argumentów wskaźnika, które nie powinny mieć wartości NULL (np add_one(0). Wywoła wersję wskaźnika i zepsuje się w czasie wykonywania), z punktu widzenia łatwości konserwacji sensowne jest używanie referencji tam, gdzie MUSI być obecny obiekt, choć szkoda stracić przejrzystość składniową.


4
Wygląda na to, że już zdecydowałeś, którego użyć, kiedy. Osobiście wolę przekazać obiekt, na którym działam, niezależnie od tego, czy go modyfikuję, czy nie. Jeśli funkcja przyjmuje wskaźnik, to mówi mi, że działa na wskaźniki, tj. Używa ich jako iteratorów w tablicy.
Benjamin Lindley

1
@ Schnommus: W porządku, najczęściej używam TextMate. Mimo to myślę, że lepiej, aby znaczenie było oczywiste na pierwszy rzut oka.
connec

4
Co add_one(a);jest niejasnego, że azostanie zmodyfikowany? W kodzie jest napisane: dodaj jeden .
GManNickG

32
@connec: Przewodnik po stylu Google C ++ nie jest uważany za dobry przewodnik po stylu C ++. Jest to przewodnik po stylach pracy ze starą bazą kodu C ++ Google'a (tzn. Dobrą dla swoich rzeczy). Przyjęcie odpowiedzi na tej podstawie nie pomaga nikomu. Czytając swoje komentarze i wyjaśnienia, przyszedłeś do tego pytania z już ustaloną opinią i po prostu szukasz innych osób, które potwierdziłyby Twój pogląd. W rezultacie bazujesz na pytaniu i odpowiedzi na to, co chcesz / oczekujesz usłyszeć.
Martin York,

1
Jest to po prostu naprawione przez nazwanie metody addOneTo(...). Jeśli nie to chcesz zrobić, spójrz na deklarację.
Stefan

Odpowiedzi:


296

Używaj referencji, gdziekolwiek możesz, wskaźników, gdziekolwiek musisz.

Unikaj wskaźników, dopóki nie będziesz w stanie.

Powodem jest to, że wskaźniki utrudniają śledzenie / czytanie, mniej bezpieczne i znacznie bardziej niebezpieczne manipulacje niż jakiekolwiek inne konstrukcje.

Zatem podstawową zasadą jest używanie wskaźników tylko wtedy, gdy nie ma innego wyboru.

Na przykład, zwracanie wskaźnika do obiektu jest prawidłową opcją, gdy funkcja może w niektórych przypadkach zwrócić nullptr i zakłada się, że tak. To powiedziawszy, lepszym rozwiązaniem byłoby użycie czegoś podobnego do boost::optional.

Innym przykładem jest użycie wskaźników do surowej pamięci dla określonych manipulacji pamięcią. Powinno to być ukryte i zlokalizowane w bardzo wąskich częściach kodu, aby pomóc ograniczyć niebezpieczne części całej bazy kodu.

W twoim przykładzie nie ma sensu używać wskaźnika jako argumentu, ponieważ:

  1. jeśli podasz nullptrjako argument, przejdziesz do krainy niezdefiniowanych zachowań;
  2. wersja atrybutu referencyjnego nie pozwala (bez łatwych do zauważenia lew) na problem z 1.
  3. wersja atrybutu referencyjnego jest łatwiejsza do zrozumienia dla użytkownika: musisz podać poprawny obiekt, a nie coś, co może być puste.

Jeśli zachowanie funkcji musiałoby działać z danym obiektem lub bez niego, wówczas użycie wskaźnika jako atrybutu sugeruje, że można podać nullptrjako argument i jest to w porządku dla funkcji. To rodzaj umowy między użytkownikiem a wdrożeniem.


49
Nie jestem pewien, czy wskaźniki utrudniają czytanie? Jest to dość prosta koncepcja, która wyjaśnia, kiedy coś może zostać zmodyfikowane. Jeśli coś, co powiedziałbym, jest trudniejsze do odczytania, gdy nic nie wskazuje na to, co się dzieje, dlaczego add_one(a)nie powinien zwrócić wyniku, zamiast ustawić go przez odniesienie?
connec

46
@connec: Jeśli add_one(a)jest mylący, to dlatego, że ma niepoprawną nazwę. add_one(&a)miałby to samo zamieszanie, tylko teraz możesz zwiększać wskaźnik, a nie obiekt. add_one_inplace(a)uniknęłoby wszelkiego zamieszania.
Nicol Bolas,

20
Jeden punkt, odniesienia mogą odnosić się do pamięci, która może zniknąć tak łatwo, jak wskaźniki. Więc niekoniecznie są bezpieczniejsze niż wskaźniki. Utrwalanie i przekazywanie referencji może być równie niebezpieczne jak wskaźniki.
Doug T.

6
@Klaim Miałem na myśli surowe wskazówki. Miałem na myśli, że C ++ ma wskazówek, NULLi nullptr, i ma je z jakiegoś powodu. I nie jest to dobrze przemyślana ani nawet realistyczna rada, aby dać „nigdy nie używaj wskaźników” i / lub „nigdy nie używaj NULL, zawsze używaj boost::optional”. To po prostu szalone. Nie zrozum mnie źle, surowe wskaźniki są potrzebne w C ++ rzadziej niż w C, ale nadal są przydatne, nie są tak „niebezpieczne”, jak niektórzy ludzie w C ++ lubią twierdzić (to też przesada), i znowu: kiedy łatwiej jest po prostu użyć wskaźnika i return nullptr;wskazać brakującą wartość ... Po co importować cały Boost?

5
@Klaim „używanie NULL to zła praktyka” - teraz jest to po prostu śmieszne. I czy ifjest przestarzały i należy go użyć while() { break; }, prawda? Ponadto, nie martw się, widziałem i pracowałem z dużymi bazami kodu, i tak, jeśli jesteś nieostrożny , wówczas własność jest problemem. Nie, jeśli będziesz trzymać się konwencji, używaj ich konsekwentnie oraz komentuj i dokumentuj swój kod. Ale w końcu powinienem po prostu użyć C, bo jestem zbyt głupi, by używać C ++, prawda?

62

Wydajności są dokładnie takie same, ponieważ odniesienia są implementowane wewnętrznie jako wskaźniki. Dlatego nie musisz się o to martwić.

Nie ma ogólnie przyjętej konwencji dotyczącej tego, kiedy używać odniesień i wskaźników. W kilku przypadkach musisz zwrócić lub zaakceptować referencje (na przykład kopiować konstruktora), ale poza tym możesz robić to, co chcesz. Dość powszechną konwencją, z jaką się spotkałem, jest używanie odwołań, gdy parametr musi odwoływać się do istniejącego obiektu, oraz wskaźników, gdy wartość NULL jest prawidłowa.

Niektóre konwencje kodowania (takie jak Google ) nakazują, aby zawsze używać wskaźników lub stałych referencji, ponieważ referencje mają trochę niejasną składnię: mają zachowanie referencji, ale składnię wartości.


10
Aby dodać trochę do tego, przewodnik po stylu Google mówi, że parametry wejściowe do funkcji powinny być stałymi odniesieniami, a wyniki powinny być wskaźnikami. Podoba mi się to, ponieważ bardzo wyraźnie czyta sygnaturę funkcji, co jest wejściem, a co wyjściem.
Dan

44
@ Dan: Przewodnik po stylu Google dotyczy starego kodu Google i nie powinien być używany do nowoczesnego kodowania. W rzeczywistości jest to raczej zły styl kodowania dla nowego projektu.
GManNickG

13
@connec: Powiem tak: null jest idealnie poprawną wartością wskaźnika. Gdziekolwiek jest wskaźnik, mogę nadać mu wartość null. Ergo swoją drugą wersję add_onejest uszkodzony : add_one(0); // passing a perfectly valid pointer value, Kaboom. Musisz sprawdzić, czy jest pusty. Niektóre osoby będą odpowiadać: „no cóż, po prostu udokumentuję, że moja funkcja nie działa z null”. W porządku, ale potem pokonujesz cel pytania: jeśli przejrzysz dokumentację, aby sprawdzić, czy wartość null jest w porządku, zobaczysz także deklarację funkcji .
GManNickG

8
Gdyby to było odniesienie, zobaczyłbyś, że tak jest. Taka retorta nie ma jednak sensu: Referencje wymuszają na poziomie języka , że odnoszą się do istniejącego obiektu i prawdopodobnie nie są zerowe, podczas gdy wskaźniki nie mają takiego ograniczenia. Myślę, że jest jasne, że egzekwowanie na poziomie języka jest silniejsze i mniej podatne na błędy niż egzekwowanie na poziomie dokumentacji. Niektórzy spróbują odpowiedzieć na to, mówiąc: „Spójrz, zerowa referencja:. int& i = *((int*)0);To nie jest poprawna retorta. Problem w poprzednim kodzie dotyczy użycia wskaźnika, a nie referencji . Referencje nigdy nie są zerowe, kropka.
GManNickG

12
Witam, zauważyłem brak prawników językowych w komentarzach, więc pozwólcie, że naprawię: referencje są zwykle wdrażane przez wskaźniki, ale standard nie mówi czegoś takiego. Wdrożenie przy użyciu innego mechanizmu byłoby w 100% reklamacją.
Thomas Bonini,

34

Z C ++ FAQ Lite -

Używaj referencji, kiedy możesz, i wskaźników, kiedy musisz.

Referencje są zwykle preferowane zamiast wskaźników, gdy nie potrzebujesz „ponownego ustawiania”. Zazwyczaj oznacza to, że odniesienia są najbardziej przydatne w publicznym interfejsie klasy. Odnośniki zwykle pojawiają się na skórze obiektu, a wskaźniki w środku.

Wyjątkiem od powyższego jest sytuacja, w której parametr lub wartość zwracana przez funkcję wymaga odwołania „sentinel” - odwołania, które nie odnosi się do obiektu. Zazwyczaj najlepiej jest to zrobić, zwracając / przyjmując wskaźnik i nadając wskaźnikowi NULL specjalne znaczenie (odwołania zawsze muszą oznaczać obiekty, a nie pozbawiony odniesienia wskaźnik NULL).

Uwaga: Dawni programiści linii C czasami nie lubią referencji, ponieważ zapewniają semantykę referencji, która nie jest jawna w kodzie osoby dzwoniącej. Jednak po pewnym doświadczeniu w C ++ szybko zdaje sobie sprawę, że jest to forma ukrywania informacji, która jest raczej zasobem niż zobowiązaniem. Na przykład programiści powinni pisać kod w języku problemu, a nie w języku maszyny.


1
Myślę, że możesz argumentować, że jeśli używasz interfejsu API, powinieneś znać to, co robi i wiedzieć, czy przekazany parametr jest modyfikowany, czy nie ... coś do rozważenia, ale zgadzam się z programistami języka C ( chociaż sam mam małe doświadczenie w języku C). Dodałbym jednak, że jaśniejsza składnia jest korzystna zarówno dla programistów, jak i maszyn.
connec

1
@connec: Upewnij się, że programista C ma poprawny język. Nie popełniaj jednak błędu, traktując C ++ jako C. Jest to zupełnie inny język. Jeśli traktujesz C ++ jako C, w końcu piszesz to, co jest tak samo uważane za C with class(co nie jest C ++).
Martin York,

15

Moja ogólna zasada brzmi:

  • Użyj wskaźników dla parametrów wychodzących lub wejściowych / wyjściowych. Widać więc, że wartość zostanie zmieniona. (Musisz użyć &)
  • Użyj wskaźników, jeśli NULL parametr jest dopuszczalną wartością. (Upewnij się, że jest constto parametr przychodzący)
  • Użyj referencji dla parametru przychodzącego, jeśli nie może mieć wartości NULL i nie jest typem pierwotnym ( const T&).
  • Podczas zwracania nowo utworzonego obiektu używaj wskaźników lub inteligentnych wskaźników.
  • Używaj wskaźników lub inteligentnych wskaźników jako elementów struktury lub klasy zamiast odniesień.
  • Użyj referencji do aliasingu (np. int &current = someArray[i])

Niezależnie od tego, którego używasz, nie zapomnij udokumentować swoich funkcji i znaczenia ich parametrów, jeśli nie są one oczywiste.


14

Oświadczenie: poza tym, że referencje nie mogą mieć wartości NULL ani „odbicia” (co oznacza, że ​​nie mogą zmienić obiektu, którego są pseudonimem), tak naprawdę sprowadza się to do gustu, więc nie powiem "to jest lepsze".

To powiedziawszy, nie zgadzam się z twoim ostatnim stwierdzeniem w poście, ponieważ nie sądzę, aby kod stracił jasność w odniesieniu do odniesień. W twoim przykładzie

add_one(&a);

może być jaśniejsze niż

add_one(a);

ponieważ wiesz, że najprawdopodobniej wartość a się zmieni. Z drugiej jednak strony podpis funkcji

void add_one(int* const n);

jest nieco niejasne: czy n będzie pojedynczą liczbą całkowitą czy tablicą? Czasami masz dostęp tylko do (słabo udokumentowanych) nagłówków i podobnych podpisów

foo(int* const a, int b);

nie są łatwe do interpretacji na pierwszy rzut oka.

Imho, referencje są tak dobre jak wskaźniki, gdy nie jest potrzebny (ponowny) przydział ani ponowne wiązanie (w sensie wyjaśnionym wcześniej). Ponadto, jeśli programista używa tylko wskaźników do tablic, sygnatury funkcji są nieco mniej dwuznaczne. Nie wspominając już o tym, że składnia operatorów jest znacznie bardziej czytelna dzięki referencjom.


Dzięki za wyraźną demonstrację, gdzie oba rozwiązania zyskują i tracą jasność. Początkowo byłem w obozie wskaźnikowym, ale to ma sens.
Zach Beavon-Collin

12

Podobnie jak inni już odpowiedzieli: Zawsze używaj referencji, chyba że zmienna będąca NULL/ nullptrjest naprawdę poprawnym stanem.

Pogląd Johna Carmacka na ten temat jest podobny:

Wskaźniki NULL są największym problemem w C / C ++, przynajmniej w naszym kodzie. Podwójne użycie jednej wartości jako flagi i adresu powoduje niesamowitą liczbę krytycznych problemów. W miarę możliwości odniesienia do C ++ powinny być preferowane nad wskaźnikami; podczas gdy odniesienie jest „naprawdę” tylko wskaźnikiem, ma domyślną umowę, że nie ma wartości NULL. Wykonaj kontrole NULL, gdy wskaźniki zostaną zamienione w odwołania, a następnie możesz zignorować problem.

http://www.altdevblogaday.com/2011/12/24/static-code-analysis/

Edytuj 2012-03-13

Użytkownik Bret Kuhns słusznie zauważa:

Standard C ++ 11 został sfinalizowany. Myślę, że nadszedł czas, aby w tym wątku wspomnieć, że większość kodu powinna doskonale sobie radzić z kombinacją odniesień, shared_ptr i unique_ptr.

To prawda, ale pytanie wciąż pozostaje, nawet przy zastępowaniu surowych wskaźników inteligentnymi wskaźnikami.

Na przykład, zarówno std::unique_ptri std::shared_ptrmoże być zbudowany jako „pustych” wskaźników poprzez ich domyślnego konstruktora:

... co oznacza, że ​​używanie ich bez sprawdzenia, czy nie są puste, grozi awarią, o to właśnie chodzi w dyskusji J. Carmacka.

A potem mamy zabawny problem: „jak przekazać inteligentny wskaźnik jako parametr funkcji?”

Jon „s odpowiedź na pytanie C ++ - przechodzącą odniesień do boost :: shared_ptr oraz następujące komentarze pokazują, że nawet wtedy, przechodząc inteligentnego wskaźnika przez kopii lub poprzez odniesienie nie jest tak jednoznaczne, jak chciałoby (I sprzyjać pragnienia" przez odniesienie ”domyślnie, ale mogę się mylić).


1
Standard C ++ 11 został sfinalizowany. Myślę, że nadszedł czas, aby w tym wątku wspomnieć, że większość kodu powinna doskonale sobie radzić z kombinacją odniesień shared_ptri unique_ptr. Semantyka własności i konwencje parametrów wejścia / wyjścia są obsługiwane przez połączenie tych trzech elementów i stałości. W C ++ prawie nie ma potrzeby używania surowych wskaźników, z wyjątkiem przypadków, gdy mamy do czynienia ze starszym kodem i bardzo zoptymalizowanymi algorytmami. Te obszary, w których są używane, powinny być jak najbardziej zamknięte i przekształcać nieprzetworzone wskaźniki w semantycznie odpowiedni „nowoczesny” odpowiednik.
Bret Kuhns,

1
Dużo czasu inteligentne wskaźniki nie powinny być przekazywane, ale powinny być testowane pod kątem zerowości, a następnie zawarty w nich obiekt przekazywany przez referencję. Jedyny moment, w którym powinieneś przekazać inteligentny wskaźnik, to przeniesienie własności (unikatowa_ptr) lub współdzielenie własności (wspólna_ptr) z innym obiektem.
Luke Worth

@povman: W pełni się zgadzam: jeśli własność nie jest częścią interfejsu (i chyba że ma zostać zmodyfikowana, nie powinna tak być), nie powinniśmy przekazywać inteligentnego wskaźnika jako parametru (lub wartości zwracanej). Sprawa staje się nieco bardziej skomplikowana, gdy własność jest częścią interfejsu. Na przykład debata Sutter / Meyers na temat tego, jak przekazać parametr unikalny_ptr jako parametr: przez kopiowanie (Sutter) lub przez odniesienie wartości r (Meyers)? Antypattern polega na przekazywaniu wskaźnika do globalnego pliku shared_ptr, z ryzykiem unieważnienia tego wskaźnika (rozwiązaniem jest skopiowanie inteligentnego wskaźnika na stos)
paercebal

7

To nie jest kwestia gustu. Oto kilka ostatecznych zasad.

Jeśli chcesz odwoływać się do statycznie zadeklarowanej zmiennej w zakresie, w którym została zadeklarowana, użyj odwołania w C ++, a będzie to całkowicie bezpieczne. To samo dotyczy statycznie deklarowanego inteligentnego wskaźnika. Przekazywanie parametrów przez referencję jest przykładem tego użycia.

Jeśli chcesz odwoływać się do czegokolwiek z zakresu, który jest szerszy niż zakres, w którym jest zadeklarowany, powinieneś użyć inteligentnego wskaźnika z licznikiem referencyjnym, aby był całkowicie bezpieczny.

Możesz odwoływać się do elementu kolekcji z odniesieniem dla wygody składniowej, ale nie jest to bezpieczne; element można usunąć w dowolnym momencie.

Aby bezpiecznie przechowywać odniesienie do elementu kolekcji, musisz użyć inteligentnego wskaźnika z licznikiem odniesień.


5

Każda różnica w wydajności byłaby tak mała, że ​​nie uzasadniałaby zastosowania mniej przejrzystego podejścia.

Po pierwsze, jednym z przypadków, o których nie wspomniano, w których referencje są na ogół lepsze, są constreferencje. W przypadku typów nieprostych przekazanie const referenceznaku unika tworzenia tymczasowego i nie powoduje zamieszania, o które się martwisz (ponieważ wartość nie jest modyfikowana). W tym przypadku zmuszanie osoby do podania wskaźnika powoduje zamieszanie, o które się martwisz, ponieważ widok adresu odebranego i przekazanego do funkcji może sprawić, że pomyślisz, że wartość się zmieniła.

W każdym razie zasadniczo się z tobą zgadzam. Nie podoba mi się, że funkcje przyjmują odwołania, aby zmodyfikować ich wartość, gdy nie jest bardzo oczywiste, że właśnie to robi funkcja. Ja też wolę używać wskaźników w tym przypadku.

Kiedy musisz zwrócić wartość w typie złożonym, wolę odwołania. Na przykład:

bool GetFooArray(array &foo); // my preference
bool GetFooArray(array *foo); // alternative

Tutaj nazwa funkcji wyjaśnia, że ​​otrzymujesz informacje z powrotem do tablicy. Więc nie ma zamieszania.

Główną zaletą referencji jest to, że zawsze zawierają poprawną wartość, są czystsze niż wskaźniki i obsługują polimorfizm bez potrzeby dodatkowej składni. Jeśli żadna z tych zalet nie ma zastosowania, nie ma powodu, aby preferować odniesienie nad wskaźnikiem.


4

Skopiowano z wiki -

Konsekwencją tego jest to, że w wielu implementacjach działanie na zmiennej z automatycznym lub statycznym czasem życia poprzez referencję, chociaż składniowo podobne do bezpośredniego dostępu do niej, może obejmować kosztowne operacje ukrywania dereferencji. Odnośniki są składniowo kontrowersyjną cechą C ++, ponieważ zaciemniają poziom pośredni identyfikatora; to znaczy, w przeciwieństwie do kodu C, w którym wskaźniki zwykle wyróżniają się składniowo, w dużym bloku kodu C ++ może nie być od razu oczywiste, czy obiekt, do którego uzyskiwany jest dostęp, jest zdefiniowany jako zmienna lokalna czy globalna, czy też jest odniesieniem (niejawnym wskaźnikiem) do jakąś inną lokalizację, zwłaszcza jeśli kod miesza odniesienia i wskaźniki. Ten aspekt może utrudnić czytanie i debugowanie źle napisanego kodu C ++ (patrz Aliasing).

Zgadzam się w 100% z tym i dlatego uważam, że powinieneś używać referencji tylko wtedy, gdy masz bardzo dobry powód.


Zgadzam się również w dużym stopniu, ale dochodzę do wniosku, że utrata wbudowanej ochrony przed wskaźnikami NULL jest nieco zbyt kosztowna z uwagi na kwestie czysto składniowe, zwłaszcza że - choć bardziej precyzyjnie - składnia wskaźnika jest dość brzydka tak czy siak.
connec

Myślę, że okoliczność byłaby również ważnym czynnikiem. Myślę, że próba użycia referencji, gdy obecna baza kodu głównie używa wskaźników, byłaby złym pomysłem. Jeśli oczekujesz, że będą to referencje, to fakt, że są tak niejawni, może być mniej ważny ...
user606723

3

Należy pamiętać o następujących kwestiach:

  1. Wskaźniki mogą być NULL, odwołania nie mogą być NULL.

  2. Referencje są łatwiejsze w użyciu, constmogą być używane jako referencje, gdy nie chcemy zmieniać wartości i potrzebujemy tylko referencji w funkcji.

  3. Wskaźnik używany z * odniesieniami while używany z &.

  4. Użyj wskaźników, gdy wymagana jest arytmetyka wskaźnika.

  5. Możesz mieć wskaźniki do typu pustki, int a=5; void *p = &a;ale nie możesz mieć odniesienia do typu pustki.

Odniesienie do wskaźnika Vs

void fun(int *a)
{
    cout<<a<<'\n'; // address of a = 0x7fff79f83eac
    cout<<*a<<'\n'; // value at a = 5
    cout<<a+1<<'\n'; // address of a increment by 4 bytes(int) = 0x7fff79f83eb0
    cout<<*(a+1)<<'\n'; // value here is by default = 0
}
void fun(int &a)
{
    cout<<a<<'\n'; // reference of original a passed a = 5
}
int a=5;
fun(&a);
fun(a);

Sprawdź, kiedy użyć czego

Wskaźnik : do tablic, list linków, implementacji drzew i arytmetyki wskaźników.

Odniesienie : W parametrach funkcji i typach zwracanych.


2

Wystąpił problem z regułą „ używaj referencji tam, gdzie to możliwe ” i pojawia się, jeśli chcesz zachować referencje do dalszego wykorzystania. Aby zilustrować to przykładem, wyobraź sobie, że masz następujące klasy.

class SimCard
{
    public:
        explicit SimCard(int id):
            m_id(id)
        {
        }

        int getId() const
        {
            return m_id;
        }

    private:
        int m_id;
};

class RefPhone
{
    public:
        explicit RefPhone(const SimCard & card):
            m_card(card)
        {
        }

        int getSimId()
        {
            return m_card.getId();
        }

    private:
        const SimCard & m_card;
};

Na początku dobrym pomysłem może być RefPhone(const SimCard & card)przekazanie parametru do konstruktora przez referencję, ponieważ zapobiega on przekazywaniu niewłaściwych / zerowych wskaźników do konstruktora. W jakiś sposób zachęca do przydzielania zmiennych na stosie i czerpania korzyści z RAII.

PtrPhone nullPhone(0);  //this will not happen that easily
SimCard * cardPtr = new SimCard(666);  //evil pointer
delete cardPtr;  //muahaha
PtrPhone uninitPhone(cardPtr);  //this will not happen that easily

Ale potem przybywają tymczasowcy, by zniszczyć wasz szczęśliwy świat.

RefPhone tempPhone(SimCard(666));   //evil temporary
//function referring to destroyed object
tempPhone.getSimId();    //this can happen

Więc jeśli ślepo trzymasz się referencji, rezygnujesz z możliwości przekazania niepoprawnych wskaźników dla możliwości przechowywania referencji do zniszczonych obiektów, co ma w zasadzie taki sam efekt.

edycja: Pamiętaj, że trzymałem się zasady „Używaj referencji, gdziekolwiek możesz, wskaźników tam, gdzie musisz. Unikaj wskaźników, dopóki nie możesz”. od najbardziej uprzywilejowanej i zaakceptowanej odpowiedzi (inne odpowiedzi również to sugerują). Chociaż powinno to być oczywiste, przykładem nie jest pokazanie, że odniesienia jako takie są złe. Mogą być jednak niewłaściwie wykorzystywane, podobnie jak wskaźniki i mogą wnieść własne zagrożenia do kodu.


Istnieją następujące różnice między wskaźnikami i odniesieniami.

  1. Jeśli chodzi o przekazywanie zmiennych, przekazywanie przez odniesienie wygląda jak przekazywanie przez wartość, ale ma semantykę wskaźnika (działa jak wskaźnik).
  2. Odniesienia nie można bezpośrednio zainicjować na 0 (null).
  3. Odwołanie (odwołanie, obiekt bez odniesienia) nie może być modyfikowane (odpowiednik wskaźnika „* const”).
  4. const referen może zaakceptować parametr tymczasowy.
  5. Lokalne odwołania do stałych przedłużają żywotność tymczasowych obiektów

Biorąc to pod uwagę, moje obecne zasady są następujące.

  • Użyj referencji dla parametrów, które będą używane lokalnie w zakresie funkcji.
  • Użyj wskaźników, gdy 0 (null) jest dopuszczalną wartością parametru lub musisz zapisać parametr do dalszego użycia. Jeśli dopuszczalne jest 0 (null), dodaję do parametru przyrostek „_n”, użyj wskaźnika zabezpieczonego (jak QPointer w Qt) lub po prostu go udokumentuj. Możesz także użyć inteligentnych wskaźników. Musisz być jeszcze bardziej ostrożny ze wspólnymi wskaźnikami niż ze zwykłymi wskaźnikami (w przeciwnym razie możesz skończyć z przeciekami pamięci i bałaganem odpowiedzialności).

3
Problem z twoim przykładem nie polega na tym, że referencje są niebezpieczne, ale że polegasz na czymś poza zakresem instancji obiektu, aby utrzymać członków prywatnych przy życiu. const SimCard & m_card;to po prostu źle napisany kod.
plamenko

@plamenko Obawiam się, że nie rozumiesz celu tego przykładu. To, czy const SimCard & m_cardjest poprawne, czy nie, zależy od kontekstu. Wiadomość w tym poście nie jest taka, że ​​referencje są niebezpieczne (mogą być, jeśli się bardzo postaramy). Przesłanie jest takie, że nie powinieneś ślepo trzymać się mantry „używaj referencji, kiedy to możliwe”. Przykład jest wynikiem agresywnego użycia doktryny „używaj referencji, gdy tylko jest to możliwe”. To powinno być jasne.
dok.

Są dwie rzeczy, które niepokoją mnie twoją odpowiedzią, ponieważ myślę, że może to wprowadzić w błąd kogoś, kto próbuje dowiedzieć się więcej na ten temat. 1. Post jest jednokierunkowy i łatwo jest odnieść wrażenie, że referencje są złe. Podałeś tylko jeden przykład tego, jak nie korzystać z referencji. 2. W swoim przykładzie nie było jasne, co jest z tym nie tak. Tak, tymczasowe zostanie zniszczone, ale to nie ta linia była błędna, to implementacja klasy.
plamenko

Praktycznie nigdy nie powinieneś mieć takich członków const SimCard & m_card. Jeśli chcesz być efektywny z tymczasowymi, dodaj explicit RefPhone(const SimCard&& card)konstruktora.
plamenko

@plamenko, jeśli nie umiesz czytać ze zrozumieniem, to masz większy problem niż zwykłe wprowadzenie mojego postu w błąd. Nie wiem, jak mógłbym być bardziej zrozumiały. Spójrz na pierwsze zdanie. Wystąpił problem z mantrą „używaj referencji, gdy to możliwe”! Gdzie w moim poście znalazłeś stwierdzenie, że referencje są złe? Na końcu mojego posta napisałeś, gdzie korzystać z referencji, więc jak doszedłeś do takich wniosków? To nie jest bezpośrednia odpowiedź na pytanie?
dok.

1

Poniżej przedstawiono niektóre wytyczne.

Funkcja wykorzystuje przekazane dane bez ich modyfikacji:

  1. Jeśli obiekt danych jest mały, taki jak wbudowany typ danych lub mała struktura, przekaż go według wartości.

  2. Jeśli obiektem danych jest tablica, użyj wskaźnika, ponieważ jest to twój jedyny wybór. Ustaw wskaźnik jako wskaźnik do stałej.

  3. Jeśli obiekt danych ma dobrą strukturę, użyj wskaźnika const lub odwołania const, aby zwiększyć wydajność programu. Zaoszczędzisz czas i miejsce potrzebne do skopiowania struktury lub projektu klasy. Ustaw wskaźnik lub referencję jako stałą.

  4. Jeśli obiekt danych jest obiektem klasy, użyj stałego odwołania. Semantyka projektowania klas często wymaga użycia odwołania, co jest głównym powodem, dla którego C ++ dodało tę funkcję. Dlatego standardowym sposobem przekazywania argumentów obiektu klasy jest odwołanie.

Funkcja modyfikuje dane w funkcji wywołującej:

1. Jeśli obiekt danych jest wbudowanym typem danych, użyj wskaźnika. Jeśli zauważysz kod taki jak fixit (& x), gdzie x jest liczbą całkowitą, jasne jest, że ta funkcja zamierza zmodyfikować x.

2. Jeśli obiekt danych jest tablicą, użyj jedynego wyboru: wskaźnika.

3. Jeśli obiekt danych jest strukturą, użyj odwołania lub wskaźnika.

4. Jeśli obiekt danych jest obiektem klasy, użyj odwołania.

Oczywiście są to tylko wytyczne i mogą istnieć powody do dokonywania różnych wyborów. Na przykład cin używa referencji dla podstawowych typów, dzięki czemu można używać cin >> n zamiast cin >> & n.


0

Referencje są czystsze i łatwiejsze w użyciu, i lepiej ukrywają informacje. Referencje nie mogą być jednak ponownie przypisane. Jeśli musisz najpierw wskazać jeden obiekt, a następnie drugi, musisz użyć wskaźnika. Referencje nie mogą mieć wartości NULL, więc jeśli istnieje jakakolwiek szansa, że ​​przedmiotowy obiekt może mieć wartość NULL, nie wolno używać referencji. Musisz użyć wskaźnika. Jeśli chcesz samodzielnie manipulować obiektami, tzn. Jeśli chcesz przydzielić miejsce w pamięci dla obiektu na stercie zamiast na stosie, musisz użyć wskaźnika

int *pInt = new int; // allocates *pInt on the Heap

0

Twój poprawnie napisany przykład powinien wyglądać

void add_one(int& n) { n += 1; }
void add_one(int* const n)
{
  if (n)
    *n += 1;
}

Dlatego referencje są w miarę możliwości preferowane ...


-1

Po prostu wkładam swoją dziesięciocentówkę. Właśnie wykonałem test. W tym podstępny. Po prostu pozwoliłem g ++ utworzyć pliki asemblera tego samego miniprogramu przy użyciu wskaźników w porównaniu z użyciem referencji. Patrząc na wynik, są dokładnie takie same. Inne niż nazwy symboli. Patrząc na wydajność (w prostym przykładzie) nie ma problemu.

Teraz na temat wskaźników vs. referencji. IMHO Myślę, że jasność stoi ponad wszystkim. Jak tylko przeczytam niejawne zachowanie, moje palce u stóp zaczynają się zwijać. Zgadzam się, że miło jest domniemywać, że odwołanie nie może mieć wartości NULL.

Dereferencje wskaźnika NULL nie stanowią problemu. spowoduje to awarię aplikacji i będzie łatwe do debugowania. Dużym problemem są niezainicjowane wskaźniki zawierające nieprawidłowe wartości. Najprawdopodobniej spowoduje to uszkodzenie pamięci, powodując niezdefiniowane zachowanie bez wyraźnego źródła.

Myślę, że tutaj odniesienia są znacznie bezpieczniejsze niż wskaźniki. I zgadzam się z poprzednim stwierdzeniem, że interfejs (który powinien być jasno udokumentowany, patrz projekt na podstawie umowy, Bertrand Meyer) określa wynik parametrów funkcji. Teraz, biorąc to wszystko pod uwagę, moje preferencje idą do korzystania z referencji gdziekolwiek / kiedykolwiek to możliwe.


-2

W przypadku wskaźników musisz je wskazać na coś, więc wskaźniki kosztują miejsce w pamięci.

Na przykład funkcja, która pobiera wskaźnik liczb całkowitych, nie przyjmuje zmiennej całkowitej. Musisz więc utworzyć wskaźnik, aby ten pierwszy przekazał funkcję.

Jeśli chodzi o odniesienie, nie będzie to kosztować pamięci. Masz zmienną całkowitą i możesz przekazać ją jako zmienną odniesienia. Otóż ​​to. Nie musisz specjalnie dla niego tworzyć zmiennej referencyjnej.


4
Nie. Funkcja, która pobiera wskaźnik, nie wymaga alokacji zmiennej wskaźnika: można przekazać wartość tymczasową &address. Referencja z pewnością będzie kosztować pamięć, jeśli jest członkiem obiektu, a ponadto wszystkie istniejące kompilatory faktycznie implementują referencje jako adresy, więc nie oszczędzasz ani na przekazywaniu parametrów, ani na dereferencji.
underscore_d
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.