Bitácora del desarrollo de mi clase de C++, en el que publicaré el material de la clase y recibiré comentarios y sugerencias de mis alumnos.

miércoles, 9 de mayo de 2007

10.2 Archivos de Acceso Aleatorio

Archivos de acceso directo o aleatorio
La función fwrite transfiere a un archivo un número especificado de bytes empezando en una posición especificada de memoria. Los datos se escriben al principio de la posición en el archivo indicada mediante el apuntador de posición de un archivo. La función fread transfiere un número especificado de bytes de la posición en el archivo, especificado por el apuntador de posición de archivo, a un área en memoria empezando a partir de una dirección especificada. Por ejemplo, para escribir un entero, en vez de especificar:

fprintf(cuentaPtr, “%d”, numero);
para escribir el mismo número de tipo int, se puede especificar:
fwrite( &numero, sizeof(int), 1, cuentaPtr);
más adelante ese dato puede ser leído con la función fread.

El primer parámetro de fprintf es la dirección de la variable a escribir en disco, el segundo el tamaño de bytes del dato que se va a escribir (devuelto por la función sizeof), el tercer parámetro es el número de datos que se van a escribir del tamaño especificado (por lo común será 1) y el último es el apuntador al archivo que se abrió para escritura o modificación.

Para tener datos de tamaño fijo en los archivos y lo que formalmente serán registros del archivo se crea una estructura, es decir, un nuevo tipo de dato definido por el usuario y compuesto por uno o varios tipos de datos con sus nombres de campo respectivos.

La declaración de un tipo struct se hace antes de la función main de la siguiente manera:

struct nombreEstructura
{
tipo1 campo1;
tipo2 campo2;
tipo3 campo3;
};

Por ejemplo, para crear una estructura que permita almacenar los datos de un cliente puede declararse de la siguiente manera:

struct DatosCliente
{
int cuenta;
char apellido[15];
char nombre[10];
float saldo;
};

De este modo se crea una estructura de nombre DatosCliente con los campos descritos, en el programa se podrán declarar variables de tipo DatosCliente y para acceder a cada uno de los campos se utilizara la notación registro.campo. La declaración de una variable de este tipo se hará:

struct DatosCliente micliente;

De esta manera la variable micliente tiene los cuatro campos definidos para el tipo de estructura, y el nombre del cliente se accede con la siguiente línea:

micliente.nombre

El uso de una estructura y de las funciones fread, fwrite y fseek será básicamente la diferencia entre programar para utilizar un archivo secuencial y un archivo de acceso directo.
Cómo crear un archivo de acceso directo.

/* Creacion de un archivo de acceso directo */
#include (stdio.h)
#include (conio.h)
struct DatosCliente
{
int cuenta;
char apellido[15];
char nombre[10];
float saldo;
};

main( )
{
int i;
struct DatosCliente clienteNulo = {0,"","",0.0};
FILE *cuentaPtr;

if ((cuentaPtr = fopen("cliente.dat","w")) == NULL)
printf("No se puede abrir el archivo\n");
else
{
/* Escritura de 100 registros en “blanco”*/
for (i=1; i<= 100; i++)
fwrite( &clienteNulo, sizeof(struct DatosCliente),1, cuentaPtr);
fclose(cuentaPtr);
}
getch ( );
return 0;
}

Cómo escribir y leer datos de un archivo de acceso directo
La introducción de los datos será mediante la misma función fwrite, donde lo que se escribe es la variable del tipo de la estructura deseada, con el tamaño de la misma estructura y el apuntador al archivo. Cada dato debe ser llenado con scanf al campo deseado de la misma estructura. A continuación se presenta un ejemplo de captura de datos y escritura al archivo.

/* Escribiendo a un archivo de acceso aleatorio */
#include (stdio.h)
#include (stdio.h)
struct DatosCliente
{
int cuenta;
char apellido[15];
char nombre[10];
float saldo;
};

main( )
{
/* la variable cliente de tipo DatosCliente servirá para
asignar los almacenar los valores para cada cliente introducido */
struct DatosCliente cliente;
FILE *cuentaPtr;
if ((cuentaPtr = fopen("cliente.dat","r+")) == NULL)
printf("No se puede abrir el archivo\n");
else
{
printf("Introduzca numero de cuenta"
" (1 a 100), 0 para finalizar)\n?");
scanf("%d", &cliente.cuenta);
while (cliente.cuenta != 0)
{
printf("Introduzca el apellido, nombre y saldo\n? ");
scanf("%s%s%f", cliente.apellido, cliente.nombre, &cliente.saldo);
fseek(cuentaPtr, (cliente.cuenta - 1) *
sizeof(struct DatosCliente), SEEK_SET);
fwrite(&cliente, sizeof(struct DatosCliente), 1, cuentaPtr);
printf("Intruduzca n{umero de cuenta\n? ");
scanf("%d", &cliente.cuenta);
}
}
fclose(cuentaPtr);
getch ( );
return 0;
}

La lectura de cada registro en el archivo se hará con la función fread, cuyos parámetros son: la variable que almacena los datos leídos del archivo, el tamaño de la estructura o del tipo de dato, el número de datos a leer y el apuntador al archivo.

/* Leyendo de un archivo de acceso aleatorio */
#include (stdio.h)
#include (conio.h)
struct DatosCliente
{
int cuenta;
char apellido[15];
char nombre[10];
float saldo;
};
main( )
{
struct DatosCliente cliente;
FILE *cuentaPtr;
clrscr();
if ((cuentaPtr = fopen("cliente.dat","r")) == NULL)
printf("No se puede abrir el archivo\n");
else
{
printf("%-8s%-16s%-11s%10s\n", "Cuenta", "Apellido", "Nombre",
"Saldo");
while (!feof(cuentaPtr))
{
fread(&cliente, sizeof(struct DatosCliente), 1, cuentaPtr);
if (cliente.cuenta != 0)
printf("%-8d%-16s%-1s%10.2f\n", cliente.cuenta,
cliente.apellido, cliente.nombre, cliente.saldo);
}
}
fclose(cuentaPtr);
getch();
return 0;
}

lunes, 7 de mayo de 2007

10.1 Archivos de Acceso Secuencial

Cómo crear un archivo de acceso secuencial
Para abrir un archivo en un programa escrito en C, es necesario utilizar la función fopen(), la cual recibe dos parámetros, el nombre del archivo y el modo de apertura del archivo. Los modos de apertura se describen a continuación.

MODO DESCRIPCIÓN
r Abrir un archivo para lectura.
w Crear un archivo para escritura. Si el archivo ya existe se pierde el contenido actual.
a Agregar, abrir o crear un archivo para escribir al final del mismo.
r+ Abrir un archivo para actualizar (leer y escribir).
w+ Crear un archivo para actualizar, si el archivo ya existe se pierde el contenido actual.
a+ Agregar, abrir o crear un archivo para actualizar, la escritura se efectuará al final del archivo.

A continuación se presenta un ejemplo de creación de un archivo secuencial:

/* Creacion de un archivo de acceso secuencial */
#include (stdio.h)
#include (conio.h)
main( )
{
int cuenta;
char nombre[30];
float saldo;
FILE *cuentaPtr;
if ((cuentaPtr = fopen("cliente.dat","w")) == NULL)
printf("No se puede abrir el archivo\n");
else
{
printf("Introduzca cuenta, nombre y saldo.\n");
printf("Introduzca EOF para terminar.\n");
printf("? ");
scanf("%d%s%f", &cuenta, nombre, &saldo);
while (!feof(stdin))
{
fprintf(cuentaPtr, "%d %s %.2f\n", cuenta, nombre,saldo);
printf("? ");
scanf("%d%s%f", &cuenta, nombre, &saldo);
}
fclose(cuentaPtr);
}
getch ( );
return 0;
}

El enunciado
FILE *cuentaPtr;
establece que cuentaPtr es una apuntador a una estructura de tipo FILE. Cada archivo abierto debe tener un apuntador declarado por separado del tipo FILE, que es utilizado para referirse al archivo.

La línea
if ((cuentaPtr = fopen("cliente.dat","w")) == NULL)
nombra el archivo cliente.dat, para ser utilizado por el programa y establece una comunicación con el archivo a través del apuntador, en este caso el archivo se abre para escritura. La estructura if se utiliza para determinar si el apuntador al archivo cuentaPtr es NULL, es decir, que el archivo no está abierto. Si es NULL, se imprime un mensaje de error y el programa termina, de lo contrario la entrada es procesada y escrita al archivo.

La línea
while (!feof(stdin))
utiliza la función feof para determinar si el indicador de fin de archivo está definido para el archivo a que se refiere stdin. El indicador de fin de archivo le informa al programa que ya no hay más datos a procesar., el indicador de fin de archivo está definido para la entrada estándar cuando el usuario introduce la combinación de teclas de fin de archivo (Control-Z). La estructura while continuará ejecutándose hasta que se defina el indicador de fin de archivo.

El enunciado
fprintf(cuentaPtr, "%d %s %.2f\n", cuenta, nombre,saldo);
escribe datos al archivo cliente.dat, que es al que apunta el apuntador cuentaPtr. Los datos pueden ser recuperados más tarde mediante un programa diseñado para leer el archivo.

Al terminar de ejecutar al programa compruebe la creación del archivo cliente.dat. Desde el explorador de Windows o saliendo al símbolo de sistema mediante la orden File- Dos shell, verifique la existencia y el contenido del archivo cliente.dat. Si se desea que el archivo sea creado en la unidad A:, el parámetro de la función fopen debe ser “a:cliente.dat”, de otra manera se creará en el directorio actual (generalmente C:\TC\BIN).

La función fclose() cierra siempre el archivo al cual apunta el apuntador enviado como parámetro, en este caso fclose(cuentaPtr) cierra el archivo cliente.dat.

Cómo leer datos de un archivo de acceso secuencial
Una vez que se ha creado el arcihvo, la lectura puede resultar más cómoda, pues las diferencias con la creación del archivo son únicamente el modo de apertura del archivo “r”, y la manera en que se leen los datos mediante la función fscanf.

La función fscanf, se le indica de qué archivo se leerán los datos, mandándole como parámetro el apuntador a FILE, en este caso cuentaPtr, la cadena de control para indicarle qué tipo de datos leerá del archivo, y la dirección de las variables a las que asignará los valores leídos (tal como a scanf). Obviamente que si el archivo tiene una ubicación diferente a la actual, el parámetro de nombre de archivo en la función debe tener la ruta completa, por ejemplo
fopen(“A:CLIENTE.DAT, “r”);

A continuación se presenta el ejemplo de lectura del archivo creado en el apartado anterior.

/* Leyendo datos de un archivo de acceso secuencial */
#include (stdio.h)
#include (conio.h)
main( )
{
int cuenta;
char nombre[30];
float saldo;
FILE *cuentaPtr;

if ((cuentaPtr = fopen("cliente.dat","r")) == NULL)
printf("No se puede abrir el archivo\n");
else
{
printf("%-10s%-13s%s\n","Cuenta","Nombre","Saldo");
fscanf(cuentaPtr,"%d%s%f",&cuenta, nombre, &saldo);

while (!feof(cuentaPtr))
{
printf("%-10d%-13s%7.2f\n", cuenta, nombre, saldo);
fscanf(cuentaPtr,"%d%s%f",&cuenta, nombre, &saldo);
}
fclose(cuentaPtr);
}
getch( );
return 0;
}

10.0 Archivos

Procesamiento de archivos
El almacenamiento de datos en variables y en arreglos es temporal; al terminar un programa todos estos datos se pierden. Para la conservación permanente de grandes cantidades de datos se utilizan los archivos. Las computadoras almacenan los archivos en dispositivos de almacenamiento secundario, especialmente en dispositivos de almacenamiento en disco.

La jerarquía de los datos
Todos los elementos de datos procesados por una computadora se reducen a combinaciones de ceros y unos. En una computadora el elemento de datos mas pequeño puede asumir el valor 0 o el valor 1. Este elemento de datos se conoce como un bit.

Sin embargo normalmente los usuarios y programadores de computadoras no trabajan con 0’s y 1’s sino con datos en forma de dígitos decimales, letras y símbolos especiales que se conocen como caracteres, que pueden ser representados por conjuntos de 8 bits, es decir, un byte, que no es otra cosa más que diferentes combinaciones de 1 y 0 en esos 8 bits.

Al igual que los caracteres están formados por bits, los campos están formados por caracteres. Un campo es un grupo de caracteres que contiene un significado. Por ejemplo, un campo que consista únicamente de letras mayúsculas y minúsculas, puede ser utilizado para representar el nombre de una persona.

Un registro es un conjunto de campos relacionados, y que generalmente son atributos de una misma entidad, por ejemplo, un registro de alumnos, estará formado por los campos de Nombre, Edad, Semestre, Grupo, etc., todos ellos datos del propio alumno.

Un archivo es un conjunto de registros del mismo tipo, siguiendo con el ejemplo anterior, el archivo para los alumnos de este grupo, tendría 32 registros, un registro para almacenar los datos de cada alumno.

Existen varias formas de organizar los registros dentro de un archivo. El tipo más popular de organización se conoce como archivo secuencial o archivo de acceso secuencial, esto significa que los registro se leen y se escriben al archivo uno tras otro.

Archivos y flujos
C ve cada uno de los archivos como un flujo secuencial de bytes. Cada archivo debe terminar con un marcador de fin de archivo.

Siempre que se ejecuta un programa se abren tres archivos y sus flujos asociados: la entrada estándar, la salida estándar y el error estándar. Cada uno de ellos es manejado con un apuntador de archivo, stdin, stdout y stderr, respectivamente.

lunes, 12 de marzo de 2007

9.2. Funciones principales de las librerías stdio.h y conio.h

Tema revisado

9.1. Definición de la función main

Tema revisado

8.2. Apuntadores

Apuntadores
Los apuntadores son variables que contienen direcciones de memoria como valores. Por lo regular una variable contiene directamente un valor específico. Un apuntador, por otra parte, contiene la dirección de una variable que contiene un valor específico. Un nombre de variable común se refiere directamente a un valor y un apuntador se refiere indirectamente a un valor. Los apuntadores le permiten a los programas simular llamadas por referencia, crear y manipular estructuras de datos.

Declaración e inicialización de variables de apuntador.
Como cualquier otra variable, los apuntadores deben ser declarados antes de que puedan ser utilizados. De forma general, un apuntador se declara como:

tipo *nombre_apuntador;

La declaración

int *contadorPtr, contador;

declara la variable contadorPtr del tipo int *, es decir, un apuntador a un valor entero y se lee contadorPtr es un apuntador a entero o apunta a un entero. También la variable contador se declara como tipo entero (no como apuntador).

Los apuntadores deben ser inicializados cuando son declarados o en un enunciado de asignación. Un apuntador puede ser inicializado a 0 (cero), NULL o a una dirección. Un apuntador con el valor NULL apunta a nada, NULL es una constante simbólica definida en el archivo de cabecera stdio.h. Inicializar un apuntador a 0 es equivalente a inicializar un apuntador a NULL, pero es preferible NULL. Cuando se asigna 0, primero se convierte en un apuntador al tipo apropiado. El valor 0 es el único valor entero que puede ser directamente asignado a una variable apuntador.


Operadores de apuntador
El & (ampersand), u operador de dirección, es un operador que regresa la dirección de su operando. Por ejemplo, suponiendo las declaraciones

int y=5;
int *yPtr;

el enunciado

yPtr = &y;

asigna la dirección de la variable y a la variable apuntador yPtr. Se dice entonces que la variable yPtr “apunta a y”. Suponiendo que la variable entera y se almacena en la posición de memoria 600000, y la variable de apuntador yPtr se almacena en la posición 500000, el valor de yPtr (en la posición de memoria 500000) es la dirección 600000, donde se encuentra almacenado el valor de y.

El operador * regresa el valor del objeto hacia el cual su operando apunta, el operando es un apuntador. Por ejemplo:

printf(“%d”, *yPtr);

imprime el valor de la variable y, es decir 5. *yPtr, se lee como el contenido de la dirección a la que apunta yPtr.

En el siguiente ejemplo se comprueba el uso de los operadores de apuntadores. %p se utiliza para mostrar el equivalente hexadecimal de la dirección de memoria.

#include (stdio.h)
#include (conio.h)
main( )
{
int y, *yPtr;
y=7;
clrscr();
yPtr=&y;
printf("La dirección de y es %p\n""El valor de yPtr es %p\n\n",&y, yPtr);
printf("El valor de y es %d\nEl valor de *yPtr es %d\n\n",y, *yPtr);
printf("* y & son operadores complementarios\n&*yPtr = %p\n\n*&yPtr = %p\n", &amp;amp;amp;amp;amp;*yPtr, *&yPtr);
return 0;
}

La dirección de y es FFF4
El valor de yPtr es FFF4

El valor de y es 7
El valor de *yPtr es 7

* y & son operadores complementarios
*&yPtr = FFF4
&*yPtr = FFF4


Cómo pasar parámetros a funciones por referencia
Como ya se ha visto existen dos formas de pasar argumentos o parámetros a una función, por valor y por referencia. Muchas funciones requieren la capacidad de modificar una o más variables de la rutina que la llama. Aunque en C todas las llamadas a funciones son por valor, se pueden utilizar apuntadores para simular llamadas por referencia y no sobrecargar con copias de variables. Cuando se llama a una función con argumentos que deban modificarse, se pasan las direcciones de los argumentos, esto se lleva a cabo aplicando el operador de dirección & a la variable cuyo valor deba ser modificado. Al recibir la dirección como parámetro, la función puede utilizar el operador * para modificar el contenido de esa dirección, es decir, el valor de esa posición en la memoria del llamador. A continuación se presentan dos ejemplos para explicar la diferencia.

/* Elevar una variable al cubo utilizando llamada por valor */
#include (stdio.h)
#include (conio.h)
int cuboPorValor(int);
main( )
{
int numero=5;
clrscr();
printf("El valor original de numero es %d\n", numero);
numero = cuboPorValor(numero);
printf("El nuevo valor de numero es %d\n", numero);
return 0;
}

int cuboPorValor(int n)
{
return n * n * n;
}

Llamada por referencia:
/* Elevar una variable al cubo utilizando llamada por referencia */
#include (stdio.h)
#include (conio.h)
void cuboPorReferencia(int *);
main( )
{
int numero=5;
clrscr();
printf("El valor original de numero es %d\n", numero);
cuboPorReferencia(&numero);
printf("El nuevo valor de numero es %d\n", numero);
return 0;
}

void cuboPorReferencia(int *nPtr)
{
*nPtr= *nPtr * *nPtr * *nPtr;
}

En ambos casos la salida es:

El valor original de numero es 5
El nuevo valor de numero es 125

En el primer ejemplo se hace una asignación directa de lo que devuelve cuboPorValor a la variable numero, en el segundo ejemplo, la misma función cuboPorReferencia se encarga de modificar la variable numero, es decir, el contenido de la dirección de numero.

Como utilizar el calificador Const con apuntadores
El calificaor const permite al programador informarle al compilador que el valor de una variable particular no deberá ser modificado. En las primeras versiones de C no existía el calificador const; fue añadido al lenguaje por el comité de ANSI C.

A través de los años, una gran base de código heredado quedó escrita en las primeras versiones de C, que no utilizan a const porque no estaba disponible. Por esta razón, existen grandes oportunidades de mejoría en la ingeniería de software del código existente de C. También, muchos programadores, que en la actualidad utilizan ANSI C, en sus programas no utilizan const, porque empezaron a programar usando las primeras versiones de C. Estos programadores están perdiendo muchas oportunidades de buena ingeniería de Software.

Existen seis posibilidades para el uso (o el no uso) de const con parámetros de función, dos al pasar parámetros en llamadas por valor y cuatro al pasar parámetros con llamadas por referencia. ¿Cómo escoger una de las seis posibilidades?, deje que el principio del mínimo privilegio sea su guía. Siempre otorgue a una función suficiente acceso a los datos en sus parámetros para llevar a cabo su tarea específica, pero no más.

Existen cuatro formas para pasar un apuntador a una función: un apuntador no constante a datos no constantes, un apuntador constante a datos no constantes, un apuntador no constante a datos constantes y un apuntador constante a datos constantes. Cada una de las cuatro combinaciones proporciona un nivel distinto de privilegio de acceso.

El nivel más alto de acceso de datos se consigue mediante un apuntador no constante a datos no constantes. En este caso, los datos pueden ser modificados a través de un apuntador desreferenciado, y el apuntador puede ser modificado para señalar a otros elementos de datos. Una declaración para un apuntador no constante a datos no constantes no incluye const Tal apuntador pudiera ser utilizado para recibir una cadena como argumento a una función que utiliza aritmética de apuntadores para procesar ( y posiblemente para modificar) cada carácter dentro de la cadena. La función convertirEnMayusculas declara como su argumento un apuntador no constante a datos no constantes, llamado s (char *s).

/* convertir letras minusculas a letras mayusculas*/
/* Utilizando un apuntador no constante a datos no constantes */
#include (stdio.h)
#include (conio.h)
void convertirEnMayusculas (char *);
main ( )
{
char string []="caracteres en minusculas";
printf ("La cadena antes de la conversion es: %s\n", string);
convertirEnMayusculas (string);
printf ("La cadena despues de la conversion es: %s\n", string);
getch ();
return 0;
}
void convertirEnMayusculas (char *s)
{
while (*s != '\0')
{
if (*s)= 'a' && *s (= 'z')
*s -=32;
++s;
}
}

Un apuntador no constante a datos constantes es un apuntador que puede ser modificado para apuntar a cualquier elemento de datos del tipo apropiado, pero no pueden ser modificados los datos hacia los cuales apunta. Tal apuntador pudiera ser utilizado para recibir un argumento de arreglo a una función, que procesaría cada elemento del arreglo, sin modificar los datos. Por ejemplo la función imprimeCaracteres declara los parámetros del tipo const char *. La declaración se lee de derecha a izquierda de la forma “s es un apuntador a una constante de carácter”. El cuerpo de la función utiliza una estructura for para extraer cada carácter de la cadena, hasta que encuentre el carácter NULL. Después de haber impreso cada carácter, el apuntador s es incrementado, para que apunte al siguiente carácter dentro de la cadena.

/* Imprimiendo una cadena mediante un caracter a la vez*/
/* Utilizando un apuntador no constante a datos constantes */
#include (stdio.h)
#include (conio.h)
void imprimeCaracteres (const char *);
main ( )
{
char string []="Imprimiendo los caracteres de una cadena";
printf ("La cadena es:\n");
imprimeCaracteres (string);
putchar ('\n');
getch ( );
return 0;
}

void imprimeCaracteres (const char *s)
{
for ( ;*s != ‘\0’ ; s++)
putchar(*s);
}

En el siguiente programa se muestra los mensajes de error producidos por el compilador Borland C++ al intentar compilar una función que recibe un apuntador no constante a datos constante y la función utiliza el apuntador con el fin de modificar los datos. /* Intentando modificar datos a traves de*/ /* Un apuntador no constante a datos constantes */ #include (stdio.h) #include (conio.h) void funcion (const int *); main ( ) { int y; funcion (&y); getch ( ); return 0; } void funcion (const int *x) { *x=100; } Un apuntador constante a datos no constantes, es un apuntador que siempre apunta a la misma posición de memoria y los datos en esa posición pueden ser modificados a través del apuntador. Esta es la forma por omisión de un nombre de arreglo. Un nombre de arreglo es un apuntador constante al inicio de dicho arreglo y los subíndices del mismo. Un apuntador constante a datos no constantes puede ser utilizado para recibir un arreglo como argumento de una función, que tiene acceso a los elementos del arreglo utilizando solo notaciones de subíndices del arreglo. Los apuntadores declarados const deben ser inicializados al ser declarados (si el apuntador es un parámetro de función, será inicializado con un apuntador que se pasa a la función). El programa siguiente intenta modificar un apuntador constante. El apuntador ptr se declara ser del tipo int * const. La declaración se lee de derecha a izquierda como “ptr es un apuntador constante a un entero”. El apuntador se inicializa con la dirección de la variable entera x. El programa intenta asignar la dirección de y a ptr, por lo que se genera el mensaje de error. /* Intentando modificar un apuntador constante*/ /* a un dato no constante */ #include (stdio.h) #include (conio.h) main ( ) { int x, y; int * const ptr=&x; ptr=&y; getch ( ); return 0; } El privilegio del mínimo acceso se concede mediante un apuntador constante a datos constantes. Un apuntador de este tipo siempre apunta a la misma posición de memoria y los datos en esa posición de memoria no pueden modificados. Esta es la forma en que debería pasarse un arreglo a una función que solo ve al arreglo utilizando notación de subíndices del mismo y no modifica dicho arreglo. El siguiente programa declara la variable de apuntador ptr ser del tipo const int * const. Esta declaración se lee de derecha a izquierda como “ptr es un apuntador constante a un entero constante”. Al compilarlo se muestran los mensajes de error cuando se intenta modificar los datos a los cuales apunta ptr, y cuando se intenta modificar la dirección almacenada en la variable del apuntador. /* Intentando modificar un apuntador constante*/ /* a un dato constante */ #include (stdio.h) #include (conio.h) main ( ) { int x=7, y; const int * const ptr=&x; *ptr=5; ptr = &y; getch ( ); return 0; }

Expresiones y aritmética de apuntadores
Los apuntadores son operadores válidos en expresiones aritméticas, en expresiones de asignación y en expresiones de comparación, sin embargo no todos los operadores normalmente utilizados en estas expresiones son válidos, en conjunción con las variables de apuntador. En este tema se describen los operadores que pueden tener apuntadores como operadores y como se utilizan dichos operadores.

Con apuntadores se pueden ejecutar un conjunto limitado de operaciones aritméticas. Un apuntador puede ser incrementado (++) o decrementado (- -), se puede añadir un entero a un apuntador (+ o +=), un entero puede ser restado de un apuntador (- o -=), o un apuntador puede ser sustraído o restado de otro.

Suponga que ha sido declarado el arreglo int v[10] y su primer elemento esta en memoria en la posición 3000. Suponga que el apuntador vptr ha sido inicializado para apuntar a v[0], es decir el valor de vptr es 3000. En la figura siguiente se muestra esta situación para una máquina con enteros de 4 bytes. Note que vptr puede ser inicializado para apuntar al arreglo v con cualquiera de los siguientes enunciados:

vptr = v;
vptr = &v[0];

En aritmética convencional, la adición 3000+2 da como resultado 3002, por lo regular, este no es el caso en la aritmética de apuntadores, cuando se añade o resta un entero de un apuntador, el apuntador no se incrementa o decrementa sólo por el valor de dicho numero, sino por el entero multiplicado por el tamaño del objeto al cual el apuntador se refiere. El número de bytes depende del tipo de datos del objeto. Por ejemplo el enunciado:

vptr += 2;

produciría 3008 (3000+2*4), suponiendo un entero almacenado en 4 bytes de memoria. En el arreglo v, vptr ahora señalaría a v[2]. Si un entero se almacena en 2 bytes de memoria, entonces el cálculo anterior resultaría en una posición de memoria 3004 (3000+2*2).

Si vptr ha sido incrementado a 3016, lo que señala a v[4], el enunciado:

vptr -= 4;

definiría a vptr de vuelta a 3000 lo que es decir al principio del arreglo. Si un apuntador está siendo incrementado o decrementado por uno, pueden ser utilizados los operadores de incremento (++) y de decremento (- -). Cualquiera de los enunciados:

++vptr;
vptr++;

incrementan el apuntador, para apunte a la siguiente posición dentro del arreglo. Cualquiera de los enunciados:

--vptr;
vptr--;

decrementa el apuntador, para que apunte al elemento anterior del arreglo.

Las variables de apuntadores pueden ser restadas una de otra, por ejemplo si vptr contiene la posición 3000, y v2ptr contiene la dirección 3008, el enunciado:

x = v2ptr-vptr;

asignaría a x el número de los elementos del arreglo de vptr hasta v2ptr, en este caso sería 2. La aritmética de apuntadores no tiene efecto, a menos que se ejecute en un arreglo. No podemos suponer que dos variables del mismo tipo estén almacenadas de manera contigua en memoria, a menos de que sean elementos adyacentes de un arreglo.


Relación entre apuntadores y arreglos
Los arreglos y los apuntadores en c están relacionados en forma íntima y pueden ser utilizados casi en forma indistinta. Un nombre de arreglo puede ser considerado como un apuntador constante. Los apuntadores pueden ser utilizados para hacer cualquier operación que involucre subíndices de arreglos.

Suponga que han sido declarados el arreglo entero b[5] y la variable de apuntador entera bptr, dado que el nombre del arreglo (sin subíndice) es un apuntador al primer elemento del arreglo, podemos definir bptr igual a la dirección del primer elemento en el arreglo b, mediante el enunciado:

bptr = b;

Este enunciado es equivalente a tomar la dirección del primer elemnto del arreglo, como se muestra a continuación:

bptr = &b[0];

Alternativamente el elemento del arreglo b[3] puede ser referenciado con la expresión de apuntador:

*(bptr+3)

El 3 en la expresión arriba citada es el desplazamiento del apuntador, cuando el apuntador apunta al principio de un arreglo, el desplazamiento indica qué elemento del arreglo debe ser referenciado, y el valor de desplazamiento es idéntico al subíndice del arreglo. La notación anterior se conoce como notación apuntador/desplazamiento. Son necesarios los paréntesis porque la precedencia de * es más alta que la de +, sin los paréntesis la expresión arriba citada sumaría 3 al valor de la expresión *bptr (es decir, se añadirían 3 a b[0], suponiendo que bptr apunta al principio del arreglo). Al igual que el elemento del arreglo puede ser referenciado con una expresión de apuntador, la dirección &b[3] puede ser escrita con la expresión de apuntador bptr+3.

El arreglo mismo puede ser tratado como un apuntador y utilizado en aritmética de apuntador, por ejemplo la expresión:

*(b+3)

también se refiere al elemento del arreglo b[3], en general, todas las expresiones de arreglos con subíndices pueden ser escritas mediante un apuntador y un desplazamiento, en este caso se utilizó notación apuntador/desplazamiento junto con el nombre del arreglo como apuntador. Note que el enunciado anterior no cambia de forma alguna el nombre del arreglo; b aún apunta al primer elemento del arreglo.

Los apuntadores pueden tener subíndices exactamente de la misma forma que los arreglos, por ejemplo la expresión:

bptr[1]

se refiere al elemento del arreglo b[1], esto se conoce como notación apuntador/subíndice, recuerde que el nombre de un arreglo es, en esencia un apuntador constante, siempre apunta al principio del arreglo, por lo tanto la expresión:

b+=3

resulta inválida, porque intenta modificar el valor del nombre de un arreglo con aritmética de apuntador.

El programa siguiente utiliza los cuatro métodos analizados aquí para referir a los elementos del arreglo, arreglos con subíndices, apuntador/desplazmiento con el nombre del arreglo como un apuntador, subíndice de apuntador y apuntador/desplzamiento con un apuntador para imprimir los cuatro elementos del arreglo entero b.

/* Usando subindices y notación de apuntadores con arreglos */
#include (stdio.h)
#include (conio.h)
main ( )
{
int i, despl, b[ ]={10, 20, 30, 40};
int *bptr=b;
printf(“Arreglo b impreso con notacion de subindices de arreglos\n”);
for (i=0; i(=3; i++)
printf(“b[%d] = %d\n”, i, b[i]); printf(“\nNotacion apuntador/desplazamiento cuando\nel apuntador es el nombre del arreglo\n”); for (despl=0; despl<=3; despl++) printf(“*(b+%d) = %d\n”, despl, *(b+despl)); printf(“\nUsando notacion de subindices de apuntador\n”); for (i=0; i<=3; i++) printf(“bptr[%d] = %d\n”, i, bptr[i]); printf(“\nUsando notacion de apuntador/desplazamiento\n”); for (despl=0; despl<=3; despl++) printf(“*(bptr+%d) = %d\n”, despl, *(bptr+despl)); getch ( ); return 0; } Arreglos de apuntadores
Los arreglos pueden contener apuntadores. Un uso común para una estructura de datis como ésta, es formar un arreglo de cadenas, conocida como un arreglo de cadenas. Cada entrada en el arreglo es una cadena, pero en C una cadena es en esencia un apuntador a su primer carácter. Por lo que en un arreglo de cadenas cada entrada es de hecho un apuntador al primer carácter de cada una de las cadenas. Veamos la declaración del arreglo de cadenas figuras, que pudiera ser útil para representar un mazo de naipes.

char *figuras[4]={“Corazones”, “Diamantes”, “Treboles”, “Espadas”};

La porción figuras[4] de la declaración indica un arreglo de cuatro elementos. La porción char * de la declaración indica que cada elemento del arreglo figuras es del tipo “apuntador a char”. Los cuatro valores a colocarse en el arreglo son “Corazones”, “Diamantes”, “Treboles” y “Espadas”. Cada una de estas está almacenada en memoria como una cadena de caracteres terminada por NULL, de una longitud de un carácter más largo que en número de caracteres entre comillas. Las cuatro cadenas son de 10, 10, 9 y 8 caracteres de longitud, respectivamente. Aunque pareciera como si estas cadenas están colocadas en el arreglo figuras, de hecho en el arreglo sólo están almacenados los apuntadores.

Cada apuntador señala al primer carácter de su cadena correspondiente. Por lo que, aunque el arreglo figuras es de tamaño fijo, permite el acceso a cadenas de caracteres de cualquier longitud. Esta flexibilidad es un ejemplo de las propiedades poderosas de estructuración de datos en C.

Práctica de apuntadores
Planteamiento: Se pretende simular el barajado y repartición de una baraja americana.

El primer asunto que hay que resolver es la representación de la baraja dentro del programa. Una de las maneras de representarla es con un arreglo bidimensional, donde el renglón sea la figura de la baraja (palo) y la columna represente el número o cara de cada carta.

int baraja[4][13];

de esta manera se tiene un arreglo de 52 elementos (4 * 13) para representar cada una de las cartas de la baraja, el número que almacenará cada carta, que es de tipo entero, representará la posición en la que se encuentran las cartas una vez que se ha barajado.

El siguiente punto será resolver las acciones de barajar y de repartir.

El proceso de barajar debe ser aleatorio, es decir, para cada carta que se vaya revolviendo, o cada posición, debe obtenerse un número aleatorio entre 0 y 4 para la figura y otro entre 0 y 12 para el número de la carta que se está acomodando. Todas las cartas deben iniciarse en 0 antes de barajar, y cuando se haya barajado, cada carta (cada elemento del arreglo) tendrá el número de posición en el que quedó ordenada la carta.

La función para revolver la baraja sería de la siguiente forma:

void barajar(int vBaraja[][13])
{
int carta, figura, cara;
for (carta = 1; carta <= 52; carta++)
{
figura = rand() % 4;
cara = rand() % 13;

/*Se verifica que la carta esté en 0, es decir que no haya
salido */
while (vBaraja[figura][cara] != 0)
{
figura = rand() % 4;
cara = rand() % 13;
}
/* al elemento del arreglo se le asigna su orden correspondiente */
vBaraja[figura][cara] = carta;
}
}


Una vez que se ha barajado, se requiere interpretar el proceso de repartición, en este caso, la repartición se hará a “un solo jugador”, en el orden en el que quedaron las cartas después de barajar. La manera como se hará la repartición consiste en un ciclo que irá de 1 a 52 (para cada carta), y dentro un doble ciclo para recorrer el arreglo en ambos sentidos, hasta localizar el elemento correspondiente a la carta que sigue en el orden que quedaron después de barajar. Si el elemento es el correspondiente a la posición, entonces se imprimirá.

void repartir(const int vBaraja[][13], const char *vCara[],
const char *vFigura[])
{
int carta, figura, cara;
for (carta = 1; carta <= 52; carta++)
for (figura = 0; figura<=3; figura ++)
for (cara = 0; cara <=12; cara ++)
if (vBaraja[figura][cara] == carta)
printf("%7s de %-10s%c", vCara[cara], vFigura[figura],
carta % 2 == 0 ? '\n':'\t');
}

La función que reparte no es del todo óptima, puesto que ningún ciclo termina cuando encuentra la carta que sigue, sino que de cualquier manera recorre la baraja en todos sus renglones (figuras) y todas sus columnas (números).

Se utilizan también dos arreglos auxiliares para darle nombre a los palos de la baraja y a los números para poder imprimirlos de manera presentable.

El programa completo se muestra a continuación:

/* Simulacion de barajar y repartir una baraja */
#include (stdio.h)
#include (stdlib.h)
#include (time.h)
#include (conio.h)

void barajar(int [][13]);
void repartir(const int [][13], const char *[], const char *[]);

main( )
{
char *figuras[4]={"Corazones","Diamantes","Treboles","Espadas"};
char *caras[13]={"As","Dos","Tres","Cuatro","Cinco","Seis","Siete",
"Ocho","Nueve","Diez","Jota","Reina","Rey"};
int baraja[4][13] = {0};
clrscr();

srand(time(NULL)); /*generación de semilla para números aleatorios*/
barajar(baraja);
repartir(baraja, caras, figuras);
return(0);
}

void barajar(int vBaraja[][13])
{
int carta, figura, cara;
for (carta = 1; carta <= 52; carta++)
{
figura = rand() % 4;
cara = rand() % 13;
while (vBaraja[figura][cara] != 0)
{
figura = rand() % 4;
cara = rand() % 13;
}
vBaraja[figura][cara] = carta;
}
}


void repartir(const int vBaraja[][13], const char *vCara[],
const char *vFigura[])
{
int carta, figura, cara;
for (carta = 1; carta <= 52; carta++)
for (figura = 0; figura<=3; figura ++)
for (cara = 0; cara <=12; cara ++)
if (vBaraja[figura][cara] == carta)
printf("%7s de %-10s%c", vCara[cara], vFigura[figura],
carta % 2 == 0 ? '\n':'\t');
}

Asignación dinámica de arreglos
Cuando se define cada variable en un programa, se le asigna suficiente especio de almacenamiento a partir de un conjunto de ubicaciones de memoria en la computadora, disponibles al compilador. Una vez que se reservan ubicaciones específicas para una variable en memoria, estas ubicaciones quedan fijas por el tiempo que dure esa variable, ya sea que se emplee o no. Por ejemplo, si una función requiere espacio de almacenamiento para un arreglo de 500 números enteros, el espacio reservado para el arreglo se asigna y se fija a partir del punto de definición del arreglo. Si la aplicación requiere menos de 500 números enteros, el espacio de almacenamiento sin usar no se devolverá al sistema hasta que el arreglo deje de existir. En cambio, si la aplicación requiere más de 500 números enteros, el tamaño del arreglo en número entero debe aumentarse y la función que define el arreglo debe compilarse de nuevo.

Una alternativa a esta asignación fija o estática de las ubicaciones de almacenamiento de memoria es la asignación dinámica de memoria. En un esquema de asignación dinámica, la cantidad de espacio de almacenamiento que debe asignarse se determina y ajusta al ejecutar el programa, en vez de fijarlo al momento de compilar.

La asignación dinámica de memoria es sumamente útil para manejar listas, pues permite ampliarlas cuando se agregan elementos o reducirlas si se eliminan. Por ejemplo, al elaborar una lista de calificaciones es posible que no se sepa el número exacto de calificaciones necesarias, en lugar de crear un arreglo fijo para almacenar las calificaciones, resulta mucho más útil tener un mecanismo mediante el cual el arreglo se pueda incrementar o reducir según sea necesario. Dos operadores en C++, new y delete, ofrecen esta capacidad, y se explican a continuación:

new.- reserva el número de bytes solicitado por la declaración, devuelve la dirección de la primer ubicación reservada, o NULL si no hay suficiente memoria disponible.

delete.- desocupa un bloque de bytes previamente reservado, la dirección de la primer ubicación reservada se transmite como un argumento a la función.

La solicitud explícita de espacio de almacenamiento dinámico para variables o arreglos a escala se hacen como parte de una instrucción declaratoria a una de asignación. Por ejemplo, la instrucción declaratoria *num=new int; reserva espacio suficiente para contener un número entero y coloca la dirección de dicho espacio en el apuntador num. Esta misma asignación dinámica se puede lograr al declarar primero el apuntador que emplea la instrucción declaratoria int *num; y subsecuentemente asignar al apuntador una dirección con la instrucción num=new int. En cualquier caso el espacio de almacenamiento asignado viene del espacio libre en la memoria de la computadora.

Un método similar y más útil es la asignación dinámica de arreglos, por ejemplo, la declaración:

Int *calif=new int[200];

Reserva suficiente espacio de almacenamiento para 200 números enteros y coloca la dirección del primer número entero en el apuntador calif. En este ejemplo se usó la constante 200, pero pudo emplearse una dimensión variable, como se describe en el siguiente ejemplo:

/* Utilizando memoria dinamica con new y delete*/
#include (stdio.h)
#include (conio.h)

main ( )
{
int numcalif, i,*calif;
printf ("Introduce el numero de calificaciones a procesar:");
scanf ("%d",&numcalif);
calif=new int[numcalif];
for (i=0; i(numcalif;i++)
{
printf ("Introduce la calificacion [%d]:",i+1);
scanf ("%d",&calif[i]);
}
printf ("\nSe creo un arreglo para %d numeros enteros\n",numcalif);
printf ("Los valores almacenados son:\n");
for (i=0; i(numcalif;i++)
printf ("%d\n",calif[i]);

getch ();
delete calif;
return 0;
}

8.1. Estructuras

Definición de estructuras
Las estructuras son colecciones de variables relacionadas, a veces denominadas agregados, bajo un mismo nombre. Las estructuras pueden contener variables de muchos tipos diferentes de datos, a diferencia de los arreglod, que contienen únicamente elementos de un mismo tipo de datos. Generalmente las estructuras se utilizan para definir registros a lamacenarse en archivos. Los apuntadores y las estructuras facilitan la formación de estructuras de datos de mayor complejidad, como son listas enlazadas, colas de espera, pilas y árboles.

Las estructuras son tipos de datos derivados, estan construidas utilizando objetos de otros tipos. Considere la siguiente definiciónde estructura:

struct cartas
{
char *cara;
char *figura;
};

La palabra reservada struct presenta la definición de estructura. El identificador cartas es el rótulo de la estructura. El rótulo de la estructura da nombre a la definición de la misma, y se utiliza con la palabra reservada struct para declarar variables del tipo estructura. En este ejemplo, el tipo estructura es struct cartas. Las variables declaradas dentro de las llaves de la definición de estructura son los miembros de la estructura. Los miembros de la misma estructura deben tener nombres únicos, pero dos estructuras diferentes pueden contener miembros con el mimo nombre sin entrar en conflicto. Cada definición de estructura debe terminar con un punto y coma.

La definición de struct cartas, contiene dos miembros del tipo char * -cara y figura-. Los miembros de la estructura pueden ser variables de los tipos de datos básicos (es decir, int, float, etc.), o agregados, como son los arreglos y otras estructuras. Como ya vimos cada elemento de un arreglo debe ser del mismo tipo. Los miembros de una estructura sin embargo, pueden ser de una variedad de tipos de datos, por ejemplo, un struct empleado pudiera contener miembros de cadenas de caracteres correspondientes a los nombres y apellidos, un miembro int, para la edad del empleado, un miembro char, que contenga 'M' o bien 'F' para el sexo del emplead, un miembro float para el salario horario del empleado, y así sucesivamente.