Punteros: Consejos y errores comunes

Este texto no es mío, lo he encontrado buscando una información y me ha parecido muy interesante compartirlo. La fuente original es http://www.chuidiang.com/clinux/funciones/punteros.php, y si bien está pensado para C es perfecta y fácilmente extrapolable a cualquier otro lenguaje.

Una cosa es lo que nos cuentan los libros de C sobre punteros y otra los problemas prácticos que se nos plantean cuando nos ponemos a programarlos. Los punteros son además una cosa muy delicada, cualquier pequeño despiste con ellos puede hacer que nuestro programa se «caiga» inesperadamente o de resultados muy extraños.

Aunque en los ejemplos de código que pongo a continuación, al ir las líneas seguidas, se ve claramente el error (al menos, esa es la intención), lo habitual es que estas líneas erroneas estén separadas en el código, incluso en funciones distintas, con lo que no es tan evidente el verlas.

Todo lo que se dice aquí, aunque esté explicado para C con las funciones malloc() y free(), se puede aplicar a C++, usando new y delete. Donde hablamos de estructuras, podemos hablar de clases.

Concepto de puntero y primeros errores

Podemos imaginar un puntero como una flecha. Esa flecha apunta a una dirección de memoria. Por ejemplo, si declaramos en C el puntero

char *puntero;

tenemos declarada una «flecha» que apunta a una dirección de memoria. ¿A cual?. Aquí se nos presenta el primer problema práctico con los punteros. Tal cual está declarado, esa flecha apunta a cualquier dirección de memoria, al azar. Lo habitual es que sea la dirección de memoria 0 (cero), pero puede ser cualquiera.

Si inmediatamente después de declararlo intentamos guardar algo en la dirección de memoria a la que apunta, como por ejemplo

*puntero = 'A';

pueden pasarnos dos cosas:

Que la dirección aleatoria a la que apunta puntero pertenezca a nuestro programa. En ese caso se meterá la ‘A’ en la dirección y aparentemente no ha pasado nada. Lo que realmente pasa es que estamos metiendo un byte 65 (código ascii de la ‘A’) en algún sitio de nuestro programa (código, zona de variables, etc). El error puede presentarse en cualquier otro lada de nuestro programa, en un sitio aparentemente correcto.
Que la dirección aleatoria a la que apunta puntero no pertenezca a nuestro programa. Esto es lo mejor que nos puede pasar. En el momento de la asignación nuestro programa se «caerá» y dará un error de violación de memoria o similar. Al ser en el momento de la asignación, podremos corregirlo más fácilmente, ya que sabemos en qué línea se ha producdo el fallo.
Por ello, como primer consejo práctico:
Inicializar todos los punteros al declararlos, por ejemplo, a NULL

char *puntero = NULL;

Si nos olvidamos de hacerle apuntar a una dirección de memoria adecuada, nos dará error en el momento de utilizarlo, y no después, en otro lado del programa. En general, casi todos los consejos que doy van orientados a poder depurar el programa con más facilidad. Se trata de conseguir que el programa se «caiga» en la instrucción incorrecta y no que dé resultados erroneos o se «caiga» en otro sitio que no tiene nada que ver.

Apuntar un puntero a una dirección de memoria

De lo comentado anteriormente, vemos que siempre debemos hacer que un puntero apunte a una dirección de memoria válida antes de utilizarlo. Para ello tenemos dos posibilidades:

Apuntarlo a una dirección de memoria ya reservada para nuestro programa. Para ello basta asignarlo a la dirección de cualquier variable que tengamos declarada o igualarlo a otro puntero que ya esté apuntando a una dirección adecuada. Por ejemplo

char unCaracter; 
char *puntero = NULL; 
... 
puntero = &unCaracter;

Ahora puntero apunta a la dirección de memoria en la que está unCaracter. Podemos utilizar con seguridad la memoria a la que apunta puntero, sabiendo que lo que pongamos ahí también se está poniendo en unCaracter.

*puntero = 'A';  /* Ahora unCaracter también tiene una 'A' */

Reservar una zona de memoria específica para nuestro puntero y hacer que apunte a ella. Para ello tenemos la función malloc(). La función malloc() nos reserva una zona de memoria del tamaño que le indiquemos y nos devuelve su dirección. Podemos hacer que nuestro puntero apunte a esa dirección

puntero = malloc(...);  /* No pongo los parámetros ... */ 
... 
*puntero = 'A';

Una vez reservada la zona de memoria, podemos utilizarla con seguridad. Si no queremos que nuestro programa consuma más memoria de la cuenta, debemos acordarnos de liberarla cuando no la necesitemos más. Para ello está la función free() a la que se le pasa la dirección de memoria que queremos liberar.

free (puntero);

Todo esto es muy bonito y es básicamente lo que nos puede contar cualquier libro de C. Sin embargo, hay varios «problemas» que se nos pueden presentar.

Si hacemos que nuestro puntero apunte a una variable local, cuando la variable desaparezca, nuestro puntero queda apuntando a una dirección de memoria que ya no es válida. Por ejemplo

char * funcion () 
{ 
    char resultado; 
    char *puntero; 
  
  
    resultado = algun_valor;

    puntero = &resultado;
    return puntero; 
}

Esto es una fuente segura de problemas. La variable resultado tiene sentido dentro de la función, pero al terminar la función, desaparece la variable, puntero apunta a la dirección de memoria que ocupaba esa variable. Cuando intentemos usar el puntero devuelto por la función, esa memoria ya está libre y es posible que alguien la sobre-escriba, haciendo que su valor sea aleatorio.

Cualquier variante de esa función también da problemas. Por ejemplo, es igual de incorrecto este código

char * funcion () 
{ 
    char resultado; 
    /* Aqui rellenamos resultado con el valor deseado */ 
    return &resultado; 
}

Al salir de la función, la dirección que estamos devolviendo ya no tiene sentido.
Además, esto nunca nos dará un error de violación de memoria, ya que esa zona de memoria pertenecía a nuestro programa. Nuestro programa no se «caerá», simplemente dará resultados incorrectos.

No devolver nunca punteros a variables locales a una función.

Al liberar una zona de memoria con free() nuestro puntero queda apuntando a una dirección que ya no es correcta, free() no lo hace apuntar a NULL automáticamente. Utilizarla posteriormente dará problemas. Por ejemplo:

char *puntero = NULL; 
puntero = malloc(); 
free (puntero); 
*puntero = 'A';

Esto no dará ningún problema de violación de memoria, puesto que la memoria a la que apunta puntero era nuestra. Sin embargo, alguien puede posteriormente sobre-escribir en esa dirección de memoria. Este problema se agrava si no lo hacemos todos seguido. Imaginemos que hemos liberado puntero y que en otra función o más adelante en el código intentamos usarlo.
Apuntar a NULL los punteros después de liberarlos

free (puntero); 
puntero = NULL;

Si lo apuntamos a NULL después de hacer el free(), cuando lo intentemos utilizar de forma incorrecta, el programa se caerá inmediatamente, con lo que será más sencillo de depurar.

Liberar dos veces la misma zona de memoria puede dar montones de problemas. Cuando liberamos por segunda vez, el programa no da ningún error, pero deja «corrupto» al gestor de memoria. Lo más probable, al menos en solaris (unix de sun), es que posteriormente el programa nos dé fallo en otro malloc(). Por ejemplo:

char *puntero1 = NULL; 
char *puntero2 = NULL;

puntero1 = malloc(); 
puntero2 = puntero1;

free (puntero1); 
puntero1 = NULL;

free (puntero2); 
puntero2 = NULL;

Al liberar puntero1, liberamos nuestro espacio de memoria. Sobra la liberación de puntero2, ya que el espacio de memoria al que apunta ya está liberado. Esto no dará ningún error en la ejecución del programa, pero más adelante tendremos problemas en algún malloc() o free().

Es bastante habitual hacer que una función reserve un espacio de memoria y lo devuelva. Luego nuestro código usará ese resultado en varios sitios e, inadvertidamente, podemos liberarlo en dos sitios distintos o dejarlo sin liberar. Es necesario, cuando hacemos un malloc(), tener claro quién va a liberar esa memoria y dónde, para evitar este tipo de problemas.

Por cada malloc(), hacer un único free().
Cuando reservemos memoria con malloc(), decidir claramente quién la va a liberar y cuándo.

Punteros dentro de estructuras

Los punteros dentro de estructuras, si se utilizan descuidadamente, son fuente de problemas. Pongamos por ejemplo una estructura como la siguiente

struct Datos 
{ 
    char *nombre; 
    int otroCampo; 
}

Todo lo dicho hasta ahora para punteros, vale para el que está dentro de la estructura. Si declaramos una variable de tipo Datos, el puntero nombre está sin inicializar.

struct Datos unNombre; 
unNombre.nombre = NULL;

y antes de usarlo reservar espacio para él

unNombre.nombre = malloc();

o bien asignarlo a alguna variable adecuada.

unNombre.nombre = &algunaVariableAdecuada;

El problema principal con las estructuras surge cuando las copiamos o asignamos. Supongamos el siguiente código

struct Datos unNombre; 
unNombre.nombre = NULL; 
struct Datos otroNombre; 
otroNombre.nombre = NULL; 
... 
unNombre.nombre = malloc(); 
... 
otroNombre = unNombre;

La última asignación copia todos los campos de la estructura unNombre en otroNombre, incluido el puntero interno. Ambos punteros van a apuntar a la misma dirección de memoria. Cambiar el contenido de uno de ellos implica cambiar el contenido del otro. El problema se presenta si liberamos uno de ellos

free (unNombre.nombre); 
unNombre.nombre = NULL;

Con esta acción también hemos liberado la memoria a la que apunta otroNombre.nombre, por lo que su contenido puede no ser válido. Utilizar o liberar posteriormente otroNombre.nombre nos dará los problemas que ya hemos mencionado.

En C++, si utilizamos clases con algún atributo puntero, tenemos algunos trucos que podemos utilizar. Uno de ellos consiste en definir el operator = () y constructores copia para que hagan una copia de los datos a los que apunta el puntero, y no sólo del puntero. Por ejemplo

class Datos 
{ 
    protected: 
        char *nombre; 
};

funciona exactamente igual que una estructura, con los mismos problemas al hacer asignaciones. Sin embargo

class Datos 
{ 
    public: 
        /* Constructor defecto */ 
        Datos() 
        { 
            nombre = NULL; 
        }

        /* Constructor copia */ 
        Datos (Datos &original) 
        { 
            *this = original;  // Llama al operador de asignación, más abajo. 
        }

        /* Destructor, Libera nombre si no es NULL */ 
        ~Datos() 
        { 
            if (nombre != NULL) 
            { 
                delete [ ] nombre; 
                nombre = NULL; 
            } 
        }

        /* Asignación entre instancias de la clase */ 
        Datos &operator = (Datos &original) 
        { 
            /* Se debería verificar si original.nombre tiene o no contenido antes de hacer la copia. 
            por simplicidad no no hago */ 
            nombre = new char[strlen (original.nombre)+1]; 
            strcpy (nombre, original.nombre); 
            return *this; 
        }

    protected: 
        char *nombre; 
};

Esto así es mucho más seguro. Cada instancia de la clase hace su propio new[] y delete[] y tiene su propia zona de memoria reservada, con lo que es más difícil «equivocarse». La pega de esto es la «ineficiencia». El mismo dato estará repetido en varias clases, con el consiguiente consumo de memoria. De todas formas, salvo para datos excesivamente grandes o aplicaciones muy críticas en memoria, es mejor evitarse problemas definiendo constructores copia y operator = ()

En C no tenemos esta facilidad, pero podemos hacer funciones del tipo copiaEstructura (estructuraOrigen, estructuraDestino) o liberaEstructura (estructura) que se encargen de hacer estas copias de los punteros y de liberarlos correctamente. La otra opción es ser muy cuidadosos al programar.

Para punteros dentro de estructuras o clases, hacer funciones o métodos
adecuados para su tratamiento.

Paso de punteros parámetro

En C, aunque no lo parezca, todos los parámetros se pasan siempre por copia. Para hacer que tanto fuera de una función como dentro se pueda acceder a la misma variable, hay que pasar un puntero a esa variable. Sin embargo, el puntero en sí mismo se está pasando por copia. Veamos esto en un ejemplo:

void funcion1 (char *p1) 
{ 
    *p1 = 'B'; 
}

void funcion2 () 
{ 
    char *p2 = NULL; 
    char unaVariable = 'A'; 
    p2 = &unaVariable; 
    ... 
    funcion1 (p2); 
}

En este ejemplo p2 apunta a la variable unaVariable. Llamamos a funcion1() pasándole el puntero p2 y dentro actuamos sobre su contenido. Tanto p1 como p2 apuntan a la misma dirección de memoria (unaVariable), por lo que *p1=’B’ afecta a unaVariable y *p2. por

Sin embargo, p1 y p2 son punteros distintos. Si dentro de funcion1() hacemos que p1 apunte a otro sitio, por ejemplo, con cualquiera de las siguientes cosas:

p1 = NULL; 
p1 = malloc(); 
p1 = &otraVariable;

sólo estamos tocando p1. El puntero p2 permanece inalterado, sigue apuntando a unaVariable.

Esto tan simple suele dar lugar a errores. Es habitual tratar de devolver algún puntero pasándolo como parámetro. Por ejemplo, se podría pretender que funcion1() creara la memoria con malloc() y luego intentar usarla con p2

void funcion1 (char *p1) 
{ 
    p1 = malloc(...); 
    strcpy (p1, "Hola mundo\n"); 
} 

void funcion2 () 
{ 
    char *p2 = NULL; 
    funcion1 (p2); 
    printf ("%s", p2); 
}

Cuando salimos de funcion1(), p2 sigue apuntando al mismo sitio, a NULL. El programa fallará en el printf().

Si queremos pasar por parámetro un puntero y que la función nos lo altere (el puntero, no su contenido), debemos pasar un puntero al puntero. La sintaxis es un poco más liada, pero sería algo así como esto:

void funcion1 (char **p1) 
{ 
    *p1 = malloc( ...); 
    strcpy (*p1, "Hola mundo\n"); 
}

void funcion2 () 
{ 
    char *p2 = NULL; 
    funcion1 (&p2);        /* Advertir el & delante de p2 */ 
    printf ("%s", p2); 
}

Esto sí funciona correctamente.

1 comment for “Punteros: Consejos y errores comunes

Deja un comentario