Unidad 8 - Computacion Avanzada

Anuncio
Unidad 8 1
Computación Avanzada
Punteros.
Dentro de la memoria de la computadora cada dato almacenado ocupa una o más celdas
contiguas de memoria. El número de celdas de memoria requeridas para almacenar un
dato depende de su tipo.
Si 'v' es una variable que representa un determinado dato, el compilador
automáticamente asigna celdas de memoria para ese dato dependiendo de su tipo. El
dato puede ser accedido si conocemos su localización ( la dirección ) de la primera celda
de memoria. La dirección de 'v' se determina mediante &v, donde & es un operador
monario, llamado operador dirección, que proporciona la dirección del operando. Si la
dirección de 'v' se le asigna a otra variable pv = &v; esta nueva variable es un puntero a
'v': representa la dirección de 'v' y no su valor. Los punteros son direcciones. El dato
representado por 'v' puede ser entonces accedido mediante *pv, donde * es un operador
monario, llamado operador indirección ( u operador de “dereferencia” ), que opera sólo
sobre una variable puntero.
En C, los punteros son una herramienta fundamental para construir programas. Sin ellos
se podría hacer realmente poca cosa. Examinemos unos ejemplos que muestran donde los
punteros son necesarios:
1.- Paso de parámetros por referencia.
Ya hablamos de ésto en funciones.
2.- Declaración de arreglos dinámicos.
Imaginemos que estamos haciendo un procesador de imágenes, y que tenemos
una función que va a hacer cierto tratamiento. Recibe por parámetro el nombre
del fichero a utilizar, y lo lee del disco, lo modifica, y lo guarda otra vez.
Nuestro problema es que no sabemos de antemano lo que va a medir la foto, y por
lo tanto, si no existieran los arreglos dinámicos, deberíamos declarar un buffer lo
suficientemente grande como para que cupiese la foto más enorme que
tuviéramos.
¿Qué estamos haciendo si actuamos de este modo? Si la foto es pequeña, estamos
usando demasiada memoria, que puede hacer incluso que se agoten los recursos
del ordenador. Si la foto es más grande de lo que habíamos previsto, nuestro
programa no será capaz de tratarla, aunque la computadora tenga recursos
suficientes.
Unidad 8 2
Computación Avanzada
3.- Acceso a memoria física.
C es el lenguaje comúnmente utilizado a la hora de programar módulos de
sistemas operativos, drivers, etc. Este tipo de programas deben acceder con
frecuencia al hardware de la maquina, para poder leer datos del exterior
(movimientos de un ratón, temperatura de un sensor) o bien para escribir sobre
ellos (dibujar en una pantalla, mover electroválvulas). Una de las maneras de hacer
todo esto consiste en “mapear” los dispositivos de entrada/salida sobre un espacio
real de memoria. De este modo, cuando se escribe en cierta posición de memoria
lo que se está haciendo realmente es actuar sobre una salida, y cuando se lee de
cierta posición de memoria se está leyendo una entrada.
Por ejemplo, en un PC, la memoria de la pantalla en modo texto se encuentra en la
posición 0B8000000H (también conocido como el par segmento:dirección
B800:0000 aunque ésto no funcionará en Windows, solo en MS-DOS, ya que
Windows protege a la máquina contra los programas de usuario bajo una capa de
memoria virtual). A partir de esta posición, cada dos bytes codifican una letra de
pantalla: el primer byte corresponde al código ASCII de la misma, y el segundo byte
corresponde al código del color a aplicarle.
Declaración de punteros
Los punteros, como cualquier otra variable, deben ser declarados antes de ser usados.
Cuando una variable puntero es definida, el nombre de la variable debe ir precedida por
un *. Este identifica que la variable es un puntero. El tipo de dato que aparece en la
declaración se refiere al tipo de objeto del puntero, el tipo de dato que se almacena en la
dirección representada por el puntero, en vez del puntero mismo. Así, una declaración de
puntero general es:
tipo_dato *ptvar;
donde ptvar es la variable puntero y tipo_dato el tipo de dato apuntado por el puntero.
Dentro de la declaración, una variable puntero puede ser inicializada asignándole la
dirección de otra variable que por supuesto debe estar previamente declarada.
Los operadores de punteros.
Los operadores monarios & y * son miembros del mismo grupo de precedencia que los
otros operadores monarios: -,++,--,!,sizeof. Hay que recordar que este grupo de
Unidad 8 3
Computación Avanzada
operadores tiene mayor precedencia que el de los operadores aritméticos y la
asociaciones de los operadores monarios es de derecha a izquierda.
El operador dirección (&) sólo puede actuar sobre variables ordinarias o elementos
simples de un arreglo.
El operador indirección (*) sólo puede actuar sobre operandos que sean punteros.
Las variables puntero pueden apuntar a variables numéricas o de caracteres, arreglos,
funciones, otras variables puntero y también pueden apuntar a otros tipos de estructuras
de datos más complejos. Por tanto a una variable puntero se le puede asignar la dirección
de una variable ordinaria. También se le puede asignar la dirección de otra variable
puntero, siempre que ambas apunten al mismo tipo de dato.
Mostremos lo que hemos hablado hasta ahora con un ejemplo:
#include<stdio.h>
main(){
int v1, v2;
int *pv;
v1 = 555; // Asigna 555 a v1
pv = &v1; // pv recibe la dirección de v1.
v2 = *pv; // Asigno a v2 el “contenido” de la posición de memoria pv.
printf(“%i”,v2); // Imprime 555;
return 0;
}
Esto hace algo como:
Direcciones
Contenido
0x00F0
v1 = 555
0x00F1
v1
0x00F2
v2 = 555
0x00F3
v2
Unidad 8 4
Computación Avanzada
Direcciones
Contenido
...
0X####
pv = 0x00F0
0X####+1
donde la dirección '####' no es realmente de importancia.
Aquí se observa cómo los punteros contienen direcciones de memoria. 'v1' es una variable
que contiene el valor de 555. 'pv' es un puntero ( almacenado en cualquier dirección ####
que realmente no nos importa en este caso ) que contiene la dirección de memoria de
'v1', o sea, 0x00F0. Luego a 'v2' se le asigna el contenido de 'pv' ( El contenido de la
localidad de memoria 0x00F0 que es 555 ). Por último el programa imprime la variable
'v2'. ¡¡¡ Fácil !!!, ¿ no es cierto ?, pues ese es el concepto detrás de los punteros.
OJO: Es muy peligroso trabajar con punteros si éstos no han sido asignados a ninguna
posición de variable con anterioridad. Esto se ve bien ya que si no ha sido asignada una
dirección, el puntero podría contener una dirección importante que ya está siendo usada
y podría variar su valor. Para evitar este tipo de conflictos se puede asignar a los punteros
un valor predeterminado o inicializarlos a NULO ( NULL ). Esto nos indicaría que al puntero
aun no se le ha asignado una dirección y por lo tanto no podrá ser usada. Ej:
int *p=NULL;
Expresiones con punteros.
En general, las expresiones que involucran punteros se ajustan a las mismas reglas que
cualquier otra expresión de C. Esto quiere decir que si tenemos:
int x;
int *p1, *p2;
podemos hacer:
p1 = &x;
p2 = p1;
ó hacer operaciones simples con punteros ó incluso comparaciones. P. ej.:
Unidad 8 5
Computación Avanzada
if (p1 == p2) printf(“Ambos apuntan al mismo sitio\n”);
p2 = p1 +1; // Ésto es aritmética de punteros.
p2--;
p1++;
Pero no se permiten otras operaciones aritméticas con punteros. Así una variable puntero
no puede ser multiplicada por una constante, dos punteros no pueden sumarse, etc.
Aritmética de punteros.
Existen sólo dos operaciones aritméticas que pueden hacerse con punteros: la suma y la
resta. En particular, un valor entero puede ser sumado o restado a una variable puntero,
pero el resultado de la operación debe ser interpretado con cuidado. Supongamos que px
es una variable puntero que representa la dirección de una variable x. Se pueden escribir
expresiones como ++px, --px, (px+3). Cada expresión representa una dirección localizada a
cierta distancia de la posición original representada por px. La distancia exacta será el
producto de la cantidad entera por el número de bytes que ocupa cada elemento al que
apunta px. Es por eso que NO ES LO MISMO UN PUNTERO A CHAR QUE UN PUNTERO A
INT. Ambos punteros contendrán una dirección de memoria pero, por ejemplo, en el
siguiente caso:
char ch='a';
char *pch=&ch; // Asignamos al puntero pch la dirección de ch.
// Supongamos que la dirección es 1000
pch++; // Incrementa pch a 1001 ya que los tipo char ocupan 1 byte.
Sí, es cierto, no se ve complicaciones ya que pch contiene el valor que esperábamos. En
cambio:
int x=10;
Unidad 8 6
Computación Avanzada
int *px=&x; // Asignamos al puntero px la dirección de x.
// Supongamos que la dirección es 1000
px++; // Incrementa px a 1002 ( no a 1001 ) ya que los tipo int ocupan 2 bytes.
Ahora sí, px tendrá 1002, NO 1001 como quizá se esperaba. Esto es debido a que “toda la
aritmética de punteros es relativa al tipo base”. Esto es de gran utilidad para manejar
arreglos ( entre otras utilidades ) pero este punto lo estudiaremos más adelante.
Las operaciones que se pueden realizar sobre punteros:
1. A una variable puntero se le puede asignar la dirección de una variable ordinaria
(pv=&v).
2. A una variable puntero se le puede asignar el valor de otra variable puntero
siempre que ambas apunten al mismo tipo de dato.
3. A una variable puntero se le puede asignar el valor nulo (NULL).
4. Una cantidad entera puede ser sumada o restada a una variable puntero.
5. Una variable puntero puede ser restada de otra con tal de que ambas apunten a
elementos del mismo arreglo. ( OJO: este caso es muy especial ).
6. Dos variables puntero pueden ser comparadas siempre que ambas apunten a
datos del mismo tipo.
7. Por último, como ya se comentó, no se permiten otras operaciones aritméticas con
punteros. Así una variable puntero no puede ser multiplicada por una constante,
dos punteros no pueden sumarse, etc.
Funciones de asignación dinámica.
Una vez compilados, todos los programas en C organizan la memoria de la computadora
en cuatro regiones que contienen: el código del programa ( en lenguaje máquina ), los
datos globales, la pila ( o Stack ) a través del cual se pasan argumentos y donde residen las
variables locales y el montón ( o Heap ) que es el área de memoria libre que es gestionada
por las funciones de asignación dinámica de C. Existen dos funciones básicas para trabajar
con asignaciones dinámicas. Estas son:
void *malloc(num_bytes);
void free(void *p);
Unidad 8 7
Computación Avanzada
La función malloc() asigna “num_bytes” número de bytes de memoria y devuelve un
puntero void ( sin tipo predeterminado. Si se desea que sea de algún tipo, debe usarse la
expresión “(tipo *)” antes del malloc(). Ej: (int *)malloc(128); ) del comienzo de dicha
memoria y free() devuelve al Heap la memoria previamente asignada para que pueda ser
reutilizada. Ambas funciones están en el archivo de cabecera “stdlib.h”.
Como comentamos en el punto “Declaración de arreglos dinámicos”, ésto es muy útil para
todos esos casos en los que _NO_ sabemos con antelación de cual tamaño será la variable
que vamos a usar. Supongamos el caso en que tenemos un programa que leerá caracteres
de un archivo. Nosotros no sabemos con antelación de que tamaño será ese archivo. Así
que podríamos crear un arreglo que contenga espacio suficiente como para almacenar el
mayor arreglo posible... quizá 100.000 caracteres pero ésto es increíblemente ineficiente
ya que la memoria se estaría usando aunque no fuese necesaria ( quizá para leer un
archivo de tan sólo 4 bytes, por lo que nos quitaría memoria para otras operaciones
realmente necesarias ). Mejor es que usáramos una asignación dinámica con el malloc().
Así, supongamos que leemos un archivo y este contiene 500 bytes, entonces nos bastaría
hacer algo como:
char *p;
unsigned long int i;
// leemos el archivo. Asignamos en i el número de bytes que contiene ( 500 bytes ).
p=(char *)malloc(i);
Después de la asignación, p apunta al primero de los 500 bytes de la memoria libre que ha
sido reservada por el malloc(). Luego de usarlo, y si ya no es más necesaria su existencia,
podemos devolver esa memoria al Heap (el Heap es región que queda disponible para las
solicitudes de memoria dinámica al sistema operativo) para ser reutilizada. Esto se hace:
free(p);
Eso sí, como el Heap no es infinito, siempre que se asigne memoria es necesario antes de
usarla comprobar el valor devuelto por malloc() y así asegurarse que no es nulo. Un
puntero nulo indicará un error en la asignación... p. ej. que no hay memoria suficiente. Si
se utiliza un puntero nulo el programa fallará. Por lo tanto, siempre es conveniente hacer
algo como:
Unidad 8 8
Computación Avanzada
if((p=malloc(100)==NULL) {
printf(“No hay memoria\n”);
exit(1);
}
También existe una función muy importante en C llamada sizeof(tipo). Esta función
retorna la cantidad de bytes que ocupa una variable de tipo “tipo” lo que asegura un buen
funcionamiento del programa sin importar el compilador lo que a su vez le da gran
portabilidad al código además del hecho de que así no tenemos que memorizar los
tamaños de todos los tipos ni calcular con anterioridad el tamaño de un tipo de dato
creado. Supongamos que necesitamos espacio para guardar 10 variables de tipo double,
podemos hacer:
p = malloc(10*sizeof(double));
Descargar