Punteros

Tema Punteros Versión 1.00
Resumen El presente documento busca profundizar el tema de punteros en lenguaje C.
Sistema Operativo Cualquiera
Autor Gabriel Agustín Praino Fecha 30/11/2001
Búsqueda punteros, C

El presente material se encuentra inscripto en el Registro Nacional de Propiedad Intelectual. Todos los derechos reservados. La reproducción de la presente publicación se encuentra sujeta a los términos establecidos en la página principal de la presente obra. Prohibida la reproducción total o parcial de esta publicación en forma independiente.

Punteros

El tema de la utilización correcta de punteros suele traer no pocos inconvenientes a quienes recién comienzan a programar. Más aún, la relación existente entre arrays y punteros suele parecer bastante confusa y llevar a errores de programación difíciles de encontrar. Por ello dedicaremos unas líneas a analizar la relación existente entre los punteros y los arrays, la forma de utilizarlos y las situaciones y problemas más frecuentes que suelen presentarse. Comencemos con un ejemplo sencillo:

void Funcion(void)
{
	unsigned Tam1, Tam2;
	char szString1[100];
	char *szString2;
	Tam1 = sizeof (szString1);
	Tam2 = sizeof (szString2);
	...

En este caso, szString1 será un string de 100 caracteres, en tanto que szString2 será un puntero a caracter (una variable que almacena una dirección de memoria donde se encuentra almacenado un caracter). Si esto está claro, ¿podría decir qué valores tomarán las variables Tam1 y Tam2?

Pués bien, Tam1 tomará el valor correspondiente al tamaño del array szString1, es decir, 100, en tanto que Tam2 tomará el valor correspondiente al tamaño de una dirección de memoria, 2 ó 4 en la mayoría de los casos. Surge entonces una paradoja. Cómo es posible realizar una operación del tipo:

szString2 = szString1; /* Esto es correcto */

Notar que estas dos variables tienen diferente tamaño, ¿cómo es posible entonces copiar su contenido? La respuesta debe buscarse en la forma en que se implementan los arrays. En C, todo array consiste en un puntero a la posición donde se encuentran los datos, es decir que szString1 es la dirección de memoria de la primer elemento del array.

El lector atento notará que en el párrafo anterior se dice que szString2 es una variable que almacena una dirección de memoria (puntero), en tanto que szString1 es directamente una dirección de memoria, que por ser un valor no podrá ser modificada. Es decir que la operación inversa no es válida:

	szString1 = szString2; /* Esto es incorrecto */

Veamos otro ejemplo. Si suponemos que existe una función:

void PrintString(char *s);

y ambas variables tienen significados diferentes, ¿cuál de las siguientes llamadas es la correcta?

PrintString(szString1);
PrintString(szString2);

La respuesta es que ambas son correctas, ya que el pasaje de parámetros a funciones se realiza por valor, y por ende ambas funciones pasarán una dirección de memoria, que será cargada en la variable s de la función PrintString(char *s);

¿Y qué hubiese pasado si hubiese definido la función PrintString() como sigue?

void PrintString(char s[100]);

La respuesta es que no hubiese pasado nada diferente. Recordemos que un array es la dirección de memoria del primer elemento. Cuando se define un array en una llamada a una función, esta dirección de memoria será recibida como parámetro, y por ende no se reserva memoria. Es decir que los elementos del array no serán copiados, sino que s contendrá la dirección de memoria especificada al llamar a la función, y cualquier modificación que se haga sobre el array se mantendrá al salir de la función. La única diferencia que existe entre el caso anterior y este es que en el anterior la variable s (por ser un puntero) puede ser modificada, en tanto que en el segundo caso, la dirección de memoria s no.

Y bien, ya que al llamar a la función no se reservar memoria, ¿para qué sirve especificar el valor 100 dentro de la función? La respuesta es que no tiene ninguna utilidad. Daría lo mismo especificar 10, 100 o cualquier otro valor, o incluso nada, tal como se muestra a continuación:

	void PrintString(char s[]);

¿Es decir que siempre puede omitirse el tamaño del array?

La respuesta es que siempre se lo puede hacer en el último tamaño del array. Por ejemplo, si estuviese pasando una matriz, debería pasar la cantidad de columnas, tal como se muestra a continuación:

	void Print (char s[][5]);

En este caso el 5 no es opcional. La función recibirá efectivamente una dirección de memoria, y no le interesa cuanta memoria haya alocada (por eso el primer parámetro no se pone), pero sí necesita saber cuántos elementos existen en cada fila para poder saber, por ejemplo, que s[1][1] se refiere al séptimo elemento, desde el comienzo del área de memoria.

Continuando con las diferencias, szString1 ya reserva un área de memoria donde almacenar los datos en el momento de su creación (concretamente en el stack o pila del sistema (al igual que todas las variables), salvo que formen parte de una estructura), en tanto que szString2 no lo hace y deberá hacerlo el programador. Si se utiliza la función malloc() para reservar memoria, esta será tomada del área de datos del sistema.

Veamos otro ejemplo con punteros:

char szString1[10] = "Hola";
char *szString2 = "Hola";
char *szString3 = (char *)malloc(10);
strcpy (szString3, "Hola");

¿Qué diferencias tienen estos 3 strings? Algunas de ellas ya las mencionamos. Las direcciones de memoria almacenadas en szString2 y szString3 pueden ser modificadas, en tanto que la dirección de memoria szString1 no. La variable szString1 será creada en la pila del sistema, en la cual se cargará inicialmente los caracteres ‘H’, ‘o’, ‘l’, ‘a’, ‘\0′, en tanto que szString3 ocupará un área en la memoria de datos del sistema. Pero, ¿dónde estará szString2? La respuesta es que esta variable tendrá la dirección de memoria del código ejecutable donde estén almacenados los 5 caracteres “Hola\0″. ¿Qué pasará si tratamos de modificar estas variables tal como se muestra a continuación?

strcpy (szString1, "Chau");
strcpy (szString2, "Chau");
strcpy (szString3, "Chau");

En el primer y tercer caso se modificarán áreas de memoria válidas, ya sea pila o área de datos, pero en el segundo se estará modificando el programa, es decir el código ejecutable. Esto puede tener diferentes consecuencias. En D.O.S. nada impide modificar cualquier área de la memoria, con lo cual el programa será efectivamente modificado y las consecuencias son imprevisibles. En un sistema con memoria protegida (Windows/UNIX) se producirá una excepción que interrumpirá la ejecución normal del software, y si la misma no es controlada (un tema que se verá más adelante) el programa se interrupirá.

Volviendo al ejemplo anterior, se definió:

char szString1[10] = "Hola";

lo cual significa que este array tomará como valor inicial estos carateres. El C permite especificar valores iniciales para un array en el momento de su creación, los cuales son cargados en el array, lo cual no puede hacerse después. Es decir, que luego de la creación del array, el siguiente código es incorrecto.

szString1 = "Hola"; /* Este código es incorrecto */

Pero sí puede efectuarse las operaciones:

szString2 = "Hola";
szString3 = "Hola";

Ya que las mismas no cargan los datos, sino que modifican el puntero cargando la dirección de memoria donde estén definidos estos strings en el código ejecutable.

Se dijo anteriormente que los arrays se crean en la pila del sistema, siempre y cuando no formen parte de una estructura. Por ejemplo:

struct str{
	char Nombre[100];
};

Nombre ocupará un lugar de memoria donde sea creada la estructura.

Como una tabla de ayuda se resumen estos puntos en la siguiente tabla:

Array Puntero a área de datos Puntero a área de ejecutable
Ejemplo char s[100]; char *s;
s = (char*)malloc (100)
char *s = “Texto”;
Tipo dirección (valor) puntero (variable que almacena un valor)
sizeof() 100 2 ó 4 generalmente
Espacio de memoria utilizado por la variable ‘s’ ninguno pila del sistema pila del sistema
Espacio de memoria utilizado por los datos apuntados por ‘s’ pila del sistema área de datos código ejecutable
La dirección puede ser modificada no
Los datos pueden ser leídos
Los datos pueden ser modificados no
Ventajas No requiere alocar/dealocar memoria en el área de datos (sino en la pila, lo cual es inmediato). Tamaño de string variable. Los datos ya están cargados y no debe alocarse ni liberarse memoria.
Desventajas La pila del sistema puede llenarse. El tamaño del string debe ser especificado durante la compilación. El proceso de alocar/dealocar memoria es costoso y contribuje a fragmentar la memoria del sistema. Los datos no pueden ser modificados.