Ci sono vantaggi del passaggio per puntatore rispetto al passaggio per riferimento in C ++?


225

Quali sono i vantaggi del passaggio per puntatore rispetto al passaggio per riferimento in C ++?

Ultimamente, ho visto numerosi esempi che hanno scelto di passare argomenti di funzione tramite puntatori anziché passare per riferimento. Ci sono benefici nel fare questo?

Esempio:

func(SPRITE *x);

con una chiamata di

func(&mySprite);

vs.

func(SPRITE &x);

con una chiamata di

func(mySprite);

Non dimenticare newdi creare un puntatore e i conseguenti problemi di proprietà.
Martin York,

Risposte:


216

Un puntatore può ricevere un parametro NULL, un parametro di riferimento no. Se c'è mai la possibilità che tu possa voler passare "nessun oggetto", usa un puntatore invece di un riferimento.

Inoltre, il passaggio tramite puntatore consente di vedere esplicitamente nel sito della chiamata se l'oggetto viene passato per valore o per riferimento:

// Is mySprite passed by value or by reference?  You can't tell 
// without looking at the definition of func()
func(mySprite);

// func2 passes "by pointer" - no need to look up function definition
func2(&mySprite);

18
Risposta incompleta. L'uso dei puntatori non autorizzerà l'uso di oggetti temporanei / promossi, né l'uso di oggetti appuntiti come oggetti simili a stack. E suggerirà che l'argomento può essere NULL quando, il più delle volte, dovrebbe essere proibito un valore NULL. Leggi la risposta di litb per una risposta completa.
paercebal,

La seconda chiamata di funzione veniva annotata func2 passes by reference. Anche se apprezzo che tu abbia inteso che passa "per riferimento" da una prospettiva di alto livello, implementata passando un puntatore in una prospettiva a livello di codice, questo è stato molto confuso (vedi stackoverflow.com/questions/13382356/… ).
Corse di leggerezza in orbita

Non lo compro. Sì, passi un puntatore, quindi deve essere un parametro di output, perché ciò che è indicato non può essere const?
Deworde,

Non abbiamo questo riferimento di passaggio in C? Sto usando l'ultima versione di codeblock (mingw) e in quella selezione di un progetto C. Passando ancora per riferimento (func (int & a)) funziona. O è disponibile in C99 o C11 in poi?
Jon Wheelock,

1
@JonWheelock: No, C non ha affatto riferimenti pass-by. func(int& a)non è C valido in nessuna versione dello standard. Probabilmente stai compilando i tuoi file come C ++ per caso.
Adam Rosenfield,

245

Passando dal puntatore

  • Il chiamante deve prendere l'indirizzo -> non trasparente
  • Un valore 0 può essere fornito per indicare nothing. Questo può essere usato per fornire argomenti opzionali.

Passa per riferimento

  • Il chiamante passa l'oggetto -> trasparente. Deve essere utilizzato per il sovraccarico dell'operatore, poiché il sovraccarico per i tipi di puntatore non è possibile (i puntatori sono tipi incorporati). Quindi non puoi fare string s = &str1 + &str2;usando i puntatori.
  • Non sono possibili valori 0 -> La funzione chiamata non deve verificarli
  • Il riferimento a const accetta anche i provvisori void f(const T& t); ... f(T(a, b, c));:, i puntatori non possono essere usati in questo modo poiché non è possibile prendere l'indirizzo di un temporaneo.
  • Ultimo ma non meno importante, i riferimenti sono più facili da usare -> meno possibilità di bug.

7
Il passaggio tramite puntatore genera anche "La proprietà viene trasferita o no?" domanda. Questo non è il caso dei riferimenti.
Frerich Raabe,

43
Non sono d'accordo con "meno possibilità di bug". Quando si ispeziona il sito della chiamata e il lettore vede "foo (& s)", è immediatamente chiaro che s può essere modificato. Quando leggi "pippo" non è affatto chiaro se s possa essere modificato. Questa è una delle principali fonti di bug. Forse ci sono meno possibilità di una certa classe di bug, ma nel complesso, passare per riferimento è un'enorme fonte di bug.
William Pursell,

25
Cosa intendi con "trasparente"?
Gbert90,

2
@ Gbert90, se vedi foo (& a) in un sito di chiamata, sai che foo () accetta un tipo di puntatore. Se vedi foo (a), non sai se è necessario un riferimento.
Michael J. Davenport,

3
@ MichaelJ.Davenport - nella tua spiegazione, suggerisci "trasparente" per significare qualcosa sulla falsariga di "ovvio che il chiamante sta passando un puntatore, ma non ovvio che il chiamante sta passando un riferimento". Nel post di Johannes, dice "Passaggio per puntatore - Il chiamante deve prendere l'indirizzo -> non trasparente" e "Passa per riferimento - Il chiamante passa solo l'oggetto -> trasparente" - che è quasi l'opposto di quello che dici . Penso che la domanda di Gbert90 "Cosa intendi con" trasparente "" sia ancora valida.
Happy Green Kid Naps,

66

Mi piace il ragionamento di un articolo di "cplusplus.com:"

  1. Passa per valore quando la funzione non vuole modificare il parametro e il valore è facile da copiare (ints, doubles, char, bool, ecc ... tipi semplici. Std :: string, std :: vector e tutti gli altri STL i contenitori NON sono tipi semplici.)

  2. Passa dal puntatore const quando il valore è costoso da copiare E la funzione non vuole modificare il valore puntato a AND NULL è un valore atteso e valido che la funzione gestisce.

  3. Passa da un puntatore non const quando il valore è costoso da copiare E la funzione vuole modificare il valore puntato a AND NULL è un valore atteso valido che la funzione gestisce.

  4. Passa per riferimento const quando il valore è costoso da copiare E la funzione non vuole modificare il valore a cui fa riferimento AND NULL non sarebbe un valore valido se invece fosse usato un puntatore.

  5. Passa per riferimento non cont quando il valore è costoso da copiare E la funzione vuole modificare il valore riferito a AND NULL non sarebbe un valore valido se invece fosse usato un puntatore.

  6. Quando si scrivono funzioni modello, non esiste una risposta chiara perché ci sono alcuni compromessi da considerare che esulano dallo scopo di questa discussione, ma basti dire che la maggior parte delle funzioni modello prende i loro parametri in base al valore o al riferimento (const) , tuttavia poiché la sintassi dell'iteratore è simile a quella dei puntatori (asterisco a "dereference"), qualsiasi funzione modello che prevede iteratori come argomenti accetterà anche i puntatori per impostazione predefinita (e non controlla NULL poiché il concetto di iteratore NULL ha una sintassi diversa ).

http://www.cplusplus.com/articles/z6vU7k9E/

Ciò che prendo da questo è che la principale differenza tra la scelta di utilizzare un puntatore o un parametro di riferimento è se NULL è un valore accettabile. Questo è tutto.

Dopotutto, se il valore è input, output, modificabile ecc. Dovrebbe essere nella documentazione / commenti sulla funzione.


Sì, per me i termini relativi a NULL sono le principali preoccupazioni qui. Grazie per la citazione ..
binaryguy

62

"Enough Rope to Shot Yourself in the Foot" di Allen Holub elenca le seguenti 2 regole:

120. Reference arguments should always be `const`
121. Never use references as outputs, use pointers

Elenca diversi motivi per cui sono stati aggiunti riferimenti a C ++:

  • sono necessari per definire i costruttori di copie
  • sono necessari per sovraccarichi dell'operatore
  • const i riferimenti consentono di avere una semantica pass-by-value evitando una copia

Il suo punto principale è che i riferimenti non dovrebbero essere usati come parametri di "output" perché nel sito di chiamata non c'è alcuna indicazione se il parametro sia un riferimento o un parametro di valore. Quindi la sua regola è usare solo constriferimenti come argomenti.

Personalmente, penso che questa sia una buona regola empirica in quanto lo rende più chiaro quando un parametro è un parametro di output o meno. Tuttavia, mentre personalmente sono d'accordo con questo in generale, mi permetto di essere influenzato dalle opinioni degli altri nel mio team se sostengono parametri di output come riferimenti (ad alcuni sviluppatori piace immensamente).


8
La mia posizione in questo argomento è che se il nome della funzione lo rende totalmente ovvio, senza controllare i documenti, che il parametro verrà modificato, allora un riferimento non const è OK. Quindi personalmente consentirei "getDetails (DetailStruct & result)". Un puntatore aumenta la brutta possibilità di un input NULL.
Steve Jessop,

3
Questo è fuorviante. Anche se ad alcuni non piacciono i riferimenti, sono una parte importante della lingua e dovrebbero essere usati come tali. Questa linea di ragionamento è come dire di non usare modelli, ma puoi sempre usare contenitori vuoti * per memorizzare qualsiasi tipo. Leggi la risposta di litb.
David Rodríguez - dribeas,

4
Non vedo come questo sia fuorviante: ci sono momenti in cui sono richiesti riferimenti e ci sono momenti in cui le migliori pratiche potrebbero suggerire di non usarli anche se tu potessi. Lo stesso si può dire per qualsiasi caratteristica della lingua - eredità, amici non membri, sovraccarico dell'operatore, IM, ecc ...
Michael Burr,

A proposito, sono d'accordo sul fatto che la risposta di Litb sia molto buona ed è certamente più completa di questa - ho appena scelto di concentrarmi sulla discussione di una logica per evitare di usare i riferimenti come parametri di output.
Michael Burr,

1
Questa regola viene utilizzata nella guida di stile di Google c ++: google-styleguide.googlecode.com/svn/trunk/…
Anton Daneyko,

9

Chiarimenti ai post precedenti:


I riferimenti NON sono una garanzia per ottenere un puntatore non nullo. (Anche se spesso li trattiamo come tali.)

Mentre il codice orribilmente negativo, come nel portarti dietro il codice negativo della legnaia , verrà compilato ed eseguito quanto segue: (Almeno sotto il mio compilatore.)

bool test( int & a)
{
  return (&a) == (int *) NULL;
}

int
main()
{
  int * i = (int *)NULL;
  cout << ( test(*i) ) << endl;
};

Il vero problema che ho con i riferimenti si trova con altri programmatori, d'ora in poi chiamati IDIOTS , che allocano nel costruttore, dealloccano nel distruttore e non riescono a fornire un costruttore di copia o operatore = ().

Improvvisamente c'è un mondo di differenza tra foo (BAR bar) e foo (BAR & bar) . (Viene invocata l'operazione di copia bit a bit automatica. La deallocazione nel distruttore viene richiamata due volte.)

Per fortuna i compilatori moderni prenderanno questa doppia deallocazione dello stesso puntatore. 15 anni fa, non lo fecero. (In gcc / g ++, utilizzare setenv MALLOC_CHECK_ 0 per rivisitare i vecchi metodi.) Risulta, in DEC UNIX, la stessa memoria allocata a due oggetti diversi. Un sacco di debugging divertente lì ...


Più praticamente:

  • I riferimenti nascondono che stai modificando i dati archiviati da qualche altra parte.
  • È facile confondere un riferimento con un oggetto copiato.
  • I puntatori lo rendono ovvio!

16
non è questo il problema della funzione o dei riferimenti. stai infrangendo le regole del linguaggio. dereferenziare un puntatore nullo da solo è già un comportamento indefinito. "I riferimenti NON sono una garanzia per ottenere un puntatore non nullo": lo standard stesso afferma che lo sono. altri modi costituiscono un comportamento indefinito.
Johannes Schaub - litb

1
Sono d'accordo con litb. Se vero, il codice che ci stai mostrando è più sabotaggio di ogni altra cosa. Esistono modi per sabotare qualsiasi cosa, incluse le notazioni "riferimento" e "puntatore".
paercebal,

1
Ho detto che era "portarti fuori dal codice negativo della legnaia"! Allo stesso modo, puoi anche avere i = new FOO; cancella i; Test (* i); Un'altra occorrenza di puntatore / riferimento penzolante (purtroppo comune).
Mr.Ree,

1
In realtà non è il dereferenziamento NULL che è il problema, ma piuttosto USARE quell'oggetto dereferenziato (null). Come tale, non c'è davvero alcuna differenza (oltre alla sintassi) tra puntatori e riferimenti dal punto di vista dell'implementazione del linguaggio. Sono gli utenti che hanno aspettative diverse.
Mr.Ree,

2
Indipendentemente da ciò che fai con il riferimento restituito, nel momento in cui dici *i, il tuo programma ha un comportamento indefinito. Ad esempio, il compilatore può vedere questo codice e assumere "OK, questo codice ha un comportamento indefinito in tutti i percorsi di codice, quindi l'intera funzione deve essere irraggiungibile." Quindi supporrà che non vengano presi tutti i rami che portano a questa funzione. Questa è un'ottimizzazione eseguita regolarmente.
David Stone,

5

La maggior parte delle risposte qui non riesce a rispondere all'ambiguità intrinseca nell'avere un puntatore non elaborato nella firma di una funzione, in termini di espressione dell'intento. I problemi sono i seguenti:

  • Il chiamante non sa se il puntatore punta a un singolo oggetto o all'inizio di un "array" di oggetti.

  • Il chiamante non sa se il puntatore "possiede" la memoria a cui punta. IE, indipendentemente dal fatto che la funzione debba liberare la memoria. ( foo(new int)- È una perdita di memoria?).

  • Il chiamante non sa se è nullptrpossibile passare in sicurezza alla funzione.

Tutti questi problemi sono risolti da riferimenti:

  • I riferimenti si riferiscono sempre a un singolo oggetto.

  • I riferimenti non possiedono mai la memoria a cui si riferiscono, sono semplicemente una vista nella memoria.

  • I riferimenti non possono essere nulli.

Ciò rende i riferimenti un candidato molto migliore per l'uso generale. Tuttavia, i riferimenti non sono perfetti: ci sono un paio di problemi importanti da considerare.

  • Nessuna indiretta esplicita. Questo non è un problema con un puntatore non elaborato, poiché dobbiamo usare l' &operatore per dimostrare che stiamo effettivamente passando un puntatore. Ad esempio, int a = 5; foo(a);qui non è affatto chiaro che a sia passato per riferimento e possa essere modificato.
  • Valori Null. Questa debolezza dei puntatori può anche essere un punto di forza, quando in realtà vogliamo che i nostri riferimenti siano nulli. Visto che std::optional<T&>non è valido (per buoni motivi), i puntatori ci danno la nullità che desideri.

Quindi sembra che quando vogliamo un riferimento nullabile con l'indirizzamento esplicito, dovremmo cercare un T*diritto? Sbagliato!

astrazioni

Nella nostra disperazione per la nullità, possiamo raggiungere T*e semplicemente ignorare tutte le carenze e le ambiguità semantiche elencate in precedenza. Invece, dovremmo cercare ciò che fa meglio C ++: un'astrazione. Se scriviamo semplicemente una classe che avvolge un puntatore, otteniamo l'espressività, così come il nullability e l'indirizzamento esplicito.

template <typename T>
struct optional_ref {
  optional_ref() : ptr(nullptr) {}
  optional_ref(T* t) : ptr(t) {}
  optional_ref(std::nullptr_t) : ptr(nullptr) {}

  T& get() const {
    return *ptr;
  }

  explicit operator bool() const {
    return bool(ptr);
  }

private:
  T* ptr;
};

Questa è l'interfaccia più semplice che ho potuto inventare, ma fa il lavoro in modo efficace. Permette di inizializzare il riferimento, controllando se esiste un valore e accedendo al valore. Possiamo usarlo così:

void foo(optional_ref<int> x) {
  if (x) {
    auto y = x.get();
    // use y here
  }
}

int x = 5;
foo(&x); // explicit indirection here
foo(nullptr); // nullability

Abbiamo raggiunto i nostri obiettivi! Vediamo ora i vantaggi, rispetto al puntatore non elaborato.

  • L'interfaccia mostra chiaramente che il riferimento dovrebbe riferirsi solo a un oggetto.
  • Chiaramente non possiede la memoria a cui fa riferimento, in quanto non ha un distruttore definito dall'utente e nessun metodo per eliminare la memoria.
  • Il chiamante sa che nullptrpuò essere passato, poiché l'autore della funzione chiede esplicitamente unoptional_ref

Da qui potremmo rendere l'interfaccia più complessa, come l'aggiunta di operatori di uguaglianza, una monade get_ore mapun'interfaccia, un metodo che ottiene il valore o genera un'eccezione, constexprsupporto. Questo può essere fatto da te.

In conclusione, invece di utilizzare i puntatori non elaborati, ragiona su cosa significano effettivamente quei puntatori nel tuo codice e sfrutta un'astrazione standard della libreria o scrivine una tua. Ciò migliorerà significativamente il tuo codice.


3

Non proprio. Internamente, il passaggio per riferimento viene eseguito essenzialmente passando l'indirizzo dell'oggetto referenziato. Quindi, non c'è davvero alcun guadagno di efficienza da ottenere passando un puntatore.

Il passaggio per riferimento ha tuttavia un vantaggio. Hai la garanzia di avere un'istanza di qualunque oggetto / tipo venga passato. Se passi un puntatore, corri il rischio di ricevere un puntatore NULL. Utilizzando il pass-by-reference, si sta spingendo un controllo NULL implicito di un livello al chiamante della propria funzione.


1
Questo è sia un vantaggio che uno svantaggio. Molte API usano i puntatori NULL per indicare qualcosa di utile (ovvero NULL timespec aspetta per sempre, mentre il valore significa aspetta così a lungo).
Greg Rogers,

1
@Brian: non voglio essere nit-picking ma: non direi che uno è garantito per ottenere un'istanza quando si ottiene un riferimento. I riferimenti penzolanti sono ancora possibili se il chiamante di una funzione fa riferimento a un puntatore penzolante, che il chiamante non può conoscere.
foraidt

a volte puoi persino ottenere prestazioni usando i riferimenti, dal momento che non devono occupare spazio di archiviazione e non hanno indirizzi assegnati per loro stessi. nessuna indiretta richiesta.
Johannes Schaub - litb

I programmi che contengono riferimenti penzolanti non sono C ++ validi. Pertanto, sì, il codice può presumere che tutti i riferimenti siano validi.
Konrad Rudolph,

2
Posso sicuramente dereferenziare un puntatore nullo e il compilatore non sarà in grado di dirlo ... se il compilatore non può dire che è "C ++ non valido", è davvero non valido?
rmeador,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.