Na poziomie języka:
- Nie ma stałych referencji, są stałe wskaźniki
- Referencji nie można "zmienić", to znaczy nie możesz mieć referencji do zmiennej A, a następnie zmienić sobie, żeby ta referencja wskazywała na B
- Referencje nie mogą być nullem (to znaczy, zawsze muszą być poprawne, choć są hacki)
- Nie można mieć tablicy referencji (m.in. do tego istnieje std::reference_wrapper)
Mamy generalnie 4 rodzaje referencji:
1. int&
2. const int&
3. int&&
4. T&&
-
1. Referencja do l-wartości (l-value reference), może się "przyczepić" tylko do istniejącego obiektu, dla uproszczenia
2. Referencja do stałej l-wartości (const l-value reference, const reference w skrócie) (tak, wiem, że często określa się to const referenca (dosłownie - stała referencja), jednak referencje nie mogą być cv-qualified, czyli nie można im przyczepić const/volatile.), może się "przyczepić" do istniejących obiektów, ale również do obiektów tymczasowych, czyli np. literałów (5, 1.5f)
3. Referencja do r-wartości (r-value reference), przyczepi się tylko do obiektu tymczasowego*, jest używana przy semantyce przenoszenia. (to, że przyczepi się do obiektu tymczasowego nie jest do końca prawdą, bo w całości nie chodzi o to, jaki, czy gdzie obiekt ma lifetime, a bardziej jakiego typu jest jego wartość, lifetime ma znaczenie, ale w zupełnie innej kwestii)
4. Uniwersalna/przekazująca referencja (universal/forwarding reference) [universal reference to termin nieoficjalny, forwarding reference to termin oficjalny, używane są raczej zamiennie - dzięki @tkz za poprawkę], to już zdecydowanie bardziej skomplikowana rzecz, generalnie T to jakikolwiek template type, który jest dedukowalny. Używane są w generycznym kodzie do szybkiego i prostego przekazywania argumentów.
(w powyższym zestawieniu int można zamienić jakimkolwiek typem/niededukowalnym template type, za to T można zastąpić jakimkolwiek dedukowalnym template type)
Wskaźniki:
- Mogą być stałe (T* const)
- Mogą wskazywać na stałe typy (const T*)
- Mogą być stałe i wskazywać na stałe typy (const T* const)
- Można je "zmienić", czyli jeśli mamy wskaźnik do X, to możemy sobie zmienić ten wskaźnik, żeby wskazywał na Y
- Mogą być nullem (to znaczy - nie mieć wartości, oznacza się to specjalnym literałem nullptr)
- Korzystanie ze wskaźników może dodawać koszt null checków (sprawdzania, czy wskaźnik jest poprawny, czy nie)
- Wskaźniki używa się do np. lazy evaluation
- Wskaźniki używa się jeśli potrzebna jest semantyka opcjonalności (wyobraź sobie, że chcesz zwrócić z/ zaakceptować do funkcji jakiś typ, ale chcesz również dać użytkownikowi możliwość niepodawania go. W funkcji musisz sprawdzić, czy to co podał jest poprawną wartością, czy użytkownik zdecydował nie podawać nic, masz generalnie 3 możliwości - wybrać pewną wartość z wachlarza możliwych wartości typu i ustalić - to znaczy, że nie podałeś nic, użyć wskaźnika, użyć od C++17 std::optional)
Czego lepiej używać.. Hhmm.. to jest generalnie dość cięzkie pytanie, bo (szczególnie od c++11) referencji nie używa się tylko jako bezpieczniejszych zamienników dla pointerów. Generalnie raczej używaj referencji. Na poziomie binarki to może, ale nie musi być praktycznie to samo, kompilator może (tego nie jestem na 100% pewien) lepiej optymalizować referencję, poza tym referencje nie mają kosztu null checkingu, więc w kodzie może faktycznie się okazać, że referencje będą szybsze. Jednak zmiana wskaźnik -> referencja oznacza również inne zastosowanie, przeznacznie, inną semantykę..
Jeśli chodzi o przekazywanie do funkcji, to jeśli:
- Nie musisz zmienić wartości przekazanej
- Typ jest trywialny (int, float, char, etc.) -> przekaż by value (czyli po prostu void foo(int x))
- Typ nie jest trywialny (std::vector, std::string, etc.) -> przekaż przez stałą referencję [ponownie, to skrót myślowy] (czyli po prostu void foo(const std::vector<...>& x))
- Musisz zmienić wartość przekazaną
- ... blah blah semantyka przenoszenia, kod generyczny, srutututu
Dodatkowo mamy opcje widoków (std::string_view, std::span), niby prosty temat, ale nie będę się w nie wgłębiać.
W twoim przypadku lepiej użyć referencji. W pierwszej funkcji dla poprawności powinieneś sprawdzić, czy przesłany wskaźnik jest nullptr, czy nie.
To co napisałem to w zasadzie nie jest wszystko, bo dalej mamy kwestie UB, forwardingu, semantyki przenoszenia, extendowania czasu życia zmiennych, etc., etc., etc.
/ prosiłbym o poprawy, bo pisałem to na szybko, mogło się wkraść sporo błędów /