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 |
Puntero a T menos
entero |
Puntero a T |
Si el puntero apunta mas allá del
limite inferior |
|
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 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.
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'.
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.
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.
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.
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 |
Reserva dinámica |
Creación de |
Los objetos locales son creados
al entrar en |
La memoria se reserva explícitamente
mediante |
Duración de los |
Los objetos locales se destruyen
al salir |
Los objetos necesitan ser
destruidos explícitamente, |
Índice de arrays |
Al reservar memoria estática
para un array |
El índice de un array puede ser
un valor variable, de modo |
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.