Dlaczego należy przekazać wskaźnik przez odwołanie w C ++?

98

W jakich okolicznościach chciałbyś użyć tego rodzaju kodu w języku c ++?

void foo(type *&in) {...}

void fii() {
  type *choochoo;
  ...
  foo(choochoo);
}
Matthew Hoggan
źródło
jeśli chcesz zwrócić wskaźnik - lepiej użyj wartości zwracanej
littleadv
6
Czy możesz wyjaśnić, dlaczego? Ta pochwała nie jest zbyt pomocna. Moje pytanie jest całkiem uzasadnione. Jest to obecnie używane w kodzie produkcyjnym. Po prostu nie do końca rozumiem dlaczego.
Matthew Hoggan
1
David podsumowuje to całkiem ładnie. Sam wskaźnik jest modyfikowany.
chris
2
Mijam tę drogę, jeśli w funkcji będę wywoływał operator „Nowy”.
NDEthos

Odpowiedzi:

143

Chciałbyś przekazać wskaźnik przez odniesienie, jeśli musisz zmodyfikować wskaźnik, a nie obiekt, na który wskazuje wskaźnik.

Jest to podobne do tego, dlaczego używane są podwójne wskaźniki; używanie odniesienia do wskaźnika jest nieco bezpieczniejsze niż używanie wskaźników.

David Z.
źródło
14
Tak podobny do wskaźnika wskaźnika, z wyjątkiem jednego mniejszego poziomu pośrednictwa dla semantyki przekazywania przez referencję?
Chciałbym dodać, że czasami chcesz również zwrócić wskaźnik przez odniesienie. Na przykład, jeśli masz klasę przechowującą dane w dynamicznie przydzielanej tablicy, ale chcesz zapewnić (niestały) dostęp do tych danych klientowi. Jednocześnie nie chcesz, aby klient mógł manipulować pamięcią za pomocą wskaźnika.
2
@William Czy możesz to rozwinąć? To brzmi interesująco. Czy to oznacza, że ​​użytkownik tej klasy mógłby uzyskać wskaźnik do wewnętrznego bufora danych i czytać / zapisywać do niego, ale nie może - na przykład - zwolnić tego bufora za pomocą deletelub ponownie powiązać ten wskaźnik z innym miejscem w pamięci? A może źle to zrozumiałem?
BarbaraKwarc
2
@BarbaraKwarc Jasne. Załóżmy, że masz prostą implementację tablicy dynamicznej. Oczywiście przeciążasz []operatora. Byłby to jeden możliwy (i faktycznie naturalny) podpis const T &operator[](size_t index) const. Ale możesz też mieć T &operator[](size_t index). Możesz mieć jedno i drugie w tym samym czasie. Ta ostatnia pozwoli ci robić takie rzeczy jak myArray[jj] = 42. Jednocześnie nie podajesz wskaźnika do swoich danych, więc dzwoniący nie może zepsuć pamięci (np. Usunąć ją przypadkowo).
O. Wydawało mi się, że mówisz o zwróceniu odniesienia do wskaźnika (twoje dokładne sformułowanie: „zwróć wskaźnik przez odniesienie”, ponieważ o to pytał OP: przekazywanie wskaźników przez odniesienia, a nie zwykłe stare odniesienia) jako jakiś fantazyjny sposób udzielanie dostępu do odczytu / zapisu do bufora bez zezwolenia na manipulowanie samym buforem (np. zwalnianie go). Zwrócenie zwykłego, starego odniesienia to inna rzecz i wiem o tym.
BarbaraKwarc
65

50% programistów C ++ lubi ustawiać swoje wskaźniki na null po usunięciu:

template<typename T>    
void moronic_delete(T*& p)
{
    delete p;
    p = nullptr;
}

Bez odwołania zmieniłbyś tylko lokalną kopię wskaźnika, nie wpływając na wywołującego.

fredoverflow
źródło
19
Podoba mi się nazwa; )
4pie0
2
Zamienię brzmiące głupio za znalezienie odpowiedzi: dlaczego nazywa się to kretynem usuwania? czego mi brakuje?
Sammaron
1
@Sammaron Jeśli spojrzysz na historię, szablon funkcji był kiedyś wywoływany paranoid_delete, ale Puppy zmienił nazwę na moronic_delete. Prawdopodobnie należy do pozostałych 50%;) W każdym razie, krótka odpowiedź jest taka, że ​​ustawienie wskaźników na null po deleteprawie nigdy nie jest przydatne (bo i tak powinny wykraczać poza zakres) i często utrudnia wykrycie błędów „użyj po zwolnieniu”.
fredoverflow,
@fredoverflow Rozumiem twoją odpowiedź, ale @Puppy wydaje się ogólnie uważać, że deletejest głupi. Szczeniaku, trochę googlowałem, nie rozumiem, dlaczego kasowanie jest kompletnie bezużyteczne (może ja też jestem okropnym googlerem;)). Czy możesz wyjaśnić trochę więcej lub podać link?
Sammaron,
@Sammaron, C ++ ma teraz "inteligentne wskaźniki", które hermetyzują zwykłe wskaźniki i automatycznie wywołują "delete", gdy wyjdą poza zakres. Inteligentne wskaźniki są zdecydowanie preferowane niż głupie wskaźniki w stylu C, ponieważ całkowicie eliminują ten problem.
tjwrona1992
14

Odpowiedź Davida jest poprawna, ale jeśli nadal jest trochę abstrakcyjna, oto dwa przykłady:

  1. Możesz chcieć wyzerować wszystkie zwolnione wskaźniki, aby wcześniej wychwycić problemy z pamięcią. W stylu C zrobiłbyś:

    void freeAndZero(void** ptr)
    {
        free(*ptr);
        *ptr = 0;
    }
    
    void* ptr = malloc(...);
    
    ...
    
    freeAndZero(&ptr);

    W C ++, aby zrobić to samo, możesz zrobić:

    template<class T> void freeAndZero(T* &ptr)
    {
        delete ptr;
        ptr = 0;
    }
    
    int* ptr = new int;
    
    ...
    
    freeAndZero(ptr);
  2. W przypadku list połączonych - często przedstawianych po prostu jako wskaźniki do następnego węzła:

    struct Node
    {
        value_t value;
        Node* next;
    };

    W takim przypadku, kiedy wstawiasz do pustej listy, koniecznie musisz zmienić przychodzący wskaźnik, ponieważ wynik nie jest NULLjuż wskaźnikiem. Jest to przypadek, w którym modyfikujesz zewnętrzny wskaźnik z funkcji, aby miał odniesienie do wskaźnika w swoim podpisie:

    void insert(Node* &list)
    {
        ...
        if(!list) list = new Node(...);
        ...
    }

W tym pytaniu jest przykład .

Antoine
źródło
8

Musiałem użyć kodu takiego jak ten, aby zapewnić funkcje do przydzielania pamięci do przekazanego wskaźnika i zwracania jego rozmiaru, ponieważ moja firma „obiekt” do mnie przy użyciu STL

 int iSizeOfArray(int* &piArray) {
    piArray = new int[iNumberOfElements];
    ...
    return iNumberOfElements;
 }

To nie jest miłe, ale wskaźnik musi być przekazywany przez odniesienie (lub użyj podwójnego wskaźnika). Jeśli nie, pamięć jest przydzielana do lokalnej kopii wskaźnika, jeśli jest przekazywana przez wartość, co powoduje wyciek pamięci.

matematyk1975
źródło
2

Jednym z przykładów jest sytuacja, gdy piszesz funkcję parsera i przekazujesz jej wskaźnik źródła do odczytu, jeśli funkcja ma popchnąć ten wskaźnik do przodu za ostatni znak, który został poprawnie rozpoznany przez parser. Użycie odniesienia do wskaźnika wyjaśnia, że ​​funkcja przesunie oryginalny wskaźnik, aby zaktualizować jego położenie.

Ogólnie rzecz biorąc, używasz odwołań do wskaźników, jeśli chcesz przekazać wskaźnik do funkcji i pozwolić mu przenieść oryginalny wskaźnik w inne miejsce, zamiast po prostu przesuwać jego kopię bez wpływania na oryginał.

BarbaraKwarc
źródło
Dlaczego głos przeciw? Czy jest coś nie tak z tym, co napisałem? Chcesz wyjaśnić? : P
BarbaraKwarc
0

Inną sytuacją, w której możesz tego potrzebować, jest to, że masz kolekcję wskaźników stl i chcesz je zmienić za pomocą algorytmu stl. Przykład for_each w języku c ++ 98.

struct Storage {
  typedef std::list<Object*> ObjectList;
  ObjectList objects;

  void change() {
    typedef void (*ChangeFunctionType)(Object*&);
    std::for_each<ObjectList::iterator, ChangeFunctionType>
                 (objects.begin(), objects.end(), &Storage::changeObject);
  }

  static void changeObject(Object*& item) {
    delete item;
    item = 0;
    if (someCondition) item = new Object();
  } 

};

W przeciwnym razie, jeśli używasz podpisu changeObject (obiekt *) , masz kopię wskaźnika, a nie oryginalną.

Dziewięć
źródło
jest to bardziej „zamiana obiektu” niż „zmiana obiektu” - jest różnica
serup