Copy Link
Add to Bookmark
Report
4x04: Como programar en C en muchos pasos (IV Parte)

-[ 4x04.txt ]----------------------------------------------------------------
-[ Como programar en C en muchos pasos (IV Parte) ]----------------[ Max 5 ]-
---------------------------------------------------------[ max5@otv.org.ve ]-
Punteros
--------
Puntero es la traduccion de la palabra pointer que significa apuntador.
Basicamente es una variable que contiene una direccion de memoria. Si una
variable contiene la direccion de otra variable, se dice que la primera
variable apunta a la segunda.
Para cada tipo de variable existe un tipo de puntero. Hay punteros tipo
int, tipo char, etc.. , segun sea el tipo de la variable que se almacena
en la direccion.
Logicamente, para declarar un puntero, debemos decir de que tipo es. La
declaracion debera tener el tipo de la variable, con el nombre precedido
de un asterisco. Por ejemplo:
int *numero;
Es la declaracion de un puntero de tipo entero.
Para resumir:
- Un puntero es una variable como cualquier otra.
- Una variable tipo puntero contiene la direccion de otra variable
almacenada en otro lugar de la memoria.
- En este otro lugar, se encuentra almacenado el dato al cual apunta el
puntero.
Nunca debe usarse un puntero que no haya sido inicializado, ya que sino
este contendria un valor impredecible, lo que, con seguridad, causeria
serios problemas al ejecutarse el programa.
Para acceder al dato al cual apunta un puntero se utiliza una referencia.
Una referencia introduce un sinonimo de un objeto. La notacion para
definir referencias es similar a la de los punteros, excepto que se emplea
& en vez de *. Por ejemplo, las sentencias:
int i;
int *p;
p = &i;
Definen p como una referencia a i. Despues de esta definicion, el valor de
puntero es la direccion de i, es decir que i y *p se refieren al mismo
objeto, como si fuera la misma variable.
El ejemplo V.1 ilustra la declaracion e inicializacion de los punteros asi
como el uso de referencias. Es fundamental analizar y ejecutar este
programa hasta entender perfectamente cada sentencia y sus efectos.
Ejemplo V.1
#include <stdio.h>
char c;
main()
{
char *pc
pc = &c;
for (c = 'A'; c <= 'Z'; c++)
printf("%c", *pc);
return 0;
}
Puntero NULL
------------
Un puntero NULL no apunta a ninguna direccion particular. Un puntero NULL
contiene un valor de 0, unica direccion que un puntero no puede alcanzar.
Es una manera de decir que "por lo menos por el momento, este puntero no
apunta a ninguna direccion valida de la memoria".
Para chequear si un puntero es valido se usa:
if (fp != NULL);
sentencia;
Cuando un puntero se declara como variable global, se inicializa en cero
es decir NULL. Cuando se declara como variable local, es una buena
practica inicializar los punteros en NULL.
Puntero Void
------------
Un puntero Void no apunta a ningun tipo especifico de dato. Es un puntero
generico que apunta a los datos sin necesidad de precisar previamente su
tipo.
Al usar un puntero tipo Void, puede resultar necesario usar un molde para
ver los datos apuntados en su forma real. Por ejemplo, un programa de base
de datos puede leer de forma rapida los registros directamente desde los
sectores del disco, y despues procesar los datos como un arreglo de valores
double. Un molde permitira indicar al compilador la orden de considerar el
puntero tipo void como un puntero tipo double.
Una manera (no necesariamente la mejor) de crear un buffer de datos es
definir un espacio de memoria y a continuacion crear uno o mas punteros
apuntando a esta direccion de memoria:
char buffer[1024];
void *bp;
bp = &buffer;
Aqui se define buffer como un array de 1024 caracteres y bp apunta al primer
byte de buffer.
Para que un programa use los datos apuntados con un apuntador tipo void, se
puede usar un molde. En la declaracion siguiente se supone que c ha sido
declarada de tipo char:
c = *(char *)bp;
La expresion (char *) indica al compilador de considerar temporalmente a
bp como del tipo char.
Similarmente, si i ha sido declarada como int y el buffer contiene valores
del tipo int, la declaracion siguiente:
i = *(int *)bp;
Asigna un valor entero del buffer a la variable i.
El ejemplo V.2 ilustra estos conceptos.
Ejemplo V.2
#include <stdio.h>
#include <dos.h>
void DispPointer(void *p);
main()
{
char buffer[1024];
void *bp;
bp = &buffer;
*(char *)bp = 'A';
buffer[1] = 'B'
buffer[2] = 0;
printf("Direccion de buffer == ");
DispPointer(&buffer);
printf("Datos en buffer == %s", (char *)bp);
return 0;
}
void DispPointer(void *p)
{
printf("%04x:%04x\n", FP_SEG(p), FP_OFF(p));
}
Este programa introduce una funcion DispPointer() que permite visualizar
el valor almacenado en un puntero. Podria en su lugar haberse usado:
printf("%p\n",p);
La opcion %p indica a printf de mostrar el contenido de un puntero. Puede
tambien usarse %Fp para mostrar un puntero lejano (far) con segmento y
desplazamiento (offset). Conceptualmente, los punteros son valores enteros,
sin embargo, su forma interna depende de la arquitectura de la memoria de
la computadora. En los PC 80x86, una direccion de memoria es representada
por un desplazamiento de 16 bits a partir de una posicion de base llamada
segmento. Estos punteros de 16 bits, cercanos (near) permiten direccionar
datos que se encuentran relativamente cercanos - desde 0 hasta 65535 bytes
por encima de la supuesta extremidad de un segmento. Los punteros lejanos
contiene tanto el segmento como el desplazamiento, y por lo tanto permiten
alcanzar cualquier lugar de la memoria.
Para extraer los valores del segmento y del desplazamiento, la funcion
DispPointer() usa dos macros, FP_SEG y FP_OFF, definidos en dos.h. Para
cualquier puntero, la expresion FP_SEG(p) contiene el valor de su segmento
y FP_OFF(p) el valor de su desplazamiento. Pueden usarse estas expresiones
siempre y cuando se permitan usar enteros unsigned.
Para experimentar con la versatilidad de los punteros void, podemos agregar
inmediatamente despues de la linea 9 del listado del ejemplo anterior las
siguientes sentencias:
double f = 3.14159;
double *fp = &f;
printf("*fp == %f\n", *fp);
printf("fp == ");
DispPointer(fp);
Punteros y Variables Dinamicas
------------------------------
Hasta el momento siempre se han almacenado los valores en variables
globales o locales. Estas variables tienen una caracteristica comun: al
compilar el codigo, se reserva un espacio de memoria para su
almacenamiento.
Otra tecnica permite asignar el espacio para las variables en tiempo de
ejecucion.
Dichas variables son dinamicas; se crean a la demanda y se almacenan en un
bloque de memoria de tamaño variable conocido como el "monton" (heap).
Este tipo de variables siempre se direcciona con un puntero. Puede
considerarse el monton como un espacio de memoria mas amplio que el espacio
generalmente disponible para almacenar las variables globales y locales, y
por consiguiente mas adaptado al almacenamiento de los buffers grandes y de
las estructuras de datos, ya que su tamaño varia en funcion de la
necesidad de acomodar cantidades variables de informacion.
Una de las tecnicas mas usadas para fijar el tamaño del monton utiliza
malloc() de alloc.h:
#include <alloc.h
......
char *sp;
sp = malloc(129);
129 es el numero de bytes que se reservan. Ya que malloc( ) regresa un
puntero void, algunos autores prefieren usar:
sp = (char *)malloc(129);
Despues de eso, sp apunta a un bloque de 129 bytes, siendo este ultimo de
uso exclusivo de sp. La sentencia puede fallar si el monton esta lleno o
no dispone de suficiente espacio para acomodar los 129 bytes. En este caso
malloc() regresa un valor null, lo que puede usarse en la codificacion:
sp = malloc(129);
if (sp == NULL)
error();
o:
sp = malloc(129);
if (sp)
sentencia;
Lo que es identico a:
if ((sp = malloc(129)) != NULL)
sentencia;
Para copiar una cadena de caracteres en la memoria apuntada por sp, se
puede utilizar:
#include <string.h>
char *sp;
sp = malloc(129);
if (sp)
strcpy(sp, "Colocame en el monton!")
Los punteros permiten direccionar las variables dinamicas de cualquier
tipo, por ejemplo:
double *v;
v = malloc(n);
Para evitar tener que decidir del valor de n, se usa:
v = malloc(sizeof(double));
Despues de estas sentencias puede usarse *v como una variable double, por
ejemplo:
*v = 3.14159
Que asigna el valor de Pi al espacio de memoria reservado para v y:
printf("Valor == %f\n", *v);
Muestra el valor almacenado en el monton.
En vez de malloc(), puede usarse tambien calloc() de alloc.h. Esta funcion
requiere de dos argumentos, a saber, el numero de objetos que se quiere
asignar y el tamaño de un objeto:
long *p;
p = calloc(1, sizeof(long));
Estas sentencias reservan un espacio para un valor long y asignan la
direccion de este espacio a p. Para reservar 10 objetos, se puede escribir:
p = calloc(10, sizeof(long));
En la practica:
p = calloc(sizeof(long), 10);
Reserva la misma cantidad de espacio que la sentencia anterior. calloc(),
ademas de reservar la memoria, coloca todos los bytes reservados en 0.
Cuando se termina de usar un bloque de memoria reservado, este debe
liberarse con free(), para poder reusarlo con malloc(), calloc(), etc...:
char *s;
s = malloc(256);
..........
free(s);
Punteros y estructuras de datos
-------------------------------
De la misma manera que los punteros apuntan a variables, ellos pueden
apuntar a arreglos y estructuras.
En realidad existe una estrecha relacion entre los punteros y los arreglos.
Como ya lo hemos visto, los arreglos son una coleccion de una o varias
variables del mismo tipo de datos. Si coleccion es un arreglo, la expresion
coleccion[0] se refiere al primer elemento del arreglo. En un cierto
sentido, puede considerarse que esta expresion apunta al primer elemento
del arreglo, y en realidad constituye internamente una referencia.
El ejemplo siguiente demuestra esta relacion entre punteros y arreglos.
Ejemplo V.3
#include <stdio.h>
#define MAX 10
void showFirst(void);
int array[MAX];
main()
{
array[0] = 123;
showFirst();
*array = 321;
showFirst();
return 0;
}
void showFirst(void)
{
printf("array[0] = %d\n", array[0]);
printf("*array = %d\n", *array);
}
El ejemplo siguiente, parecido al interior ilustra el uso de punteros en
vez de indices para direccionar los elementos de un arreglo.
Ejemplo V.4
#include <stdio.h>
#define MAX 10
void showFirst(void);
int array[MAX];
main()
{
int *p = array;
int i;
for (i = 0; i < MAX; i++)
array[i] = i;
for (i = 0; i < MAX; i++)
printf("%d\n", *p++);
p = &array[5];
printf("array[5] = %d\n", array[5]);
printf("*p ..... = %d\n", *p);
return 0;
}
La linea 11 declara un puntero entero y al mismo tiempo asigna a p la
direccion del arreglo. Seria un error usar &array ya que array es un
puntero y puede asignarse su valor directamente a otro puntero siempre y
cuando este ultimo sea del mismo tipo.
Las lineas 14-15 utilizan indices para llenar el arreglo con valores de 0
a MAX -1. El bucle de las lineas 16 - 17 muestra los valores de los
elementos del arreglo usando el puntero p en vez de los indices para
referirse a dichos elementos.
Con la expresion *p++ ocurren dos acciones. *p da la direccion apuntada
por p, mientras ++ adelanta p al elemento siguiente de array.
*p++ constituye una muestra de la aritmetica que puede llevarse a cabo con
los punteros. Solo + y - pueden usarse en dicha aritmetica:
p -=2;
p +=10;
etc...
Las comparaciones de puntero, tales como, (p1 < p2) o (p1 >= p2) deben
realizarse con mucho cuidado ya que la comparacion de los punteros lejanos
puede dar resultados erroneos.
En efecto, en las computadoras basadas en 80x86 que tienen la memoria
segmentada, dos valores diferentes de direccion pueden corresponder al
mismo espacio fisico. Por ejemplo, la direccion 0001:0000 es equivalente a
0000:0010 ya que el primer valor representa el principio de una extremidad
de un segmento que se encuentra exactamente en el mismo lugar que tiene
por direccion el segundo valor. Para este tipo de comparacion se debe usar
punteros de tipo huge (enorme), que fisicamente representan lo mismo que
los punteros lejanos pero poseen valores del desplazamiento normalizados
en el rango (decimal) de 0 al 15. Con ellos, cada direccion de memoria
tiene un valor unico. En otras palabras, cada direccion se representa por
un par unico segmento-desplazamiento con un desplazamiento en el rango de
0 al 15.
El ejemplo siguiente ilustra la comparacion de punteros tipo huge y far.
Ejemplo V.5
#include <stdio.h>
#include <dos.h>
main()
{
char far *fp1;
char far *fp2;
char huge *hp1;
char huge *hp2;
fp1 = MK_FP(0, 0x0010);
fp2 = MK_FP(0x0001, 0);
if (fp1 == fp2)
printf("Los punteros far son iguales\n");
else
printf("Los punteros far son diferentes\n");
hp1 = MK_FP(0, 0x0010);
hp2 = MK_FP(0x0001, 0);
if (hp1 == hp2)
printf("Los punteros huge son iguales\n");
else
printf("Los punteros huge son diferentes\n");
return 0;
}
Arreglos dinamicos
------------------
Los arreglos de tamaño fijo pueden malgastar espacio. Por ejemplo, si se
declara:
double miArreglo[100];
Y se usa solo 70 elementos, se malgasta 240 bytes (30 x 8). En un programa
grande con muchos arreglos de este tipo, el espacio malgastado puede
resultar enorme.
Cuando no se hace necesario determinar por anticipacion el tamaño del
arreglo, puede usarse la siguiente tecnica para declarar un arreglo
dinamico cuyo tamaño puede variar en tiempo de ejecucion. Se declara un
puntero del tipo de las variables que se almacenaran en el arreglo:
double *miArregloP;
Suponiendo que en cualquier otra parte del programa se necesita un arreglo
de 70 elementos tipo double, se usara:
miArregloP = malloc(70 * sizeof(double));
o:
miArregloP = calloc(70, sizeof(double));
Despues de esta asignacion, si miArregloP no es NULL, este apunta a un
bloque de memoria capaz de almacenar 70 valores double. Ya que los
arreglos y punteros estan directamente relacionados, puede usarse el
identificador del puntero como si hubiera sido declarado como un arreglo:
miArreglo[11] = 3.14159;
Al terminar de usar el arreglo, debe liberarse la memoria:
free(miArregloP);
A continuacion puede llamarse de nuevo a malloc() para reservar un espacio
de memoria diferente y asignar la nueva direccion a miArregloP.
Arreglos de Punteros
--------------------
Cuando, por ejemplo, se extraen cadenas de 80 caracteres de un archivo de
texto, al almacenarlos en arreglos de tamaño fijo declarados como char
c[128], se perderan 48 bytes por cadena. El ejemplo siguiente ilustra como
evitar este malgasto de la memoria usando un arreglo de punteros tipo char
(Este ejemplo asume que existe suficiente espacio en el monton para
almacenar las cadenas consideradas. Sin embargo en un programa mas grande
deberia chequearse la validez de los punteros antes de usarlos).
Ejemplo V.6
#include <stdio.h>
#include <string.h>
#include <alloc.h>
#define MAX 3
char *ReadString(void);
main()
{
int i;
char *array[MAX];
printf("Teclear %d cadenas:\n", MAX);
for (i = 0; i < MAX; i++)
array[i] = ReadString();
puts("\n\nSus cadenas son:\n");
for (i = 0; i < MAX; i++)
puts(array[i]);
return 0;
}
char *ReadString(void)
{
char *p;
char buffer[128];
gets(buffer);
p = (char *)malloc(1 + strlen(buffer));
strcpy(p, buffer);
return p;
}
Punteros y Cadenas
------------------
Un puntero a una cadena es literalmente un puntero de puntero y en vez de:
char *array[];
puede usarse:
char **array;
El uso mas tipico de los punteros a cadenas se ilustra en el ejemplo
siguiente.
Ejemplo V.7
#include <stdio.h>
main(int argc, char *argv[])
{
if (argc <= 1) {
puts("");
puts("CMDLINE por Max 5");
puts("Intro CMDLINE [x[y][z]] para probar");
} else
while (--argc > 0)
puts(*++argv);
return 0;
}
En la linea 3 se declara main() con parametros. Argc, que es igual al
numero de parametros de la linea de orden mas uno para el path del
programa; argv es un puntero a un arreglo de punteros a caracteres,
apuntando cada uno de ellos a un argumento de la linea de orden.
Despues de compilar este programa, se ejecuta desde el DOS con cmdline
arg1 arg2 arg3. Para entender mejor las lineas 11 y 12, despues de agregar
la declaracion:
char *p;
Estas lineas pueden sustituirse por:
while (argc > 0) {
argc--;
p = *argv;
puts(p);
argv++;
}
Punteros al entorno
-------------------
La funcion main() tambien puede usar un tercer parametro, puntero a un
arreglo de cadenas, siendo cada cadena una cadena de entorno definida por
el sistema operativo.
Se declara main() segun:
main(int argc, char *argv[], char *env[])
Punteros y Estructuras
----------------------
Sea la estructura:
struct xyrec {
int x;
int y;
};
Puede declararse el puntero de tipo estructura:
struct xyrec *p;
llamar malloc() para reservar memoria en el monton y asignar su direccion a
p:
p = (struct xyrec *)malloc(sizeof(struct xyrec));
Para acceder los miembros de la estructura se usa entonces:
p->x = 123;
p->y =321;
El ejemplo siguiente ilustra estos conceptos.
Ejemplo V.8
#include <stdio.h>
#include <stdlib.h>
typedef struct xyrec {
int x;
int y;
} Xyrec;
typedef Xyrec *PXyrec;
main()
{
PXyrec xyp;
xyp = (PXyrec)malloc(sizeof(Xyrec));
if (xyp == NULL) {
puts("Out of memory!");
exit(1);
}
xyp->x = 10;
xyp->y = 11;
printf("x == %d\n", xyp->x);
printf("y == %d\n", xyp->y);
return 0;
}
Punteros y Funciones
--------------------
Las funciones pueden regresar punteros y se puede pasar punteros a los
parametros de una funcion. Tambien un puntero puede apuntar a una funcion,
es decir al codigo en vez de datos.
Antes, de considerar estos conceptos se usa el ejemplo siguiente para
demostrar como declarar y usar punteros a punteros.
Ejemplo V.9
#include <stdio.h>
main()
{
double d;
double *dp;
double **dpp;
d = 3.14159;
dp = &d;
dpp = &dp;
printf("Value of d == %f\n", d);
printf("Value of *dp == %f\n", *dp);
printf("Value of **dpp == %f\n", **dpp);
return 0;
}
Regresando a las funciones, si tenemos la estructura siguiente:
struct fecha {
int dia;
int mes;
unsigned anio;
};
Entonces puede diseñarse una funcion MuestraFecha() para que reciba un
puntero tipo struct fecha pasado como argumento. Su prototipo seria:
void MuestraFecha(struct fecha *pd);
Y la definicion de la funcion:
void MuestraFecha(struct fecha *pd)
{
printf("%02d/%02%d/%04d", pd->dia, pd->mes, pd->anio);
}
Para mostrar una fecha, en alguna parte del programa se tendria:
struct fecha laFecha = {23, 10, 96};
MuestraFecha(&laFecha);
Usando este metodo se ahorra tiempo y memoria ya que pasando la
informacion directamente se copia esta ultima en la pila.
Regresando a los punteros de punteros, el ejemplo siguiente ilustra los
requerimientos basicos que se necesitan para pasar un puntero de puntero
como parametro.
Ejemplo V.10
#include <stdio.h>
void Assign(double **dpp);
main()
{
double d;
double *dp;
d = 0;
dp = &d;
Assign(&dp);
printf("Valor de d == %f\n", d);
printf("Valor de *dp == %f\n", *dp);
return 0;
}
void Assign(double **dpp)
{
**dpp = 3.14159;
}
Experimentando con este ejemplo, vamos a agregar:
double q = 1.234;
y modificar Assign() sustituyendo la linea 20 por:
*dpp = &q;
Despues de eso *dp se refiere al nuevo valor global. Cuando se combinan
con estructuras los punteros de punteros, se debe tomar todavia mas
precauciones. El ejemplo siguiente ilustra el procedimiento a seguir.
Ejemplo V.11
#include <stdio.h>
typedef struct item {
int a;
int b;
} Item;
typedef Item *Itemptr;
typedef Itemptr *Itemref;
main()
{
Item i;
Itemptr ip;
Itemref ir;
i.a = 1;
i.b = 2;
ip = &i;
ir = &ip;
printf("Value of i.a == %d\n", (*ir)->a);
printf("Value of i.b == %d\n", (*ir)->b);
return 0;
}
El ejemplo V.12 lleva estos conceptos al proximo nivel logico, pasando un
puntero de puntero a estructura al parametro de una funcion.
Ejemplo V.12
#include <stdio.h>
typedef struct item {
int a;
int b;
} Item;
typedef Item *Itemptr;
typedef Itemptr *Itemref;
void Assign(Itemref ir);
main()
{
Item i;
Itemptr ip;
i.a = 1;
i.b = 2;
ip = &i;
Assign(&ip);
printf("Valor de i.a == %d\n", ip->a);
printf("Valor de i.b == %d\n", ip->b);
return 0;
}
void Assign(Itemref ir)
{
(*ir)->a = 4;
(*ir)->b = 5;
}
Experimentemos otra vez, agregando la declaracion:
Item q = {100, 200}
Y sustituyendo las lineas 29 y 30 en Assign() por:
*ir = &q;
Punteros como resultados de funciones
-------------------------------------
Las funciones que regresan un puntero pertenecen a dos categorias:
- Las funciones que modifican un argumento cuya direccion se pasa a la
funcion, y regresan un puntero a este mismo argumento.
- Las funciones que asignan espacio en el monton para una variable dinamica
nueva y regresan la direccion de este espacio o un puntero null si el
monton esta lleno o dañado.
El ejemplo V.13 ilustra un caso tipico de funcion que regresa un puntero a
caracter.
Ejemplo V.13
#include <stdio.h>
#include <ctype.h>
#include <string.h>
char *Uppercase(char *s);
main()
{
char title[] = "Este es el caso de la letra capital";
char *cp;
cp = Uppercase(title);
puts(cp);
return 0;
}
char *Uppercase(char *s)
{
int i;
for (i = 0; i < strlen(s); i++)
s[i] = toupper(s[i]);
return s;
}
La linea 5 el el prototipo de una funcion que regresa un puntero a
caracter. Las lineas 12 y 13 pueden sustituirse por:
puts(Uppercase(title));
El ejemplo V.14 muestra como se usa una funcion para crear variables
dinamicas nuevas en el monton (No compilar todavia este programa).
Ejemplo V.14
#include <stdio.h>
#include <alloc.h>
#define SIZE 64
void *MakeBlok(unsigned size, char init);
main()
{
void *p = NULL;
p = MakeBlok(SIZE, 0xff);
free(p);
return 0;
}
void *MakeBlok(unsigned size, char init)
{
void *p;
int i;
p = (void *)malloc(size);
if (p) {
for (i = 0; i < size; i++)
((char *)p)[i] = init;
}
return p;
}
La funcion MakeBlok() regresa un puntero tipo void de uso general que
puede apuntar a cualquier tipo de datos o a un buffer de memoria que
permite almacenar temporalmente informacion. Para asignar un espacio en el
monton se usa malloc() con un molde para evitar una advertencia del
compilador. Si malloc() no regresa un null (indicando un monton lleno o
dañado), el bucle llena la memoria recientemente asignada con el valor de
char init, pasado como argumento a la funcion. La expresion (char *)p
indica al compilador de considerar p como un puntero a un char. Sin
embargo, p apunta a un bloque de memoria de varios caracteres (si SIZE es
mayor que 1). El indice se usa entonces, para referirse a cada caracter
del bloque.
((char *)p) es un puntero a un char. Sin embargo, como punteros y arreglos
son equivalentes puede usarse indices para referirse a cada caracter de
bloque de memoria apuntado por p.
El programa debe compilarse desde la linea de comando del DOS con
tcc -v ej14.c.
El switch -v genera informacion que se puede usar para chequear como
funciona el programa usando el turbo debugger con td ejv14. Cuando aparece
el TD:
- Presione dos veces F8 para ejecutar el codigo de arranque del programa
e inicializar p a NULL.
- Coloque el cursor en p y presione Ctrl+I para inspeccionar el valor del
puntero (deberia ser igual a ds:0000).
- Presione F8 para llamar MakeBlok(). Examine el cambio de valor de p cuando
el programa le asigna el valor del puntero que regresa la funcion. El
puntero ahora apunta a un nuevo bloque de memoria en la direccion
indicada.
- Presione Alt+VD para abrir una ventana que permite visualizar el contenido
de la memoria.
- Presione Ctrl+G y teclee p para ver el bloque de datos apuntado por p.
Todos los bytes son igulaes a FF, el valor hexadecimal pasado a la funcion
(linea 11).
Punteros a Funciones
--------------------
Un puntero a funcion apunta al codigo ejecutable de la misma funcion. Se
declara de la manera siguiente:
float (* mifnptr)(void);
Esta declaracion crea un puntero a una funcion que regresa un float en vez
de regresar un puntero tipo float. Si la funcion requiere de parametros se
declaran de la manera usual:
float ( *mifnptr)(int x,int y);
Para asignar la direccion de la funcion a mifnptr, asumiendo que dicha
funcion se llama LaFuncion, se utiliza:
mifnptr = LaFuncion;
No se usa & ya que los nombres de funciones se consideran como punteros
que apuntan al codigo. La funcion se define de la manera usual:
float LaFuncion(int x, int y)
{
.........
}
La funcion puede llamarse directamente:
respuesta = LaFuncion(5,6);
o por referencia:
respuesta = (* mifnptr)(5,6);
El ejemplo V.15 ilustra estos conceptos en un programa que grafica una
funcion. Usando un puntero a funcion se simplifica la sustitucion de la
funcion sin tener que modificar el resto del programa.
Ejemplo V.15
#include <stdio.h>
#include <math.h>
#include <dos.h>
#include <conio.h>
#define XSCALE 20
#define YSCALE 10
#define XMIN 1
#define XMAX 79
#define YMIN 1
#define YMAX 25
typedef double (* Pfptr)(int x);
void Yplot(int x, double f);
double Afunction(int x);
main()
{
int x;
Pfptr pf = Afunction;
clrscr();
for (x = XMIN; x <= XMAX; x++)
Yplot(x, (* pf)(x * XSCALE));
gotoxy(XMIN, YMAX - 2);
return 0;
}
void Yplot(int x, double f)
{
gotoxy(x, YMIN + (YSCALE + (f * YSCALE)));
delay(50);
putch('*');
}
En la linea 25:
Yplot(x, (* pf)(x * XSCALE)); la expresion (* pf)(x*XSCALE) llama a la
funcion apuntada por pf, pasando como argumento (x * XSCALE). *pf indica
al compilador llamar al codigo apuntado por pf, es decir a la funcion que
se quiere graficar. En este caso sencillo es evidente que deberia haberse
usado: Yplot(x, Afunction(x*XSCALE)); Sin embargo, en un programa mas
grande donde el programa graficador puede encontrarse en un modulo y la
funcion matematica en otro, puede resultar mas conveniente usar la primera
de estas opciones que modificar la llamada a Yplot(). Ademas, este cambio
puede realizarse aun cuando no se tiene acceso al codigo fuente del
programa graficador.
Estructuras de datos dinamicas
------------------------------
a.- Listas
------
Una estructura puede poseer un puntero que apunta a otra estructura del
mismo tipo:
struct item (
int Inform;
struct item *sigue;
};
Se forman asi listas que pueden almacenar cualquier tipo de informacion:
Para crear una lista se declara primero la estructura:
typedef struct item {
int info;
struct item *sigue;
} Item;
typedef Item *Itemptr;
typedef Itemptr *Itemref;
En estas declaraciones Itemptr se usa en vez de struct Item *, que es un
puntero a un Item. Itemref a su vez es un puntero a un puntero a un Item.
A continuacion se declara un puntero a un Item que se usa para encabezar
la lista:
Itemptr head = NULL;
Cuando el puntero head es NULL, la lista esta vacia, razon por la cual se
inicializa el puntero en la declaracion. Para empezar la lista, se examina
head, y si este esta vacio se asigna espacio para un nuevo Item:
if (head == NULL) {
head = (Itemptr)malloc(sizeof(Item));
head->info = 1;
head->sigue = NULL;
}
Para agregar un segundo elemento se usa:
Itemptr p;
p = (Itemptr)malloc(sizeof(Item));
p->info = 2;
p->sigue = head->sigue;
head->sigue = p;
Para acceder a los elementos de la lista puede usarse:
i = head->info;
i = head->sigue->info;
i = head->sigue->sigue ->info;
i = head->sigue->sigue->sigue ->info;
Sin embargo, es preferible utilizar un bucle. Por ejemplo, para mostrar
los valores de la lista podria usarse:
Itemptr p;
p = head;
while (p != NULL) {
printf("%d\n", p->info);
p = p->sigue;
}
En este caso, el ultimo elemento de la lista debe tener su miembro sigue
null. Para borrar el elemento de la lista apuntado por head se usa:
Itemptr p;
p = head;
head = head->sigue;
free(p);
Pilas
-----
El ejemplo V.16 muestra como usar una lista para crear pilas de
estructuras Item. El programa crea dos listas, una para las estructuras
activas y otra para las estructuras borradas. Despues de compilar el
programa, presione A varias veces para agregar nuevos items a la pila y a
continuacion presione D el mismo numero de veces para borrar items y
agregarlos a la lista de estructuras borradas. Si se borra mas items que
creados, el programa crea estructuras iguales a -1 y las agrega a la lista
de items borrados. Si, a continuacion se presiona de nuevo A, en vez de
crear estructuras nuevas el programa utiliza cualquiera de la estructuras
de la lista de items borrados y la agrega a la lista de items activos.
Ejemplo V.16
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <conio.h>
#define FALSE 0
#define TRUE 1
typedef struct item {
int data;
struct item *next;
} Item;
typedef Item *Itemptr;
typedef Itemptr *Itemref;
void Push(Itemptr newitem, Itemref list);
void Pop(Itemref newitem, Itemref list);
void AddItem(void);
void DelItem(void);
void ShowList(Itemptr p);
void Display(void);
Itemptr avail;
Itemptr itemlist;
main()
{
int done = FALSE;
int c;
while (!done) {
Display();
gotoxy(1, 25);
cprintf("A-gregar, B-orrar, S-alir");
c = getch();
switch (toupper(c)) {
case 'A':
AddItem();
break;
case 'B':
DelItem();
break;
case 'S':
done = TRUE;
break;
}
}
return 0;
}
void Push(Itemptr newitem, Itemref list)
{
newitem->next = *list;
*list = newitem;
}
{
if (*list == NULL) { /* Si la lista esta vacia ... */
*newitem = malloc(sizeof(struct item)); /* Crear item */
(*newitem)->data = -1; /* Inicializa los datos en -1 */
} else {
*newitem = *list;
*list = (*list)->next;
}
}
void AddItem(void)
{
Itemptr newitem;
Pop(&newitem, &avail);
if (newitem->data < 0)
newitem->data = rand();
Push(newitem, &itemlist);
}
void DelItem(void)
{
Itemptr newitem;
Pop(&newitem, &itemlist);
Push(newitem, &avail);
}
void ShowList(Itemptr p)
{
while (p != NULL) {
gotoxy(1, wherey() + 1);
cprintf("%d", p->data);
clreol();
p = p->next;
}
}
void Display(void)
{
clrscr();
gotoxy(1, wherey() + 1);
cprintf("Avail list:");
ShowList(avail);
gotoxy(1, wherey() + 2);
cprintf("Item list:");
ShowList(itemlist);
}
Este programa no utiliza ningun concepto nuevo y en consecuencia deberia
entenderse la mayoria de sus sentencias.
Arboles
-------
Si se agrega otro puntero a las estructuras anteriores, cada estructura
puede apuntar a dos estructuras del mismo tipo al mismo tiempo, creandose
asi un arbol.
Typedef struct item {
char *info;
struct item *izq;
struct item *der;
} Item;
typedef Item *Itemptr;
typedef Itemptr * Itemref;
El ejemplo V.17 muestra como un programa puede examinar cada elemento del
arbol.
Ejemplo V.17
#include <stdio.h>
#include <string.h>
#include <alloc.h>
typedef struct item {
char *data;
struct item *left;
struct item *right;
} Item;
typedef Item *Itemptr;
typedef Itemptr *Itemref;
Itemptr root;
void Search(Itemref tree, const char *s);
void Process(Itemptr node);
void PreOrder(Itemptr node);
void InOrder(Itemptr node);
void PostOrder(Itemptr node);
main()
{
int done = 0;
char s[128];
puts("Arbol de Prueba");
while (!done) {
printf("Datos (enter para salir): ");
gets(s);
done = (strlen(s) == 0);
if (!done)
Search(&root, s);
}
puts("\nPREORDER:\n");
PreOrder(root);
puts("\n\nINORDER:\n");
InOrder(root);
puts("\n\nPOSTORDER:\n");
PostOrder(root);
puts("");
return 0;
}
void Search(Itemref tree, const char *s)
{
Itemptr p;
int cmpresult;
if (*tree == NULL) {
p = (Itemptr)malloc(sizeof(Item));
p->data = strdup(s);
p->left = NULL;
p->right = NULL;
*tree = p;
} else {
p = *tree;
cmpresult = strcmp(s, p->data);
if (cmpresult < 0)
Search(&p->left, s);
else if (cmpresult > 0)
Search(&p->right, s);
else {
puts("Datos Duplicados!");
Process(p);
puts("");
}
}
}
void Process(Itemptr node)
{
printf("%s ", node->data);
}
void PreOrder(Itemptr node)
{
if (node != NULL) {
Process(node);
PreOrder(node->left);
PreOrder(node->right);
}
}
void InOrder(Itemptr node)
{
if (node != NULL) {
InOrder(node->left);
Process(node);
InOrder(node->right);
}
}
void PostOrder(Itemptr node)
{
if (node != NULL) {
PostOrder(node->left);
PostOrder(node->right);
Process(node);
}
}
Compile y ejecute este programa. Cuando el programa le pide introducir la
informacion, introduzca mango, cambur, manzana, cereza, durazno y pera y
presione Intro despues del ultimo nombre.
Como puede verse en este programa, una de las ventajas de los arboles
binarios es su capacidad de mantener ordenados los datos sin necesidad de
usar una funcion de ordenacion particular.
Uso de punteros para acceder a la informacion sobre el sistema. El ejemplo
siguiente muestra como acceder a la informacion sobre el sistema usando
punteros. En este caso, se obtiene informacion sobre el display en el
arranque, asicomo el numero de columnas soportado
Ejemplo V.18
#include <stdio.h>
#include <dos.h>
main()
{
char far *mode = (char far *)MK_FP(0x0040, 0x0049);
int far *cols = (int far *)MK_FP(0x0040, 0x004a);
printf("CRT startup mode = %d\n", *mode);
printf("CRT columns = %d\n", *cols);
return 0;
}
El ejemplo V.19 muestra como usar punteros para detectar cuando el usuario
presiona las teclas Alt, ctrl, Shift Left, Shift Right.
Ejemplo V.19
#include <stdio.h>
#include <dos.h>
#include <string.h>
#include <conio.h>
typedef struct keyboard {
unsigned shiftRight : 1;
unsigned shiftLeft : 1;
unsigned ctrl : 1;
unsigned alt : 1;
unsigned scrollLock : 1;
unsigned numLock : 1;
unsigned capsLock : 1;
unsigned insert : 1;
unsigned : 8;
} Keyboard;
int CmpKeys(void far* p1, void far* p2);
void ShowString(int *y, char *s);
void ShowValue(int *y, char *s, unsigned v);
main()
{
Keyboard far *keys;
Keyboard oldkeys;
int done = 0;
int y;
clrscr();
keys = (Keyboard far *)MK_FP(0x0040, 0x0017);
while (!done) {
y = 1;
ShowString(&y, "Estado del teclado");
y++;
ShowString(&y, "State bits (press and release):");
ShowValue(&y, " <Insert> ....... ", keys->insert);
ShowValue(&y, " <Caps lock> .... ", keys->capsLock);
ShowValue(&y, " <Num lock> ..... ", keys->numLock);
ShowValue(&y, " <Scroll lock> .. ", keys->scrollLock);
y++;
ShowString(&y, "Shift bits. :");
ShowValue(&y, " <Alt> .......... ", keys->alt);
ShowValue(&y, " <Ctrl> ......... ", keys->ctrl);
ShowValue(&y, " <Shift left> ... ", keys->shiftLeft);
ShowValue(&y, " <Shift right> .. ", keys->shiftRight);
y++;
ShowString(&y, "Presione cualquier tecla para cambiar el estado");
ShowString(&y, "Presione <Ctrl>-<Alt>-<shiftLeft> para salir");
done = ( keys->alt
&& keys->ctrl
&& keys->shiftLeft);
oldkeys = *keys;
while (CmpKeys(&oldkeys, keys)) ;
}
gotoxy(1, 25);
return 0;
}
int CmpKeys(void far* p1, void far* p2)
{
return *(char far *)p1 == *(char far *)p2;
}
void ShowString(int *y, char *s)
{
gotoxy(1, *y);
cputs(s);
(*y)++;
}
void ShowValue(int *y, char *s, unsigned v)
{
ShowString(y, s);
cprintf("%u", v);
}
Como se accede a la memoria
---------------------------
Debe conocerse como accede el microprocesador a la memoria para saber
tambien como se accede a la memoria desde un programa. Conviene no olvidar
que los recursos de que se dispone en las instrucciones son en realidad
los que da el microprocesador a nivel hardware, y saber como accede el
micro a ella es saber como acceder desde los programas. Para empezar se
debe saber que, en un PC con micro 8086, la memoria instalada puede ser de
hasta un maximo de 1 Mbyte (1 Mbyte = 1024 Kbytes = 1048576 bytes =
octetos de bits). Dicha cantidad de bytes estan colocados secuencialmente
y cada uno de ellos tiene un numero asociado correspondiente al lugar que
ocupan (offset que significa <desplazamiento relativo a base>). Se tienen
pues bytes que van desde el 0 hasta el 1048575. Dicho numero no cabe en
16 bits, que es lo maximo (offset maximo) que puede indicar la CPU al
sistema mediante el bus de direcciones de 16 bits que posee; por lo tanto,
en principio, teoricamente solo se podria acceder hasta el byte numero 65535
(el numero mayor posible de 16 bits) contando a partir del inicio, que es el
byte 0.
Para solucionar esto se creo un complemento al offset a la hora de indicar
el numero de byte a que se quiere acceder. A este complemento se le llama
Base y corresponde a lo que se conoce por segmento en ensamblador, que es
tambien un numero de 16 bits que da, por decirlo asi, la posibilidad de
tener una base programable. De esta forma, cada vez que se quiere acceder a
un byte concreto se debe indicarle en que byte empieza la base virtual
(segmento) a partir de la cual empieza a contar el sistema el offset que se
le indique junto a ella. Si, como hemos visto, solo se puede acceder a
65536 bytes con el OFFSET, para poder acceder a todos los bytes por encima
del byte numero 65535, se invento el truco de que la base siempre funcionase
como si se multiplicase por 16. De esta forma, cuando por ejemplo se indica
al ordenador como base el segmento con valor 10000, se esta indicando que
se quiere acceder al byte numero OFFSET que se encuentra contando a partir
del byte numero 10000*16, o sea a partir del byte 160000. El ordenador,
gracias al truco de multiplicar la base*16, hace que pueda crear bases
usando un numero de 16 bits que lleguen hasta el limite del meqabvte de
que disponemos (65536*16 = 1 Mbyte).
Mediante este metodo, desde el segmento que se le indica, se puede ahora
acceder a los 65536 bytes que haya a partir del byte que corresponde a esa
base (segmento). De esta forma se tiene acceso a toda la memoria del PC (a
cada uno de los 1024 Kbytes). A la direccion de memoria completa que apunta
a un byte, por ejemplo 50:4000, se le da el nombre de puntero (pointer en
ingles) y es un tecnicismo muy conocido tambien entre los programadores de
C y todos los lenguajes de bajo-medio nivel que permiten trabajar
directamente con direcciones de memoria.
Para representar de forma escrita a los punteros se escriben ambos
componentes separados solo por dos puntos, colocando en primer lugar al
segmento. Dicha sintaxis de representacion es un estandar de programacion.
Ejemplos:
1- Si decimos que tenemos un segmento con valor 50 y un offset con valor
40000 (50:40000), se esta indicando que se accede al byte 50*16+ 40000.
2- Si se pone por eiemplo la direccion (Segmento:offset) 10000:5000, para
saber a que byte lineal corresponde dentro de la memoria del PC, solo
hay que calcular: (10000*16)+5000. Osea, en este caso se accede al byte
numero 165000.
<EOF>