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.
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:
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.
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:
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' */
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.
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;
}
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.
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.
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.
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.
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.
En la parte de trucos C++ tienes una sugerencia de cómo encontrar punteros descarriados.
Estos son básicamente los errores con los que me he tropezado o he visto a mis compañeros tropezarse cuando empezabamos con los punteros. Si conoces algún otro error típico, envíame un correo y lo pondré por aquí.