Ejemplos java y C/linux

Google
 

Tutoriales

Enlaces

Licencia

Creative Commons License
Esta obra está bajo una licencia de Creative Commons.
Para reconocer la autoría debes poner el enlace http://www.chuidiang.com

Conceptos y Consejos prácticos para el uso correcto de punteros

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:

 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:

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.

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.

Depuración

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í.

 

Estadísticas y comentarios

Numero de visitas desde el 4 Feb 2007: