Puntatori è un concetto che all'inizio per molti può essere fonte di confusione, in particolare quando si tratta di copiare i valori dei puntatori e fare ancora riferimento allo stesso blocco di memoria.
Ho scoperto che la migliore analogia è considerare il puntatore come un pezzo di carta con un indirizzo di casa su di esso e il blocco di memoria a cui fa riferimento come casa reale. È quindi possibile spiegare facilmente ogni tipo di operazione.
Ho aggiunto un po 'di codice Delphi in basso e alcuni commenti, se del caso. Ho scelto Delphi poiché il mio altro linguaggio di programmazione principale, C #, non mostra cose come le perdite di memoria allo stesso modo.
Se desideri solo imparare il concetto di alto livello di puntatori, allora dovresti ignorare le parti etichettate "Layout di memoria" nella spiegazione sotto. Hanno lo scopo di fornire esempi di come potrebbe apparire la memoria dopo le operazioni, ma sono di natura più di basso livello. Tuttavia, al fine di spiegare con precisione come funzionano davvero i sovraccarichi del buffer, è stato importante aggiungere questi diagrammi.
Dichiarazione di non responsabilità: a tutti gli effetti, questa spiegazione e i layout di memoria di esempio sono notevolmente semplificati. Ci sono più spese generali e molti più dettagli che dovresti sapere se hai bisogno di gestire la memoria a basso livello. Tuttavia, allo scopo di spiegare la memoria e i puntatori, è abbastanza accurato.
Supponiamo che la classe THouse usata di seguito assomigli a questa:
type
THouse = class
private
FName : array[0..9] of Char;
public
constructor Create(name: PChar);
end;
Quando si inizializza l'oggetto house, il nome assegnato al costruttore viene copiato nel campo privato FName. C'è un motivo per cui è definito come un array di dimensioni fisse.
In memoria, ci sarà un certo overhead associato con l'allocazione della casa, lo illustrerò di seguito in questo modo:
--- [ttttNNNNNNNNNN] ---
^ ^
| |
| + - l'array FName
|
+ - spese generali
L'area "tttt" è sovraccarica, in genere ce ne sarà di più per vari tipi di runtime e lingue, come 8 o 12 byte. È indispensabile che i valori memorizzati in quest'area non vengano mai modificati da qualcosa di diverso dall'allocatore di memoria o dalle routine del sistema principale, oppure si rischia di arrestare il programma in modo anomalo.
Allocare memoria
Ottieni un imprenditore per costruire la tua casa e darti l'indirizzo della casa. Contrariamente al mondo reale, non è possibile dire dove allocare l'allocazione di memoria, ma troverà un posto adatto con spazio sufficiente e riporterà l'indirizzo alla memoria allocata.
In altre parole, l'imprenditore sceglierà il posto.
THouse.Create('My house');
Layout di memoria:
--- [ttttNNNNNNNNNN] ---
1234 La mia casa
Mantieni una variabile con l'indirizzo
Scrivi l'indirizzo nella tua nuova casa su un pezzo di carta. Questo documento servirà come riferimento a casa tua. Senza questo pezzo di carta, ti sei perso e non riesci a trovare la casa, a meno che non ci sia già dentro.
var
h: THouse;
begin
h := THouse.Create('My house');
...
Layout di memoria:
h
v
--- [ttttNNNNNNNNNN] ---
1234 La mia casa
Copia il valore del puntatore
Scrivi l'indirizzo su un nuovo pezzo di carta. Ora hai due pezzi di carta che ti porteranno nella stessa casa, non due case separate. Qualsiasi tentativo di seguire l'indirizzo da una carta e riorganizzare i mobili di quella casa sembrerà che l'altra casa sia stata modificata nello stesso modo, a meno che tu non riesca a rilevare esplicitamente che in realtà è solo una casa.
Nota Questo è di solito il concetto che ho più problemi a spiegare alle persone, due puntatori non significano due oggetti o blocchi di memoria.
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := h1; // copies the address, not the house
...
h1
v
--- [ttttNNNNNNNNNN] ---
1234 La mia casa
^
h2
Liberare la memoria
Demolire la casa. Successivamente puoi riutilizzare la carta per un nuovo indirizzo, se lo desideri, o cancellarlo per dimenticare l'indirizzo della casa che non esiste più.
var
h: THouse;
begin
h := THouse.Create('My house');
...
h.Free;
h := nil;
Qui prima costruisco la casa e ottengo il suo indirizzo. Quindi faccio qualcosa per la casa (lo uso, il ... codice, lasciato come esercizio per il lettore), e poi lo libero. Infine, desidero cancellare l'indirizzo dalla mia variabile.
Layout di memoria:
h <- +
v + - prima di libero
--- [ttttNNNNNNNNNN] --- |
1234 La mia casa <- +
h (ora non punta da nessuna parte) <- +
+ - dopo libero
---------------------- | (nota, la memoria potrebbe ancora
xx34La mia casa <- + contiene alcuni dati)
Puntatori ciondolanti
Dici al tuo imprenditore di distruggere la casa, ma ti dimentichi di cancellare l'indirizzo dal tuo pezzo di carta. Quando in seguito guardi il pezzo di carta, hai dimenticato che la casa non è più lì e va a visitarla, con risultati falliti (vedi anche la parte relativa a un riferimento non valido di seguito).
var
h: THouse;
begin
h := THouse.Create('My house');
...
h.Free;
... // forgot to clear h here
h.OpenFrontDoor; // will most likely fail
L'uso h
dopo la chiamata .Free
potrebbe funzionare, ma è solo pura fortuna. Molto probabilmente fallirà, nel luogo di un cliente, nel mezzo di un'operazione critica.
h <- +
v + - prima di libero
--- [ttttNNNNNNNNNN] --- |
1234 La mia casa <- +
h <- +
v + - dopo libero
---------------------- |
xx34La mia casa <- +
Come puoi vedere, h punta ancora ai resti dei dati in memoria, ma poiché potrebbe non essere completo, usarlo come prima potrebbe non riuscire.
Perdita di memoria
Perdi il pezzo di carta e non riesci a trovare la casa. La casa è ancora in piedi da qualche parte, però, e quando in seguito vuoi costruire una nuova casa, non puoi riutilizzare quel posto.
var
h: THouse;
begin
h := THouse.Create('My house');
h := THouse.Create('My house'); // uh-oh, what happened to our first house?
...
h.Free;
h := nil;
Qui abbiamo sovrascritto il contenuto della h
variabile con l'indirizzo di una nuova casa, ma quella vecchia è ancora in piedi ... da qualche parte. Dopo questo codice, non c'è modo di raggiungere quella casa e rimarrà in piedi. In altre parole, la memoria allocata rimarrà allocata fino alla chiusura dell'applicazione, a quel punto il sistema operativo la distruggerà.
Layout di memoria dopo la prima allocazione:
h
v
--- [ttttNNNNNNNNNN] ---
1234 La mia casa
Layout di memoria dopo la seconda allocazione:
h
v
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
1234 La mia casa 5678 La mia casa
Un modo più comune per ottenere questo metodo è semplicemente dimenticare di liberare qualcosa, invece di sovrascriverlo come sopra. In termini di Delphi, ciò si verificherà con il seguente metodo:
procedure OpenTheFrontDoorOfANewHouse;
var
h: THouse;
begin
h := THouse.Create('My house');
h.OpenFrontDoor;
// uh-oh, no .Free here, where does the address go?
end;
Dopo che questo metodo è stato eseguito, non c'è posto nelle nostre variabili che l'indirizzo alla casa esiste, ma la casa è ancora là fuori.
Layout di memoria:
h <- +
v + - prima di perdere il puntatore
--- [ttttNNNNNNNNNN] --- |
1234 La mia casa <- +
h (ora non punta da nessuna parte) <- +
+ - dopo aver perso il puntatore
--- [ttttNNNNNNNNNN] --- |
1234 La mia casa <- +
Come puoi vedere, i vecchi dati vengono lasciati intatti in memoria e non verranno riutilizzati dall'allocatore di memoria. L'allocatore tiene traccia di quali aree della memoria sono state utilizzate e non le riutilizzerà se non lo si libera.
Liberare la memoria ma mantenendo un riferimento (ora non valido)
Demolisci la casa, cancella uno dei pezzi di carta ma hai anche un altro pezzo di carta con sopra il vecchio indirizzo, quando vai all'indirizzo, non troverai una casa, ma potresti trovare qualcosa che ricorda le rovine di uno.
Forse troverai persino una casa, ma non è la casa a cui ti è stato originariamente assegnato l'indirizzo, e quindi ogni tentativo di usarlo come se ti appartenesse potrebbe fallire in modo orribile.
A volte potresti persino scoprire che un indirizzo vicino ha una casa piuttosto grande allestita su di esso che occupa tre indirizzi (Main Street 1-3) e il tuo indirizzo va al centro della casa. Qualsiasi tentativo di trattare quella parte della grande casa a 3 indirizzi come una singola piccola casa potrebbe anche fallire in modo orribile.
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := h1; // copies the address, not the house
...
h1.Free;
h1 := nil;
h2.OpenFrontDoor; // uh-oh, what happened to our house?
Qui la casa è stata demolita, attraverso il riferimento h1
e, anche se è h1
stata cancellata, h2
ha ancora il vecchio indirizzo, obsoleto. L'accesso alla casa che non è più in piedi potrebbe o potrebbe non funzionare.
Questa è una variazione del puntatore pendente sopra. Vedi il suo layout di memoria.
Buffer sovraccarico
Sposta più cose nella casa di quante possiate inserire, versando nella casa o nel cortile dei vicini. Quando il proprietario di quella casa vicina tornerà a casa, troverà ogni sorta di cose che considererà sue.
Questo è il motivo per cui ho scelto un array di dimensioni fisse. Per preparare il terreno, supponiamo che la seconda casa che assegniamo verrà, per qualche motivo, posizionata prima della prima in memoria. In altre parole, la seconda casa avrà un indirizzo inferiore rispetto alla prima. Inoltre, sono allocati uno accanto all'altro.
Pertanto, questo codice:
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := THouse.Create('My other house somewhere');
^-----------------------^
longer than 10 characters
0123456789 <-- 10 characters
Layout di memoria dopo la prima allocazione:
h1
v
----------------------- [ttttNNNNNNNNNN]
5678 La mia casa
Layout di memoria dopo la seconda allocazione:
h2 h1
vv
--- [ttttNNNNNNNNNN] ---- [ttttNNNNNNNNNN]
1234 La mia altra casa da qualche parte
^ --- + - ^
|
+ - sovrascritto
La parte che più spesso causerà l'arresto anomalo è quando si sovrascrivono parti importanti dei dati archiviati che in realtà non devono essere cambiati casualmente. Ad esempio, potrebbe non essere un problema che parti del nome di h1-house siano state modificate, in termini di crash del programma, ma la sovrascrittura del sovraccarico dell'oggetto molto probabilmente si arresterà in modo anomalo quando si tenta di utilizzare l'oggetto rotto, come sovrascrivendo i collegamenti che sono memorizzati su altri oggetti nell'oggetto.
Elenchi collegati
Quando segui un indirizzo su un pezzo di carta, arrivi a una casa e in quella casa c'è un altro pezzo di carta con un nuovo indirizzo su di esso, per la casa successiva nella catena e così via.
var
h1, h2: THouse;
begin
h1 := THouse.Create('Home');
h2 := THouse.Create('Cabin');
h1.NextHouse := h2;
Qui creiamo un collegamento dalla nostra casa alla nostra cabina. Possiamo seguire la catena fino a quando una casa non ha NextHouse
riferimenti, il che significa che è l'ultima. Per visitare tutte le nostre case, potremmo usare il seguente codice:
var
h1, h2: THouse;
h: THouse;
begin
h1 := THouse.Create('Home');
h2 := THouse.Create('Cabin');
h1.NextHouse := h2;
...
h := h1;
while h <> nil do
begin
h.LockAllDoors;
h.CloseAllWindows;
h := h.NextHouse;
end;
Layout di memoria (aggiunto NextHouse come collegamento nell'oggetto, annotato con le quattro LLLL nel diagramma seguente):
h1 h2
vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
1234Home + 5678Cabin +
| ^ |
+ -------- + * (nessun collegamento)
In termini di base, che cos'è un indirizzo di memoria?
Un indirizzo di memoria è in termini basici solo un numero. Se pensi alla memoria come a un grande array di byte, il primo byte ha l'indirizzo 0, il successivo l'indirizzo 1 e così via verso l'alto. Questo è semplificato, ma abbastanza buono.
Quindi questo layout di memoria:
h1 h2
vv
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
1234 La mia casa 5678 La mia casa
Potrebbe avere questi due indirizzi (il più a sinistra - è l'indirizzo 0):
Ciò significa che il nostro elenco di link sopra riportato potrebbe apparire così:
h1 (= 4) h2 (= 28)
vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
1234 Home 0028 5678 Cabina 0000
| ^ |
+ -------- + * (nessun collegamento)
È tipico memorizzare un indirizzo che "non punta da nessuna parte" come indirizzo zero.
In termini di base, cos'è un puntatore?
Un puntatore è solo una variabile che contiene un indirizzo di memoria. In genere puoi chiedere al linguaggio di programmazione di darti il suo numero, ma la maggior parte dei linguaggi di programmazione e dei runtime cerca di nascondere il fatto che c'è un numero sotto, solo perché il numero stesso non ha davvero alcun significato per te. È meglio pensare a un puntatore come a una scatola nera, cioè. non sai davvero o ti preoccupi di come è effettivamente implementato, purché funzioni.