22 - Punteros - Variables de tipo puntero

Un tema importante del lenguaje Go es el manejo del concepto de punteros.

Los punteros en el lenguaje Go son esenciales:

  • Para que un programa sea muy eficiente.
  • Modificar variables de tipo int, float64, string etc. en otras funciones.
  • Poder requerir memoria durante la ejecución del programa (hay muchas situaciones donde no sabemos cuanto espacio reservar)

Definición de puntero.

Una variable de tipo puntero almacena la dirección de memoria de otra variable que puede ser int, float64, string etc.

Es difícil en un principio entender y ver las ventajas que trae trabajar con punteros pero su uso es fundamental si vamos a trabajar con el lenguaje Go.

Definición de punteros a tipos de datos int y float64.

La sintaxis para definir una variable de tipo puntero se logra antecediendo el caracter * al nombre de la variable:

    var pe *int 
    var pf *float64

Problema 1:

Definir dos variables enteras y almacenar valores por asignación. Definir una variable puntero a entero y guardar sucesivamente las direcciones de dichas dos variables y acceder a sus valores.

Programa: programa106.go

package main

import "fmt"

func main() {
    var valor1 int = 10
    var valor2 int = 20
    var pe *int
    pe = &valor1
    fmt.Println("Lo apuntado por pe es:", *pe)
    fmt.Println("La direccion que almacena pe es:", pe)
    pe = &valor2
    fmt.Println("Lo apuntado por pe es:", *pe)
    fmt.Println("La direccion que almacena pe es:", pe)
}

La ejecución de este programa tiene una salida similar a:

punteros a int

Haremos una serie de problemas para entender el concepto de una variable de tipo puntero que en principio no le encontraremos mayores ventajas a como veníamos resolviendo problemas. Lo más conveniente es empezar con problemas muy sencillos para luego ver la verdadera potencia que nos proporcionan las variables de tipo puntero.

En este problema definimos dos variables de tipo entera y las inicializamos con los valores 10 y 20:

    var valor1 int = 10
    var valor2 int = 20

Es lo mismo si utilizamos la sintaxis implícita para definir las dos variables enteras:

    valor1 := 10
    valor2 := 20

Nosotros ya hemos visto que si queremos acceder e imprimir el contenido de la variable "valor1" lo hacemos por medio de su nombre:

    fmt.Println(valor1)

Hay otra forma de acceder al contenido de la variable "valor1" mediante el concepto de una variable de tipo puntero. Definimos una tercer variable llamada pe (una variable puntero a entero):

    var pe *int

La variable "pe" puede almacenar una dirección de memoria, es incorrecto (no podemos asignar valor1 a pe):

pe = valor1

pe y valor1 son variables de distinto tipo, valor1 es una variable int y pe es una variable que apunta a una dirección de memoria donde se almacena un int.

Para almacenar en la variable pe la dirección de memoria donde se almacena valor1 utilizamos el caracter &:

    pe = &valor1

La línea anterior la podemos leer como: "en la variable pe almacenamos la dirección de memoria donde se almacena valor1".

Si vemos el contenido de la memoria podemos identificar dos variables "valor1" y "pe":

punteros a int

La variable "valor1" almacena el número entero 10. La variable "valor1" se almacena por ejemplo en la dirección de memoria 1000 (este valor es a modo de ejemplo y la dirección real sucede cuando se ejecuta el programa)

La variable "pe" puede almacenar la dirección de una variable entera, cuando hacemos la asignación:

    pe = &valor1

Estamos guardando la dirección 1000 en la variable "pe".

Para imprimir lo apuntado por el puntero pe utilizamos la sintaxis *pe, en nuestro caso accedemos al valor 10 almacenado en valor1:

    fmt.Println("Lo apuntado por pe es:", *pe)

Imprimir el contenido de pe puede tener poco sentido ya que veremos la dirección que almacena pe, en el caso que lo hagamos debemos indicar:

    fmt.Println("La direccion que almacena pe es:", pe)

Una variable de tipo puntero puede cambiar la dirección que almacena a lo largo de la ejecución del programa, luego si hacemos:

    pe = &valor2

Estamos almacenando la dirección de la variable valor2. Si imprimimos lo apuntado por pe tendremos el número 20:

    fmt.Println("Lo apuntado por pe es:", *pe)

Problema 2:

Definir dos variables enteras y no inicializarlas.
Definir una variable puntero a entero, hacer que apunte sucesivamente a las dos variables enteras definidas previamente y cargue sus contenidos.
Imprimir las dos variables enteras.

Programa: programa107.go

package main

import "fmt"

func main() {
    var x1, x2 int
    var pe *int
    pe = &x1
    *pe = 100
    pe = &x2
    *pe = 200
    fmt.Println("Primer variable entera:", x1)
    fmt.Println("Segunda variable entera:", x2)
}

Definimos dos variable enteras y no les cargamos valor (recordemos que se carga un cero por defecto cuando definimos variables de tipo int):

   var x1, x2 int

Definimos una variable puntero a entero:

    var pe *int

Almacenamos en la variable puntero la dirección de la variable x1:

    pe = &x1

Modificamos el contenido de x1 accediendo a su contenido mediante el puntero pe:

    *pe = 100

Modificamos el contenido de la variable puntero pe con la dirección de la variable x2:

    pe = &x2

Modificamos ahora el contenido de x2 accediendo por medio de puntero pe:

    *pe = 200

Imprimimos los contenidos de x1 y x2:

    fmt.Println("Primer variable entera:", x1)
    fmt.Println("Segunda variable entera:", x2)

Entiendo que este programa es más eficiente si directamente cargamos x1 y x2 asignándole las valores 100 y 200, pero tengamos presente que lo más importante es entender el concepto de punteros la la sintaxis para obtener la dirección de una variable (&) y acceder a lo apuntado por medio de (*)

Problemas propuestos

  • Se tienen el siguiente programa:

    package main
    
    import "fmt"
    
    func main() {
        var s1 string = "uno"
        var s2 string ="dos"
        var ps *string
        ps = &s1
        fmt.Println(s1) //se imprime: ?
        *ps = "tres"
        fmt.Println(s1) //se imprime: ?
        s1 = "cuatro"
        fmt.Println(*ps) //se imprime: ?
        ps = &s2
        fmt.Println(*ps) //se imprime: ?
    }
    Indicar que valor se imprime en cada llamada a Println
  • Se tienen el siguiente programa:

    package main
    
    import "fmt"
    
    func main() {
        var f int
        var pe *int
        pe = &f
        for *pe = 1; *pe <= 10; *pe = *pe + 1 {
            fmt.Println(f)  //se imprime ? ? ? ? ? ? ? ? ? ?
        }
    }
    
    Indicar que valor se imprime en cada llamada a Println
  • Se tienen el siguiente programa:

    #package main
    
    import "fmt"
    
    func main() {
        var z1, z2 float64
        var pf *float64
        pf = &z1
        *pf = 10.2
        pf = &z2
        *pf=20.90
        fmt.Println(z1, z2) // Se imprime ? ?
    }
    
    Indicar que valor se imprime en la llamada a Println
Solución
programa108.go

package main

import "fmt"

func main() {
    var s1 string = "uno"
    var s2 string ="dos"
    var ps *string
    ps = &s1
    fmt.Println(s1) //se imprime: uno
    *ps = "tres"
    fmt.Println(s1) //se imprime: tres
    s1 = "cuatro"
    fmt.Println(*ps) //se imprime: cuatro
    ps = &s2
    fmt.Println(*ps) //se imprime: dos
}




programa109.go

package main

import "fmt"

func main() {
    var f int
    var pe *int
    pe = &f
    for *pe = 1; *pe <= 10; *pe = *pe + 1 {
        fmt.Println(f)  //se imprime 1 2 3 4 5 6 7 8 9 10
    }
}
//si queremos utilizar el operador ++ con lo apuntado debe ser: *pe++




programa110.go

package main

import "fmt"

func main() {
    var z1, z2 float64
    var pf *float64
    pf = &z1
    *pf = 10.2
    pf = &z2
    *pf=20.90
    fmt.Println(z1, z2) // Se imprime 20.2    20.9
}