Utilidad de los punteros

Aritmetica de punteros

Son posibles ciertas operaciones con punteros y los operadores de suma y resta (-,+,--,++). Siendo 'T' el tipo a que apunta el puntero, el siguiente cuadro sintetiza las distintas posiblidades y el tipo de resultado generado:

Operacion

Resultado     

Comentarios

Puntero +- entero

Puntero a T mas entero       

Puntero a T

Si el puntero apunta mas alla del limite superior del
array el resultado es no definido
Puntero a T menos entero

Puntero a T

Si el puntero apunta mas alla del limite inferior
del array el resultado es no definido

Puntero +- puntero

Puntero mas puntero

-------------

No permitido

Puntero a T menos puntero a T  

Entero

El resultado indica el numero de elementos T entre
los dos punteros

Los punteros son direcciones de memoria pero la aritmetica de punteros no es una simple suma o resta de direcciones. Estas operaciones estan adaptadas especialmente para tratar con arrays, de ahi que incrementar en 1 el valor de un puntero no apunte a la proxima direccion de memoria, sino al proximo elemento de un array. El unico caso donde 'proxima direccion' es igual a 'proximo elemento' es el caso de un array de caracteres, los restantes tipos (por lo menos los propios del lenguaje) ocupan mas de un byte por elemento.

Puntero +- entero

Es el tipo de operacion mas frecuente con punteros, especialmente porque el incremento del puntero en 1 permite recorrer un array elemento por elemento. Hay dos modos de realizar esto, el primero consiste en modificar el valor del puntero, y el segundo en direccionar el elemento igual a [puntero+ entero], procedimiento que tiene la ventaja relativa de no modificar el valor inicial del puntero, que seguira apuntando al mismo elemento del array. Por ejemplo:

long k [4] = {35,34524,543594,354};
long* pk = k;
int a;
//Primer modo -------------------------------------
for {a=0;a<4;a++) {
printf("%ld",*pk);
pk++;
}
//Segundo modo--------------------------------------
for (a=0;a<4;a++){
printf("%ld, *(pk+a));
}

En ambos casos se obtiene el mismo resultado, pero al salir del bucle el estado del puntero sera diferente segun la modalidad adoptada. En el primer caso el puntero estara apuntando fuera del array (pk+4), en el segundo el puntero seguira apuntando al comienzo del array, pues las direcciones sucesivas se tomaban del valor temporal de (pk+a), sin afectar al valor del puntero. En el bucle de la segunda modalidad es importante la presencia del parentesis, la notacion *pk+a  tendria un efecto por completo diferente, el operador '*' tiene mayor precedencia que '+', por lo tanto se tomaria siempre el elemento *pk (el primer elemento del array) y luego se le sumaria 'a'.

Debe quedar claro que el puntero se puede incrementar con cualquier valor entero, hay rutinas que necesitan tomar un elemento por medio, en tal caso dentro de un bucle tomariamos los sucesivos valores (p+2). O si necesitaramos obviar los primeros 'n' caracteres de una cadena, y copiar el resto en un buffer, podriamos escribir:  strcpy(buffer, cad+n); La suma de enteros a punteros tiene muchas aplicaciones, en especial si se combinan con las librerias standard de C y C++.

En la tabla previa se menciona que de darse el caso de desbordar el limite del array el resultado de la suma (o resta) es 'indeterminado'. Esto puede depender en parte de cada compilador, pero como norma general lo que es indeterminado no es el valor-resultado del puntero (la direccion que almacena), sino el valor almacenado en tal direccion. Es decir, dado un puntero-resultado 'pr', por mas que ese valor desborde el array al que apuntaba, 'pr' sera previsible mientras que '*pr' no lo sera, se dice que su valor es 'indefinido'.

Puntero - puntero

Esta operacion da como resultado no un puntero sino un valor entero. Es necesario que ambos punteros sean del mismo tipo, en caso contrario se producira un error en tiempo de compilacion. La resta de punteros tiene la propiedad de que su valor es independiente de los tipos implicados, es decir: dado un puntero 'p1' y otro 'p2' que apuntan respectivamente a los elementos 'n' y 'm' de un array, el valor entero de 'p1-p2' sera igual a 'n-m', independientemente del tipo a que apunten los punteros.
El valor entero del resultado debe ser interpretado como 'numero de elementos (del mismo tipo que los punteros) entre ambos punteros', y no debe ser tomado como una simple resta de valores de memoria. Por ejemplo:

int t [] = {45,345,5,354,345};
int* pt1 = &t[1];                  //en vez de &t[2] se puede t+2;
int* pt2 = &t[4];
int res = pt2-pt1;

El valor de 'res' sera 3, mientras que en termino de direcciones de memoria la distancia entre ambos punteros es de 6, pues cada entero ocupa 2 bytes. No es necesario involucrarse con demasiados detalles de lo que sucede en memoria, los punteros y su aritmetica estan adaptados para tratar con direcciones de modo implicito.

La resta de punteros no es tan frecuente como las operaciones de incremento pero puede prestar usos valiosos, sobre todo en relacion a cadenas de caracteres y junto a librerias standard de funciones. Supongamos que necesitaramos una rutina que extraiga una subcadena de una cadena dada, y que la subcadena estuviera determinada por dos caracteres delimitadores, por ej 'ch1' y 'ch2', el esquema de una funcion de extraccion seria el siguiente:
1-Se setean dos punteros (p1 y p2) apuntando a los delimitadores 'ch1' y 'ch2'. Funcion que realiza strchr().
2-En un bucle se extraen (p1-p2) caracteres a partir de 'ch1'.

En general se recomienda que la aritmetica de punteros se realice en el nivel mas simple posible. La principal fuente de error proviene de desbordar los limites (inferior o superior) del array al que apunta el puntero. Estas dificultades no se presentan si se esta tratando con mapeo de memoria, flujos de bytes, analisis sintactico a nivel compilador o rutinas similares de bajo nivel, pero cuando los datos presenten mayor nivel de estructuracion seran necesarias mayores precauciones.

Itinerar en un array

Supongamos una cadena 'cad' y un puntero que apunta a esa cadena.

char cad [] = "hola";
char * ptr = cad;

El puntero 'ptr' apunta al primer elemento de 'cad'. Si ahora incrementamos el puntero:

ptr++;

este apuntara a cad[1], es decir el segundo byte de cad, y si sacamos en pantalla 'p' y '*p' se vera lo siguiente:

printf ("%s", p);      // sale "ola"
printf("%c", *p);      // sale 'o'

Como hemos visto antes, printf() saca en pantalla todo lo que haya desde el char al que apunta el puntero recibido como parametro hasta el primer '\0' que encuentre. La explicacion de lo sucedido es la siguiente: 'p' contiene (como cualquier variable) un valor, este valor es una localidad de memoria, por ej 0xfff2, al incrementar el puntero con p++ lo que hacemos es incrementar el valor que contiene, por eso pasara de 0xfff2 a 0xfff3. Si el puntero hubiera sido tipo entero, el incremento 'p++' habria sumado en 2 la localidad apuntada (0xfff4), pues un entero ocupa 2 bytes de memoria.
El mecanismo es simple y muy eficaz para itinerar a traves de un array, pero no solo eso, tambien nos permite itinerar por cualquier zona de memoria y es el metodo mas comodo para hacerlo.

Mapear localidades de memoria

Casi siempre se mencionan a los punteros en relacion a arrays, pero un puntero puede operar de modo totalmente independiente de cualquier array, precisamente para itinerar libremente por regiones de memoria, segun Stroustrup (1995), "la implementacion de punteros tiene por finalidad mapear directamente los mecanismos de direccionamiento de la maquina en que se ejecuta un programa ".
Veamos un ejemplo. Supogamos que nuestro programa opera en modelo small (como la mayoria de los ejemplos dados) y que queremos observar el estado del segmento de datos-stack durante la ejecucion del programa. Una funcion como la siguiente podria cumplir esa funcion:

int funcion() 
{ 
char *tt=0;                  //Apunta al comienzo del segmento de datos
unsigned char ch;            //Unsigned, para no lidiar con valores negativos
int a,fil,col;

for (a=0;a<256;a++) { 
 ch = *(tt+a);              //Para ver el final del segmento seria: ch=*(tt+0xff00+a);
  col = a%16; 
  fil = a/16;

gotoxy(col*3+24,fil+4); 
  printf("%02X",ch);         //Representacion hexadecimal
if (ch<32) ch=46;            //Si ch <32 se reemplaza con puntos

gotoxy(col+2,fil+4); 
  printf("%c",ch);           //Representacion ascii
}
return 0;}

La funcion saca en pantalla los primeros 256 bytes del segmento de datos-stack. Los detalles del bucle de impresion son para que la salida sea similar a la de un editor hexadecimal, en una columan los caracteres ascii, excluyendo a aquellos cuyo valor es menor a 32 (0x20), en realidad muchos de estos caracteres se pueden imprimir bien, mientras que es mejor evitar algunos como 7,8,10,13, pero se han evitado todo los menores a 32 para simplificar el codigo. En otra columna se exhibiran los valores hexadecimales de esos caracteres.
El final del segmento de datos es muy interesante pues almacena los valores de las variables locales, por esta causa sufre importantes cambios con cada llamado a funcion. Para observar tal sector, con el codigo anterior, basta con reemplazar la primera linea debajo del bucle por la indicada en el comentario. El esquema de la funcion, aplicada a lectura de ficheros (archivos), podria ser de utilidad en una salida a pantalla de un editor hexadecimal.

Paso de parametros en funciones

Por default las variables declaradas dentro de una funcion no estan disponibles para otras funciones. Cuando necesitamos que una funcion acceda a datos de otra el principal recurso es el paso de argumentos. El paso de argumentos se hace principalmente a traves de la pila (stack), un bloque de memoria especializado en el almacenamiento de datos temporales. El espacio total disponible para uso de la pila varia segun el modelo de memoria utilizado por el programa (aparte de las limitaciones de hardware), si nuestro programa utiliza el modelo 'small' de memoria el espacio total para uso de pila y datos sera de 64 Kb.
Cuando se guardan en la pila mas valores de los que caben se produce un 'stack overflow', un desborde de pila. Las funciones recursivas trabajan haciendo una copia de si mismas y guardandola en la pila, motivo por el cual no es raro encontrar desbordes de pila provocados por recursiones mal calculadas. Hay muchos motivos para utilizar la pila del modo mas economico posible, y los punteros cumplen una gran utilidad en este caso.

Un parametro puede ser pasado a una funcion de dos modos diferentes: por valor y por referencia. Pasarlo por valor implica que la funcion receptora hace una copia del argumento y trabaja con ese doble del original, cualquier modificacion realizada en la variable-copia no producira ningun cambio en el parametro enviado. En cambio al pasar un valor por referencia estamos pasando la direccion de memoria del mismo argumento, en este caso no hay otra 'copia' del mismo, el dato es uno y el mismo para las dos funciones, la que envia el dato y la que lo recibe.

Una variable comun puede ser pasada por valor o por referencia. En el siguiente ejemplo la funcion 'f ' recibe dos parametros pasados por la funcion 'principal', el primero es pasado por valor y el segundo por referencia. En el primer caso 'f ' incrementara el valor de la variable 'a' que es una copia local del argumento 'x', ese incremento afectara a 'a' pero no a 'x'. En el segundo caso la variable 'y' es pasada por referencia, por lo tanto el incremento operado sobre 'b' es al mismo tiempo un incremento de 'y'. Tanto la variable 'y' de la primera funcion como 'b' estan asociadas a la misma localidad de memoria (hay dos nombres para una misma localidad de memoria).

void f (int a, int& b) 
{
a++;
b++;
}
//---------------------------
void principal() 
{
int x = 1;
int y = 1;
f(x,y);
...................

Un array en cambio siempre es pasado por referencia, la funcion que recibe el parametro recibe un puntero al elemento inicial del array. Por ejemplo:

int xstrlen (char* str) 
{
int a=0;
while (*str++!=0) {                 //alternativa-->     while (str[a]!=0) {a++;}
  a++; }
return a; }
//-----------------------------
void principal () 
{
char cad[] = "hola";
printf ("%d", xstrlen(cad));
....................................... 

En el ejemplo, la funcion xstrlen() nos da el largo de un array de caracteres buscando la posicion del '\0' de fin de cadena, y la funcion 'principal' sacara ese valor entero en pantalla. Observese la sintaxis alternativa para el bucle, en un caso anotamos 'str' como puntero y en el otro como 'array', ambas notaciones son intercambiables.

Al pasar un array por referencia, la funcion receptora solo recibe la direccion inicial del array, es decir 2 bytes, por lo tanto pasar un array de 30 KB consume menos recursos de stack que pasar una variable de tipo long (pasada por valor), que requiere 4 bytes. El principal inconveniente de pasar un parametro por referencia radica en la posibilidad que tiene la funcion receptora de alterar todos los datos del parametro, por esta causa es frecuente que, en la declaracion de la funcion, tal parametro se declare como "const" para evitar la corrupcion accidental de ese dato.

Reserva de Memoria Dinamica

En primer lugar recordemos que es la 'memoria dinamica'. Hay tres formas de usar la memoria en C++ para almacenar valores:
1-Memoria estatica.
Es el caso de las variables globables y las declaradas como 'static'. Tales objetos tienen asignada la misma direccion de memoria desde el comienzo al final del programa.
2-Memoria automatica.
Usada por los argumentos y las variables locales. Cada entrada en una funcion crea tales objetos, y son destruidos al salir de la funcion. Estas operaciones se realizan en la pila (stack).
3-Memoria dinamica.
Tambien llamado 'almacenamiento libre' (free store). En estos casos el programador solicita memoria para almacenar un objeto y es responsable de liberar tal memoria para que pueda ser reutilizada por otros objetos.

La operacion de reservar y liberar espacio para variables globables, estaticas o locales son realizadas de modo implicito por el programa, la unica modalidad que requiere mayor atencion por parte del programador es la de reservar memoria en forma dinamica.

El papel de los punteros en relacion a la memoria dinamica es muy importante, por la razon de que al pedir, al sistema operativo, una cantidad determinada de memoria dinamica para un objeto, el sistema nos retorna un puntero que apunta a esa zona de memoria libre, la respuesta dependera de si hay o no tanto espacio como el solicitado.
a) Si hay suficiente memoria se retorna un puntero que apunta al comienzo de esa zona de memoria.
b) Si no hay suficiente, retorna un puntero nulo.

En C++ los operadores usados para requerir y liberar memoria dinamica son new y delete. La sintaxis es la siguiente:

Variable individual Array de elementos individuales
Reserva de memoria dinamica

int* a = new int

int* a = new int [n];

Liberacion de memoria reservada

delete a;

delete [] a;

Nota: las primeras versiones de TurboC++ no admiten la linea "delete [ ] a" con corchetes vacios, para liberar memoria dinamica de un array requiere que explicitemos cuantos elementos hay que borrar, explicitando este valor entre corchetes.En la version TurboC++3.0 esta caracteristica de la sintaxis standard ya se encuentra implementada.

Las ventajas de utilizar memoria dinamica se valoran mejor en comparacion con las caracteristicas de la reserva estatica de memoria.

Reserva estatica
de memoria
Reserva dinamica
de memoria
Creacion de
objetos
Los objetos locales son creados al entrar en
la funcion que los declara. Los globales son
creados al iniciarse el programa.
La memoria se reserva explicitamente mediante
el operador new.
Duracion de los
objetos
Los objetos locales se destruyen al salir
de la funcion en que han sido creados. Los
globales, al salir del programa.
Los objetos necesitan ser destruidos explicitamente,
con el operador delete.
Indice de arrays Al reservar memoria estatica para un array
el valor del indice debe ser un valor constante.
Ej:
int n [20];
int n [variable no const];        //no permitido
El indice de un array puede ser un valor variable, de modo
que la cantidad de memoria reservada por una linea de codigo
puede variar en tiempo de ejecucion (runtime).
Ej:
int* n = new int [variable no const]           //correcto

La estrecha relacion que existe entre arrays y punteros explica que la solicitud de memoria dinamica para un array culmine en la devolucion de un puntero, una vez que ha sido reservada la memoria suficiente operamos sobre el puntero directamente, de modo muy similar a como operamos con un array.

Los mecanismos de bajo nivel que implementan el uso de memoria dinamica son bastante complejos y no nos detendremos en ello. Desde el punto de vista del programador, la principal fuente de errores se deriva de una mala coordinacion entre operadores new y delete, sea que olvidemos liberar la memoria que ya no utilicemos, o que intentemos borrar, o utilizar, un objeto ya borrado.

PRINCIPAL





Free Web Hosting