Puntatori
Mentre una variabile contiene un valore, un puntatore contiene un indirizzo, ovvero un riferimento ad un valore.
In Go una variabile può essere gestita o per valore, oppure per riferimento, come in C e C++.
Il simbolo che esprime l'idirizzo di una variabile è &
, quello che esprime l'accesso indiretto tramite puntatore (dereferenziazione) è *
.
Entrambi sono operatori prefissi e non si mettono mai spazi tra loro e la variabile operata.
In Java non esistono simboli espliciti per i puntatori, perchè le variabili o sono solo gestite per valore o solo per riferimento.
Esempio
Il seguente programma illustra l'uso di base dei puntatori.
(171-punt.go):
package main
import "fmt"
func main() {
var a int = 5
// var p *int
// var q *int
p := &a
fmt.Printf("valore di a: %d\n", a)
fmt.Printf("indirizzo di a: %d\n", &a)
fmt.Printf("valore di p: %d\n", p)
fmt.Printf("valore di *p: %d\n", *p)
// fmt.Printf("valore di *q: %d\n", *q)
}
Il codice var p *int
asserisce che p è un puntatore ad interi. Il compilatore deve sapere sia che p è un puntatore, cioè un riferimento a variabile, sia di che tipo è la variabile a cui si riferisce.
Il codice p = &a
inizializza il puntatore p con l'indirizzo di a. Se il membro sinistro di un'assegnazione è un puntatore, il membro destro deve essere un indirizzo.
Si può anche scrivere p := &a
e il compilatore deduce il tipo di puntatore dal tipo della variabile.
Un puntatore deve essere sempre, nell'ordine:
- dichiarato
- inizializzato
- usato
Un puntatore dichiarato ma non ancora inizializzato vale nil
, cioè l'idirizzo di memoria 0.
In Linux, l'indirizzo zero non è mai allocato nella mappa di segmenti di nessun processo. Il tentativo di dereferenziare l'indirizzo zero produce l'errore runtime Segment Violation con conseguente panic.
Scommentando i due commenti sopra si può vedere questo effetto.
Funzioni con puntatori
Il seguente programma illustra l'uso di funzioni che prendono un putatore come parametro.
Una funzione con parametro normale (non puntatore), che modifica il valore di tale parametro, ha modificato una copia e non il valore nel codice invocante.
Se il parametro di una funzione è un puntatore, la sua modifica è anche la modifica del valore del codice chiamante.
(170pointers.go):
package main
import "fmt"
// La funzione riceve come argomento un valore intero
// Passaggio parametri per copia o valore
func zeroval(ival int) {
ival = 0
}
// La funzione riceve come argomento un puntatore a intero
// Passaggio parametri per riferimento
func zeroptr(iptr *int) {
*iptr = 0
}
func main() {
i := 1
fmt.Println("initial:", i)
// Invocazione per valore
zeroval(i)
fmt.Println("zeroval:", i)
// Invocazione per riferimento
// '&i' è l'indirizzo di i
// Se il parametro formale è un puntatore
// il parametro attuale deve essere un indirizzo
zeroptr(&i)
fmt.Println("zeroptr:", i)
// E' possibile stampare l'indirizzo
fmt.Println("pointer:", &i)
// Gli indirizzi sono già stampati in esadecimale, come da
fmt.Printf("hex: %x\n", &i)
// se si vuole stamparli in decimale:
fmt.Printf("dec: %d\n", &i)
}
// Il comportamento è come quello del linguaggio C
// La funzione Printf è come quella equivalente del C
Le tre fasi di impiego di un puntatore qui sono:
- dichiarazione - nella dichiarazione di parametro della funzione
- inizializzazione - nel passaggio di parametri alla invocazione della funzione: se il parametro formale è un puntatore, il parametro attuale deve essere un indirizzo
- uso - nel codice della funzione
Una funzione deve ricevere un parametro formale per riferimento (un puntatore) quando è suo compito modificare (scrivere) il valore originale del corrispondente parametro attuale. Dovrebbe invece sempre ricevere un parametro formale per valore quando deve solo leggere senza modificare il corrispondente parametro attuale.
new
Una variabile gestita tramite puntatore può essere generata con la funzione built-in del linguaggio new
, come nel segmento di codice:
func one(xPtr *int) {
*xPtr = 1
}
func main() {
xPtr := new(int)
one(xPtr)
fmt.Println(*xPtr) // x vale 1
}
new
vuole sapere dal suo argomento quanta memoria dinamica allocare e ritorna un puntatore a tale memoria dinamica allocata. L'argomento di new può essere un tipo, nel qual caso viene allocata esattamente la memoria necessaria per lo storaggio di variabili di quel tipo.
In effetti è raro usare new con tipi semplici, ma è molto comune con struct e con slice e map.
Swap con puntatori e senza
Il metodo tradizionale di compiere lo swap di due valori è tramite una funzione che usa puntatori. Si può ancora procedere in tale modo.
Go ha un nuovo metodo molto più semplice.
(172-swap-punt.go):
package main
import "fmt"
func swap(x, y *int) {
t := *x
*x = *y
*y = t
}
func main() {
a := 10
b := 20
fmt.Println("Metodo antico")
fmt.Printf("Prima: a=%d, b=%d\n", a, b)
swap(&a, &b)
fmt.Printf("Dopo: a=%d, b=%d\n", a, b)
a = 10
b = 20
fmt.Println("Metodo nuovo")
fmt.Printf("Prima: a=%d, b=%d\n", a, b)
b, a = a, b
fmt.Printf("Dopo: a=%d, b=%d\n", a, b)
}
Fantastico! Non è nemmeno una funzione che ritorna due valori, che già è impensabile in C, C++ e Java. E' un'assegnazione multipla combinata, che nessuno dei tre citati linguaggi neanche si sogna.