Análisis y Diseño de Algoritmos                                                                                                      

Prof: Ing. Victor Garro

Asistente: Marco Elizondo Vargas

 

Punteros

 

utilidad de los punteros

 

Aritmética 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 posibilidades y el tipo de resultado generado:

 

 

Operación

Resultado     

Comentarios

Puntero +- entero

Puntero a T mas entero       

Puntero a T

Si el puntero apunta mas allá del limite superior del
array el resultado es no definido

Puntero a T menos entero

Puntero a T

Si el puntero apunta mas allá 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 aritmética de punteros no es una simple suma o resta de direcciones. Estas operaciones están adaptadas especialmente para tratar con arrays, de a.C. que incrementar en 1 el valor de un puntero no apunte a la próxima dirección de memoria, sino al próximo elemento de un array. El único caso donde 'próxima dirección' es igual a 'próximo 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 operación 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 seguirá 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 será diferente según la modalidad adoptada. En el primer caso el puntero estará apuntando fuera del array (pk+4), en el segundo el puntero seguirá 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 paréntesis, la notación *pk+a  tendría un efecto por completo diferente, el operador '*' tiene mayor precedencia que '+', por lo tanto se tomaría 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 tomaríamos los sucesivos valores (p+2). O si necesitáramos obviar los primeros 'n' caracteres de una cadena, y copiar el resto en un buffer, podríamos escribir:  strcpy(buffer, cad+n); La suma de enteros a punteros tiene muchas aplicaciones, en especial si se combinan con las librerías 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 dirección que almacena), sino el valor almacenado en tal dirección. Es decir, dado un puntero-resultado 'pr', por mas que ese valor desborde el array al que apuntaba, 'pr' será previsible mientras que '*pr' no lo será, se dice que su valor es 'indefinido'.

Puntero - puntero

Esta operación da como resultado no un puntero sino un valor entero. Es necesario que ambos punteros sean del mismo tipo, en caso contrario se producirá un error en tiempo de compilación. 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' será 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' será 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 aritmética están adaptados para tratar con direcciones de modo implícito.

La resta de punteros no es tan frecuente como las operaciones de incremento pero puede prestar usos valiosos, sobre todo en relación a cadenas de caracteres y junto a librerías standard de funciones. Supongamos que necesitáramos 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 función de extracción seria el siguiente:
1-Se setean dos punteros (p1 y p2) apuntando a los delimitadores 'ch1' y 'ch2'. función que realiza
strchr().
2-En un bucle se extraen (p1-p2) caracteres a partir de 'ch1'.

En general se recomienda que la aritmética 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, análisis sintáctico a nivel compilador o rutinas similares de bajo nivel, pero cuando los datos presenten mayor nivel de estructuración serán 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 parámetro hasta el primer '\0' que encuentre. La explicación 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++' habría 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 trabes de un array, pero no solo eso, también nos permite itinerar por cualquier zona de memoria y es el método mas cómodo para hacerlo.

Mapear localidades de memoria

Casi siempre se mencionan a los punteros en relación a arrays, pero un puntero puede operar de modo totalmente independiente de cualquier array, precisamente para itinerar libremente por regiones de memoria, según Stroustrup (1995), "la implementación de punteros tiene por finalidad mapear directamente los mecanismos de direccionamiento de la maquina en que se ejecuta un programa ".
Veamos un ejemplo. Supongamos que nuestro programa opera en modelo small (como la mayoría de los ejemplos dados) y que queremos observar el estado del segmento de datos-stack durante la ejecución del programa. Una función como la siguiente podría cumplir esa función:

int función()

{

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 función saca en pantalla los primeros 256 bytes del segmento de datos-stack. Los detalles del bucle de impresión son para que la salida sea similar a la de un editor hexadecimal, en una columna 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 código. En otra columna se exhibirán 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 función. Para observar tal sector, con el código anterior, basta con reemplazar la primera línea debajo del bucle por la indicada en el comentario. El esquema de la función, aplicada a lectura de ficheros (archivos), podría ser de utilidad en una salida a pantalla de un editor hexadecimal.

Paso de parámetros en funciones

Por default las variables declaradas dentro de una función no están disponibles para otras funciones. Cuando necesitamos que una función acceda a datos de otra el principal recurso es el paso de argumentos. El paso de argumentos se hace principalmente a trabes 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 según 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 será 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 guardándola 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 económico posible, y los punteros cumplen una gran utilidad en este caso.

Un parámetro puede ser pasado a una función de dos modos diferentes: por valor y por referencia. Pasarlo por valor implica que la función receptora hace una copia del argumento y trabaja con ese doble del original, cualquier modificación realizada en la variable-copia no producirá ningún cambio en el parámetro enviado. En cambio al pasar un valor por referencia estamos pasando la dirección 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 envía el dato y la que lo recibe.

Una variable común puede ser pasada por valor o por referencia. En el siguiente ejemplo la función 'f ' recibe dos parámetros pasados por la función '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 función como 'b' están 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 función que recibe el parámetro 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 función xstrlen() nos da el largo de un array de caracteres buscando la posición del '\0' de fin de cadena, y la función 'principal' sacara ese valor entero en pantalla. Obsérvese 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 función receptora solo recibe la dirección 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 parámetro por referencia radica en la posibilidad que tiene la función receptora de alterar todos los datos del parámetro, por esta causa es frecuente que, en la declaración de la función, tal parámetro se declare como "const" para evitar la corrupción accidental de ese dato.

Reserva de Memoria dinámica

En primer lugar recordemos que es la 'memoria dinámica'. Hay tres formas de usar la memoria en C++ para almacenar valores:

1-Memoria estática.
Es el caso de las variables globales y las declaradas como 'static'. Tales objetos tienen asignada la misma dirección de memoria desde el comienzo al final del programa.
2-Memoria automática.
Usada por los argumentos y las variables locales. Cada entrada en una función crea tales objetos, y son destruidos al salir de la función. Estas operaciones se realizan en la pila (stack).
3-Memoria dinámica.
también 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 operación de reservar y liberar espacio para variables globales, estáticas o locales son realizadas de modo implícito por el programa, la única modalidad que requiere mayor atención por parte del programador es la de reservar memoria en forma dinámica.

El papel de los punteros en relación a la memoria dinámica es muy importante, por la razón de que al pedir, al sistema operativo, una cantidad determinada de memoria dinámica para un objeto, el sistema nos retorna un puntero que apunta a esa zona de memoria libre, la respuesta dependerá 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 dinámica son new y delete. La sintaxis es la siguiente:

 

 

Variable individual

Array de elementos individuales

Reserva de memoria dinámica

int* a = new int

int* a = new int [n];

Liberación de memoria reservada

delete a;

delete [] a;

 

Nota: las primeras versiones de TurboC++ no admiten la línea "delete [ ] a" con corchetes vacíos, para liberar memoria dinámica de un array requiere que explicitemos cuantos elementos hay que borrar, explicitando este valor entre corchetes.En la versión TurboC++3.0 esta característica de la sintaxis standard ya se encuentra implementada.

Las ventajas de utilizar memoria dinámica se valoran mejor en comparación con las características de la reserva estática de memoria.

 

 

Reserva estática
de memoria

Reserva dinámica
de memoria

Creación de
objetos

Los objetos locales son creados al entrar en
la función que los declara. Los globales son
creados al iniciarse el programa.

La memoria se reserva explícitamente mediante
el operador new.

Duración de los
objetos

Los objetos locales se destruyen al salir
de la función en que han sido creados. Los
globales, al salir del programa.

Los objetos necesitan ser destruidos explícitamente,
con el operador delete.

Índice de arrays

Al reservar memoria estática para un array
el valor del índice debe ser un valor constante.
Ej:
int n [20];
int n [variable no const];        //no permitido

El índice de un array puede ser un valor variable, de modo
que la cantidad de memoria reservada por una línea de código
puede variar en tiempo de ejecución (runtime).
Ej:
int* n = new int [variable no const]           //correcto

 

La estrecha relación que existe entre arrays y punteros explica que la solicitud de memoria dinámica para un array culmine en la devolución 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 dinámica 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 coordinación 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.

 

Free Web Hosting