Qual è la differenza tra un riferimento C # e un puntatore?


86

Non capisco bene la differenza tra un riferimento C # e un puntatore. Entrambi indicano un posto nella memoria, no? L'unica differenza che riesco a capire è che i puntatori non sono così intelligenti, non possono puntare a nulla sull'heap, sono esenti dalla garbage collection e possono fare riferimento solo a strutture o tipi di base.

Uno dei motivi per cui chiedo è che c'è la percezione che le persone abbiano bisogno di capire bene i puntatori (da C, immagino) per essere un buon programmatore. Molte persone che imparano le lingue di livello superiore lo perdono e quindi hanno questa debolezza.

Non capisco cosa c'è di così complesso in un puntatore? È fondamentalmente solo un riferimento a un luogo della memoria, non è vero? Può restituire la sua posizione e interagire direttamente con l'oggetto in quella posizione?

Ho perso un punto enorme?


1
La risposta breve è sì, ti sei perso qualcosa di ragionevolmente significativo, e questo è il motivo per "... la percezione che le persone hanno bisogno di capire i suggerimenti". Suggerimento: C # non è l'unico linguaggio disponibile.
jdigital

Risposte:


51

I riferimenti C # possono e verranno riposizionati dal Garbage Collector, ma i normali puntatori sono statici. Questo è il motivo per cui utilizziamo la fixedparola chiave quando acquisiamo un puntatore a un elemento dell'array, per evitare che venga spostato.

EDIT: Concettualmente, sì. Sono più o meno la stessa cosa.


Non esiste un altro comando che impedisce a un riferimento C # di spostare l'oggetto a cui fa riferimento dal GC?
Richard

Oh scusa, pensavo fosse qualcos'altro perché il post si riferiva a un puntatore.
Richard

Sì, un GCHandle.Alloc o un Marshal.AllocHGlobal (oltre il fisso)
ctacke

È stato risolto in C #, pin_ptr in C ++ / CLI
mmx

Marshal.AllocHGlobal non allocherà affatto memoria nell'heap gestito e naturalmente non è soggetto a Garbage Collection.
mmx

133

C'è una leggera, ma estremamente importante, distinzione tra un puntatore e un riferimento. Un puntatore punta a un punto della memoria mentre un riferimento punta a un oggetto in memoria. I puntatori non sono "type safe", nel senso che non è possibile garantire la correttezza della memoria a cui puntano.

Prendiamo ad esempio il codice seguente

int* p1 = GetAPointer();

Questo è indipendente dai tipi, nel senso che GetAPointer deve restituire un tipo compatibile con int *. Tuttavia non esiste ancora alcuna garanzia che * p1 punti effettivamente a un int. Potrebbe essere un carattere, un doppio o solo un puntatore nella memoria casuale.

Un riferimento, tuttavia, indica un oggetto specifico. Gli oggetti possono essere spostati in memoria ma il riferimento non può essere invalidato (a meno che non si utilizzi un codice non sicuro). I riferimenti sono molto più sicuri da questo punto di vista dei suggerimenti.

string str = GetAString();

In questo caso str ha uno dei due stati 1) non punta a nessun oggetto e quindi è nullo o 2) punta a una stringa valida. Questo è tutto. Il CLR garantisce che sia così. Non può e non lo farà per un puntatore.


13

Un riferimento è un puntatore "astratto": non puoi fare operazioni aritmetiche con un riferimento e non puoi giocare trucchi di basso livello con il suo valore.


8

Una delle principali differenze tra un riferimento e un puntatore è che un puntatore è una raccolta di bit il cui contenuto è importante solo quando viene utilizzato attivamente come puntatore, mentre un riferimento incapsula non solo un insieme di bit, ma anche alcuni metadati che mantengono il quadro sottostante informato della sua esistenza. Se esiste un puntatore a un oggetto in memoria, e quell'oggetto viene cancellato ma il puntatore non viene cancellato, l'esistenza continua del puntatore non causerà alcun danno a meno che o fino a quando non si tenti di accedere alla memoria a cui punta. Se non viene fatto alcun tentativo di utilizzare il puntatore, nulla si preoccuperà della sua esistenza. Al contrario, framework basati su riferimenti come .NET o JVM richiedono che sia sempre possibile per il sistema identificare ogni riferimento a un oggetto esistente, e ogni riferimento a un oggetto esistente deve sempre esserenull oppure identificare un oggetto del suo tipo appropriato.

Notare che ogni riferimento a un oggetto incapsula effettivamente due tipi di informazioni: (1) il contenuto del campo dell'oggetto che identifica e (2) l'insieme di altri riferimenti allo stesso oggetto. Sebbene non esista alcun meccanismo mediante il quale il sistema possa identificare rapidamente tutti i riferimenti che esistono a un oggetto, l'insieme di altri riferimenti che esistono a un oggetto può spesso essere la cosa più importante incapsulata da un riferimento (questo è particolarmente vero quando le cose di tipo Objectsono usate come cose come i token di blocco). Sebbene il sistema conservi alcuni bit di dati per ogni oggetto da utilizzare GetHashCode, gli oggetti non hanno un'identità reale al di là dell'insieme di riferimenti che esistono per loro. If Xcontiene l'unico riferimento esistente a un oggetto, sostituendoXcon un riferimento a un nuovo oggetto con lo stesso contenuto di campo non avrà alcun effetto identificabile tranne che per modificare i bit restituiti da GetHashCode(), e anche quell'effetto non è garantito.


5

I puntatori puntano a una posizione nello spazio degli indirizzi di memoria. I riferimenti puntano a una struttura dati. Tutte le strutture dati vengono spostate continuamente (beh, non così spesso, ma ogni tanto) dal garbage collector (per compattare lo spazio di memoria). Inoltre, come hai detto, le strutture dati senza riferimenti verranno raccolte dopo un po 'di tempo.

Inoltre, i puntatori sono utilizzabili solo in un contesto non sicuro.


5

Penso che sia importante per gli sviluppatori comprendere il concetto di puntatore, ovvero capire l'indirizzamento. Ciò non significa che debbano necessariamente usare i puntatori. È anche importante capire che il concetto di un riferimento differisce dal concetto di puntatore , anche se solo leggermente, ma che l'attuazione di un riferimento quasi sempre è un puntatore.

Vale a dire, una variabile che contiene un riferimento è solo un blocco di memoria delle dimensioni di un puntatore che contiene un puntatore all'oggetto. Tuttavia, questa variabile non può essere utilizzata nello stesso modo in cui può essere utilizzata una variabile puntatore. In C # (e C e C ++, ...), un puntatore può essere indicizzato come un array, ma un riferimento no. In C #, un riferimento viene rilevato dal Garbage Collector, un puntatore non può essere. In C ++, un puntatore può essere riassegnato, un riferimento no. Sintatticamente e semanticamente, i puntatori e i riferimenti sono abbastanza diversi, ma meccanicamente sono gli stessi.


La cosa dell'array sembra interessante, è fondamentalmente dove puoi dire al puntatore di compensare la posizione di memoria come un array mentre non sei in grado di farlo con un riferimento? Non riesco a pensare quando sarebbe utile ma comunque interessante.
Richard,

Se p è un int * (un puntatore a un int), allora (p + 1) è l'indirizzo identificato da p + 4 byte (la dimensione di un int). E p [1] è lo stesso di * (p + 1) (cioè, "dereferenzia" l'indirizzo 4 byte dopo p). Al contrario, con un riferimento ad array (in C #), l'operatore [] esegue una chiamata di funzione.
P Daddy

5

Per prima cosa penso che tu debba definire un "Puntatore" nella tua sematica. Intendi il puntatore che puoi creare in codice non sicuro con fisso ? Intendi un IntPtr che ottieni forse da una chiamata nativa o da Marshal.AllocHGlobal ? Intendi una GCHandle ? Tutti sono essenzialmente la stessa cosa - una rappresentazione di un indirizzo di memoria in cui è memorizzato qualcosa - che si tratti di una classe, un numero, una struttura, qualunque cosa. E per la cronaca, possono certamente essere sul mucchio.

Un puntatore (tutte le versioni precedenti) è un elemento fisso. Il GC non ha idea di cosa ci sia a quell'indirizzo e quindi non ha la capacità di gestire la memoria o la vita dell'oggetto. Ciò significa che perdi tutti i vantaggi di un sistema di raccolta dei rifiuti. È necessario gestire manualmente la memoria degli oggetti e si ha la possibilità di perdite.

Un riferimento d'altra parte è praticamente un "puntatore gestito" che il GC conosce. È ancora un indirizzo di un oggetto, ma ora il GC conosce i dettagli del target, quindi può spostarlo, compattare, finalizzare, smaltire e tutte le altre cose carine che fa un ambiente gestito.

La differenza principale, in realtà, sta nel come e perché li useresti. Per la stragrande maggioranza dei casi in un linguaggio gestito, utilizzerai un riferimento a un oggetto. I puntatori diventano utili per eseguire l'interoperabilità e la rara necessità di un lavoro molto veloce.

Modifica: In effetti, ecco un buon esempio di quando potresti usare un "puntatore" nel codice gestito - in questo caso è un GCHandle, ma la stessa identica cosa avrebbe potuto essere fatta con AllocHGlobal o usando fixed su un array di byte o uno struct. Tendo a preferire GCHandle perché mi sembra più ".NET".


Un piccolo cavillo che forse non dovresti dire "puntatore gestito" qui - anche con virgolette spaventose - perché questo è qualcosa di abbastanza diverso da un riferimento a un oggetto, in IL. Sebbene esista la sintassi per i puntatori gestiti in C ++ / CLI, in genere non sono accessibili da C #. In IL, si ottengono con le istruzioni (ie) ldloca e ldarga.
Glenn Slayden

5

Un puntatore può puntare a qualsiasi byte nello spazio degli indirizzi dell'applicazione. Un riferimento è strettamente vincolato, controllato e gestito dall'ambiente .NET.


1

La cosa dei puntatori che li rende un po 'complessi non è quello che sono, ma quello che puoi fare con loro. E quando hai un puntatore a un puntatore a un puntatore. È allora che inizia davvero a diventare divertente.


1

Uno dei maggiori vantaggi dei riferimenti sui puntatori è la maggiore semplicità e leggibilità. Come sempre, quando semplifichi qualcosa, lo rendi più facile da usare, ma a scapito della flessibilità e del controllo che ottieni con le cose di basso livello (come hanno detto altre persone).

I puntatori sono spesso criticati per essere "brutti".

class* myClass = new class();

Ora ogni volta che lo usi devi prima dereferenziarlo

myClass->Method() or (*myClass).Method()

Nonostante la perdita di leggibilità e l'aggiunta di complessità, le persone avevano ancora bisogno di usare spesso i puntatori come parametri in modo da poter modificare l'oggetto reale (invece di passare per valore) e per il guadagno di prestazioni di non dover copiare oggetti enormi.

Per me questo è il motivo per cui i riferimenti sono "nati" in primo luogo per fornire lo stesso vantaggio dei puntatori ma senza tutta quella sintassi del puntatore. Ora puoi passare l'oggetto reale (non solo il suo valore) E hai un modo più leggibile e normale di interagire con l'oggetto.

MyMethod(&type parameter)
{
   parameter.DoThis()
   parameter.DoThat()
}

I riferimenti C ++ differivano dai riferimenti C # / Java in quanto una volta assegnato un valore ad esso, non era possibile riassegnarlo (e deve essere assegnato quando è stato dichiarato). Era come usare un puntatore const (un puntatore che non poteva essere reindirizzato a un altro oggetto).

Java e C # sono linguaggi moderni di altissimo livello che hanno ripulito molti dei pasticci accumulati in C / C ++ nel corso degli anni e i puntatori erano sicuramente una di quelle cose che dovevano essere "ripulite".

Per quanto il tuo commento sulla conoscenza dei puntatori ti renda un programmatore più forte, questo è vero nella maggior parte dei casi. Se sai "come" funziona qualcosa invece di usarlo senza saperlo, direi che questo può spesso darti un vantaggio. Quanto di un vantaggio varierà sempre. Dopo tutto, usare qualcosa senza sapere come viene implementato è una delle tante bellezze di OOP e Interfaces.

In questo esempio specifico, cosa ti aiuterebbe sapere sui puntatori con i riferimenti? Capire che un riferimento C # NON è l'oggetto stesso ma punta all'oggetto è un concetto molto importante.

# 1: NON stai passando per valore Bene per i principianti quando usi un puntatore sai che il puntatore contiene solo un indirizzo, questo è tutto. La variabile stessa è quasi vuota ed è per questo che è così bello passare come argomenti. Oltre al miglioramento delle prestazioni, stai lavorando con l'oggetto reale in modo che le modifiche apportate non siano temporanee

# 2: polimorfismo / interfacce Quando hai un riferimento che è un tipo di interfaccia e punta a un oggetto, puoi solo chiamare i metodi di quell'interfaccia anche se l'oggetto può avere molte più capacità. Gli oggetti possono anche implementare gli stessi metodi in modo diverso.

Se comprendi bene questi concetti, non credo che ti manchi troppo dal non aver usato i puntatori. Il C ++ è spesso usato come linguaggio per l'apprendimento della programmazione perché a volte è bene sporcarsi le mani. Inoltre, lavorare con aspetti di livello inferiore ti fa apprezzare i comfort di un linguaggio moderno. Ho iniziato con C ++ e ora sono un programmatore C # e sento che lavorare con i puntatori non elaborati mi ha aiutato ad avere una migliore comprensione di ciò che accade sotto il cofano.

Non penso sia necessario che tutti inizino con i puntatori, ma ciò che è importante è che capiscano perché i riferimenti vengono utilizzati al posto dei tipi di valore e il modo migliore per capirlo è guardare al suo antenato, il puntatore.


1
Personalmente, penso che C # sarebbe stato un linguaggio migliore se la maggior parte dei luoghi che utilizzano .utilizzati ->, ma foo.bar(123)era sinonimo di una chiamata al metodo statico fooClass.bar(ref foo, 123). Ciò avrebbe permesso cose come myString.Append("George"); [che modificherebbe la variabile myString ], e ha reso più evidente la differenza di significato tra myStruct.field = 3;e myClassObject->field = 3;.
supercat
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.