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.

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", &*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.

7.2. Definición e inicialización de arreglos

pendiente

7.1. Arreglos unidimensionales (listas) y multidimensionales (tablas)


Ejemplos de uso de arreglos
A continuación se muestra un programa sencillo donde se declara un arreglo de diez elementos, se recorre el arreglo para iniciar todos los elementos en 0, y posteriormente se imprimen los valores de los elementos con su subíndice correspondiente.


#include (stdio.h)
#include (conio.h)
main( )
{
int n[10], i; /*declaración del arreglo n y la variable entera i */

for (i=0; i<=9; i++) /* ciclo de inicialización del arreglo */
n[i]=0;
printf("%8s%13s\n", "Elemento", "Valor");

for (i=0; i<=9; i++) /* ciclo para mostrar los valores del arreglo */
printf("%8d%13d\n", i, n[i]);
getch ( );
return 0;
}

Los elementos de un arreglo también pueden ser inicializados en la misma declaración del arreglo, siguiendo al nombre del arreglo con un signo igual y una lista de valores separados por coma y encerrados entre llaves. Si el número de valores indicado es menor al número de elementos en el arreglo, los elementos restantes se inicializan automáticamente en cero. Sin embargo si el número de valores inicializadores es mayor al número de elementos en el arreglo se genera un error.

El siguiente ejemplo muestra una inicialización en la declaración:

/* Inicializacion de un arreglo dentro de la declaracion */
#include (stdio.h)
#include (conio.h)
main( )
{
/*declaracion e inicializacion del arreglo n */
int n[10] = {32,27,64,18,95,14,90,70,60,37};
int i;

printf("%8s%13s\n", "Elemento", "Valor");
for (i=0; i<=9; i++) /* ciclo para mostrar los valores del arreglo */
printf("%8d%13d\n", i, n[i]);
getch ( );
return 0;
}

Si de una declaración con una lista inicializadora se omite el tamaño del arreglo, el número de elementos en el arreglo será el número de elementos incluidos en la lista inicializadora. Por ejemplo:

int n[]={2,4,6,8};

se reserva memoria para 4 elementos del arreglo n (del elemento 0 al 3).

En el ejemplo siguiente la línea:
#define TAMANO 12
define una constante simbólica TAMANO cuyo valor es 12. Una constante simbólica es un valor que se reemplaza con texto de reemplazo en el preprocesador C, antes de que el programa sea compilado. Cuando el programa es preprocesado, todas las instancias de la constante simbólica TAMANO serán remplazadas por el texto de reemplazo 12. Esa es la función de la directiva del preprocesador #define.

/* Calcular la suma de los elementos de un arreglo */
#include (stdio.h)
#include (conio.h)
#define TAMANO 12
main( )
{
int a[TAMANO] = {10,8,7,52,42,27,18,95,14,70,60,37}, i, total=0;
for (i=0; i<=TAMANO -1; i++) /*ciclo para acumulación de valores*/
total+=a[i];
printf("La suma de los elementos del arreglo es %d\n", total);
getch ( );
return 0;
}

En el siguiente ejemplo se utilizan dos arreglos para la determinación de frecuencias de respuesta en una encuesta. También se utiliza la directiva del preprocesador #define.

/* Programa de frecuencias */
#include (stdio.h)
#include (conio.h)
#define NUMERO_RESPUESTAS 40
#define NUMERO_FRECUENCIAS 11

main()
{
int respuesta, numero;
int respuestas[NUMERO_RESPUESTAS] = {1,2,5,6,3,4,7,8,10,2,5,
8,10,9,1,4,5,7,5,1,4,5,8,7,8,2,1,8,10,10,10,9,8,5,6,7,5,6,5,6};
int frecuencia[NUMERO_FRECUENCIAS]={0}; /* Inicialización en cero */
/* Con el siguiente ciclo se recorre el arreglo de respuestas y se
incrementa el arreglo de frecuencias en su elemento correspondiente */
for (respuesta=0; respuesta <= NUMERO_RESPUESTAS -1; respuesta++)
++frecuencia[respuestas[respuesta]];
printf("%8s%13s\n", "Número", "Frecuencia");

/* Ciclo de impresión de frecuencias */
for (numero=1; numero<=10; numero++)
printf("%8d%13d\n", numero, frecuencia[numero]);
getch ( );
return 0;
}

En el siguiente ejemplo se utilizan arreglos para la impresión de un histograma.

/* Impresión de histogramas */
#include (stdio.h)
#include (conio.h)
#define TAMANO 10
main( )
{
int n[TAMANO] = {10,8,7,18,14,6,10,15,4,2};
int i, j;
printf("%15s%15s%s%-20s\n","Elemento","Valor"," ","Histograma");
/*Ciclo para la impresión de cada valor */
for (i=0; i<=TAMANO -1; i++)
{
printf("%15d%15d%s",i, n[i]," ");
/* ciclo para la impresión de cada asterisco (de 1 hasta el
valor de n[i], es decir el valor del elemento */
for (j=1; j<=n[i]; j++)
printf("%c",'*');
printf("\n");
}
getch ( );
return 0;
}

En C una cadena de caracteres es un arreglo de caracteres individuales. Los arreglos de caracteres tienen varias características únicas. Un arreglo de caracteres puede ser inicializado utilizando una literal de cadena. Por ejemplo:

char string1[] = “primero”;

inicializa los elementos de la cadena o arreglo de caracteres string1 a los caracteres individuales de la cadena “primero”. El tamaño del arreglo queda determinado por el compilador, basado en la longitud de la cadena. La cadena “primero” contiene 7 caracteres más un caracter especial de terminación de cadena, conocido como caracter nulo. Entonces el arreglo string1 contiene 8 elementos. La representación del caracter nulo es ‘\0’. En C todas las cadenas terminan con este caracter. Un arreglo de caracteres que represente una cadena, debe declararse siempre lo suficientemente grande para contener el número de caracteres de la cadena incluyendo el caracter nulo de terminación.

La declaración anterior es equivalente a:

char string1[]={‘p’,’r’,’i’,’m’,’e’,’r’,’o’,’\0’};

Dado que la cadena es una arreglo se puede referenciar un caracter en particular utilizando el subíndice. Por ejemplo string1[0] es ‘p’, y string1[4] es la ‘m’.

Se puede leer desde teclado una cadena de caracteres con scanf de la siguiente forma:

char string2[20]; /* declaración del arreglo de 19 caracteres y el nulo */
scanf(“%s”, string2); /*lectura de teclado de una cadena y asignación a string2 */

en este caso no se utiliza el operador de dirección &, puesto que un arreglo es ya de por sí la dirección en memoria del inicio del arreglo.

La función scanf lee caracteres de teclado hasta que se encuentra el primer caracter de espacio en blanco sin importar el tamaño.

Es posible imprimir una cadena de caracteres con printf de la siguiente forma:

printf(“%s”, string2);

En el siguiente ejemplo se analiza el manejo de arreglos de tipo char.

/* Manejo de arreglos de tipo char */
#include (stdio.h)
#include (conio.h)
main( )
{
char string1[20], string2[]="Cadena de Caracteres";
int i;

printf("Introduzca un string: ");
scanf("%s", string1);
printf("El string1 es: %s\n",string1);
printf("El string2 es: %s\n",string2);
printf("El string1 con espacios es: \n");
/* impresion con un espacio despues de cada caracter*/
for (i=0; string1[i] != '\0'; i++)

if (string1[i]>='a' && string1[i]<='z')
/* si es una letra minuscula hacerla mayuscula */
printf("%c ", string1[i]-32);
else
/* si no es letra minuscula imprimir tal cual */
printf("%c ", string1[i]);
printf("\n");
getch ( );
return 0;
}

Paso de arreglos como parámetros a funciones.
Para pasar como argumento un arreglo a una función, se especifica el nombre del arreglo, sin corchetes, por ejemplo, para pasar a una función imprimePresion el arreglo PresionDiaria se realiza de la siguiente manera:

imprimePresion(PresionDiaria, 7);

en este ejemplo se pasa también como parámetro la longitud del arreglo, esto se hace normalmente para que las funciones puedan procesar todos los elementos del arreglo sin tratar de acceder a elementos inexistentes.

C pasa de forma automática los arreglos a las funciones utilizando simulación de llamadas por referencia, es decir, las funciones llamadas pueden modificar los valores de los elementos en los arreglos originales de los llamadores. Esto se debe a que la función llamada no realiza una copia como lo hace con las variables normales, sino que recibe la dirección de memoria del inicio del arreglo, por tanto, donde se hacen las posibles asignaciones dentro de las funciones es en los espacios originales que ocupa el arreglo en memoria.

Para que una función reciba un arreglo a través de una llamada de función, la lista de parámetros de la función debe especificar que se va a recibir un arreglo. Por ejemplo, el encabezado de función para la función modificarArreglo puede ser escrito como:

void modificarArreglo(int b[], int size)

A continuación se presenta un ejemplo utilizando llamadas a funciones con paso de arreglos como parámetros.

/* Pasando arreglos y elementos individuales de arreglo a funciones */
#include (stdio.h)
#include (conio.h)
#define SIZE 5
void modificarArreglo( int [], int); /*prototipos de funcion*/
void modificarElemento(int);
main( )
{
int a[SIZE] = {4,3,2,1,0};
int i;
printf("Efecto de pasar un arreglo a una funcion\n");
printf("Los valores originales son:\n");
for (i=0; i<=SIZE-1; i++)
printf("%3d", a[i]);
printf("\n");
modificarArreglo(a, SIZE);
printf("Los valores modificados son:\n");
for (i=0; i<=SIZE-1; i++)
printf("%3d", a[i]);
printf("\n\nEfecto de pasar un solo elemento por valor\n");
printf("El valor de a[3] es: %d\n", a[3]);
modificarElemento(a[3]);
printf("El valor de a[3] es: %d\n", a[3]);
getch ( );
return 0;
}
/* función que modifica los valores del arreglo */
void modificarArreglo(int c[], int size)
{
int j;
for (j=0; j<=size-1; j ++)
c[j] *= 2;
}
/* función que modifica un entero, en este caso un elemento del arreglo */
void modificarElemento(int elemento)
{
printf("El valor en modificarElemento es: %d\n", elemento *= 2);
}

Existen situaciones en las que no se desea que el arreglo original sea modificado por una función. En estos casos es necesario utilizar el calificador especial const, calificando al arreglo tanto en el prototipo como en el encabezado de la función, cuando se hace esto el arreglo se recibe como constante, y cualquier modificación que se intente hacer sobre él, causará un error. El siguiente programa es un ejemplo de ello:

/* Demostracion del calificador de tipo const */
#include (stdio.h)
#include (conio.h)
void tratarDeModificarArreglo(const int []);
main( )
{
int a[]= {10,20,30};
tratarDeModificarArreglo(a);
printf("%d %d %d\n", a[0], a[1], a[2]);
return 0;
}

void tratarDeModificarArreglo(const int b[])
{
/*Generan error: No se puede modificar un objeto constante */
b[0] /= 2;
b[1] /= 2;
b[2] /= 2;
}


Ordenamiento de arreglos.
La mayoría de las aplicaciones de cómputo requieren tener datos ordenados En el siguiente ejemplo se realiza uno de los métodos de ordenación más simples conocido como método de burbuja u ordenación por hundimiento. La técnica consiste en llevar a cabo varias pasadas a través del arreglo, en cada pasada se comparan pares sucesivos de elementos. Si un par está en orden creciente (o son valores idénticos), los valores se quedan tal como están. Si un par aparece en orden decreciente sus valores se intercambian de lugar, utilizando una variable que almacenará temporalmente el valor del primer elemento, éste tomará el valor del segundo elemento del par, y por último el segundo tomará el valor temporal que correspondía al primer elemento.

/* Ordenamiento de arreglo por método de burbuja */
#include (stdio.h)
#include (conio.h)
#define SIZE 10
main( )
{
int a[SIZE]= {12,35,6,48,8,27,32,87,52,75};
int i, temporal, ciclo;
printf("Los valores en el orden original son: \n");
for (i=0; i<= SIZE -1; i++)
printf("%4d", a[i]);
/* Ordenamiento */
for (ciclo=1; ciclo<=SIZE -1; ciclo++)
for (i=0; i<=SIZE-1-ciclo; i++)
/*Intercambio de valores en caso de no estar en orden */
if (a[i] > a[i+1])
{
temporal = a[i];
a[i] = a[i+1];
a[i+1]= temporal;
}
/*Impresion de valores ordenados */ printf("\nLos valores ordenados son: \n");
for (i=0; i<= SIZE -1; i++)
printf("%4d", a[i]);
getch ( );
return 0;
}

Existen otros métodos de ordenamiento más complejos en cuanto a su implementación, pero más eficientes en el número total de comparaciones que realizan, ocupando menor tiempo (en términos computacionales) en ordenar el arreglo.

Búsqueda de arreglos
Existen también varios métodos para buscar un elemento en un arreglo, sin embargo, el buen funcionamiento de algunos depende de que los elementos del arreglo estén ordenados de alguna manera. El método para buscar un elemento en un arreglo que no se encuentre ordenado (también funciona para arreglos ordenados), es la búsqueda lineal, que consiste en comparar cada elemento del arreglo (comenzando por el primero) con el valor buscado, y cuando se encuentre uno igual, señalar el índice del elemento correspondiente.

El siguiente ejemplo muestra una función que realiza la búsqueda lineal de un elemento.

/* Busqueda lineal en un arreglo */
#include (stdio.h)
#include (conio.h)
#define SIZE 10
int BusquedaLineal(int [], int,int);
main()
{
int a[SIZE]= {12,35,6,48,8,27,32,87,52,75};
int valorBuscado, indiceValor, i;
clrscr();
printf("Introduzca el valor a buscar en el arreglo: ");
scanf("%d", &valorBuscado);
indiceValor= BusquedaLineal(a,valorBuscado, SIZE);
if (indiceValor != -1)
printf("%d corresponde al elemento %d", valorBuscado, indiceValor);
else
printf("No se encontro %d en el arreglo",valorBuscado);
return 0;
}

int BusquedaLineal(int arreglo[], int valor, int size)
{
int j;
/*Compara todos los elementos */
for (j=0; j<=size-1;j++)
if (arreglo[j]==valor)
return j; /* Valor regresado en cuanto lo encuentra*/
return -1; /* regresa -1 cuando no encuentra el valor */
}

Para arreglos ordenados es muy utilizado el método de búsqueda binaria, que consiste en irse directamente al elemento de central (posición de en medio) del arreglo, si el valor buscado es menor la primera mitad del arreglo se desecha, y si es mayor la se desecha la segunda. Se hace lo mismo con la parte que quedo del arreglo buscando el punto medio, y así sucesivamente. El mayor número de comparaciones posibles es igual a la mitad del tamaño del arreglo.

/* Busqueda binaria en un arreglo */
#include (stdio.h)
#include (conio.h)
#define SIZE 10
void OrdenaArreglo(int[] , int );
int BusquedaBinaria(int [], int , int );
main( )
{
int b[SIZE]= {12,35,6,48,8,27,32,87,52,75};
int valorBuscado, elemento, i;
clrscr();
OrdenaArreglo (b, SIZE);
printf("Los valores del arreglo son: \n");
for (i=0; i<= SIZE -1; i++)
printf("%4d", b[i]);
printf("\n\nIntroduzca el valor a buscar en el arreglo: ");
scanf("%d", &valorBuscado);
elemento = BusquedaBinaria(b, valorBuscado, SIZE);
if (elemento != -1)
printf("%d es el elemento %d", valorBuscado, elemento);
else
printf("No se encontro %d en el arreglo", valorBuscado);
getch ( );
return 0;
}
/* Ordenamiento */
void OrdenaArreglo(int a[], int size)
{
int ciclo, temporal,i;
for (ciclo=1; ciclo<=size -1; ciclo++)
for (i=0; i<=size-1-ciclo; i++)
if (a[i] > a[i+1])
{
temporal = a[i];
a[i] = a[i+1];
a[i+1]= temporal;
}
}
/*Busqueda binaria */
int BusquedaBinaria(int arreglo[], int valor, int size)
{
int medio, primero=0, ultimo=size -1;
while (primero <= ultimo)
{
medio= (primero + ultimo) / 2;
if (valor == arreglo [medio])
return medio;
else
if (valor < arreglo [medio])
ultimo = medio - 1;
else
primero = medio + 1;
}
return -1;
}

Arreglos bidimensionales
En algunos lenguajes de programación, y es el caso de C, se permiten arreglos de varios subíndices, es decir, arreglos multidimensionales. Generalmente estos arreglos se utilizan para representar tablas de valores con renglones y columnas, donde el primer subíndice representa el renglón y el segundo la columna. Para referenciar cualquier elemento del arreglo se deben indicar los dos subíndices.

Un arreglo de múltiple subíndice puede ser inicializado en su declaración en forma similar a un arreglo de un subíndice. Por ejemplo, un arreglo de doble subíndice b[2][2] puede ser inicializado con

int b[2][2] = {{ 1, 2}, {3, 4}};

Los valores se agrupan por renglones entre llaves. Por lo tanto, 1 y 2 inicializan b[0][0] y b[0][1], 3 y 4 inicializan b[1][0] y b[1][1]. Si para un renglón dado no se proporcionan suficientes inicializadores, los elementos restantes de dicho renglón se inicializarán a 0. Por ejemplo:

int b[2][2] = {{ 1}, {3, 4}};

inicializaría b[0][0] en 1 y b[0][1] en 0, b[1][0] en 3 y b[1][1] en 4.

En la tabla siguiente se muestra la representación gráfica de un arreglo con tres renglones y cuatro columnas.

                     Columna0   Columna1  Columna2  Columna 3
Renglón0    a[0][0]        a[0][1]       a[0][2]       a[0][3]
Renglón1     a[1][0]        a[1][1]       a[1][2]        a[1][3]
Renglón2    a[2][0]        a[2][1]       a[2][2]        a[2][3]

El recorrido de un arreglo de dos dimensiones para impresión o para cualquier procesamiento deberá hacerse con dos ciclos for anidados, de tal modo que el más exterior recorra todos los renglones, y para cada renglón (el ciclo for interior) recorra todas las columnas del arreglo, de tal forma que cada elemento sea referenciado en los ciclos anidados. A continuación se muestra un ejemplo simple para impresión de un arreglo bidimensional.

/* Inicialización de arreglos bidimensionales */
#include (stdio.h)
#include (conio.h)
void imprimeArreglo(int [][3]);
main( )
{
int arreglo1[2][3] = { {1,2,3}, {4,5,6}},
arreglo2[2][3] = { 1,2,3,4,5},
arreglo3[2][3] = { {1,2}, {4}};
clrscr();
printf("Los valores en arreglo1 por renglón son: \n");
imprimeArreglo(arreglo1);
printf("Los valores en arreglo2 por renglón son: \n");
imprimeArreglo(arreglo2);
printf("Los valores en arreglo3 por renglón son: \n");
imprimeArreglo(arreglo3);
getch ( );
return 0;
}
void imprimeArreglo(int arreglo[][3])
{
int i,j;
for (i=0; i<=1; i++)
{
for (j=0; j<=2; j++)
printf("%5d", arreglo[i][j]);
printf("\n");
}
}

PRACTICA DE ARREGLOS.
En un grupo existen cinco alumnos, los cuales presentaron tres exámenes cada uno obteniendo las siguientes calificaciones:

CALIFICACIONES
ALUMNOS  Examen0  Examen1  Examen2
Alumno0      77               74              84
Alumno1      96               85              76
Alumno2      70               69              88
Alumno3      58               86              90
Alumno4      75               91              72

Se desea obtener la tabla de calificaciones (semejante a la mostrada), la calificación más baja, la calificación más alta y el promedio por cada alumno.

El problema se resuelve de la siguiente forma:

Como se conoce el número de alumnos y el número de calificaciones se definirán dos constantes simbólicas ALUMNOS y EXAMENES de 5 y 3 respectivamente.

/* Ejemplo de arreglos bidimensionales */
#include (stdio.h)
#include (conio.h)
#define ALUMNOS 5
#define EXAMENES 3

Debido a que se utilizarán funciones para imprimir los valores, encontrar la calificación mínima, la máxima y el promedio por alumno, es necesario comenzar a trabajar con dichas funciones. La función mínima debe encontrar la calificación más baja entre todos los elementos del arreglo, por tanto será necesario que reciba como parámetros el mismo arreglo, el número de filas (o alumnos) y el número de columnas (calificaciones), esto debido a que la función no debe ignorar elementos, ni tampoco tratar de acceder a elementos que no existen en el arreglo. La manera de encontrar la calificación mínima es suponer una mínima seguramente mayor a todas, como 100 (para que sea sustituida rápidamente por el primer elemento del arreglo). Todas los elementos se compararán con la calificación mínima registrada hasta el momento, y de ser menor, este valor será asignado a la variable minimo, de tal forma que al término del recorrido de todo el arreglo, la variable minimo contenga el valor de la calificación más baja.

/*Calificación mínima */
int minima (int calificaciones[][EXAMENES], int alum, int prueba)
{
int i, j, minimo = 100;
for (i=0;i<=alum-1; i++)
for (j=0; j<=prueba-1; j++)
if (calificaciones[i][j] < minimo)
minimo = calificaciones[i][j];
return minimo;
}

De la misma forma que la función minima, deberá plantearse la función maxima, que encontrará la calificación más alta. En este caso la variable maximo es iniciada en 0 para ser sustituida por cualquier elemento mayor. Posteriormente se recorre el arreglo y se va guardando el valor mayor en la variable maximo.

/* Calificación máxima */
int maxima (int calificaciones[][EXAMENES], int alum, int prueba)
{
int i, j, maximo = 0;
for (i=0;i<=alum-1; i++)
for (j=0; j<=prueba-1; j++)
if (calificaciones[i][j] > maximo)
maximo = calificaciones[i][j];
return maximo;
}

La función para imprimir el arreglo, simplemente hace un recorrido en un primer ciclo for, de todos los renglones (alumnos) en el arreglo, y en un for anidado recorre cada una de las columnas de cada fila (calificaciones), imprimiendo renglón por renglón.

/* imprime el arreglo */
void imprimeArreglo(int calificaciones[][EXAMENES], int estudiante, int pruebas)
{
int i,j;
printf("\n%-20s%-30s\n"," ", "CALIFICACIONES");
printf("%-20s%6s%6s%6s", "ALUMNO","[0]","[1]","[2]","[3]");
for (i=0; i<= estudiante-1; i++)
{
printf("\nAlumno %d ", i);
for(j=0; j<=pruebas-1; j++)
printf("%6d", calificaciones[i][j]);
}
}

Para determinar el promedio por alumno se hará otra función, que devuelve un tipo float (el promedio) y que recibe únicamente dos parámetros, un arreglo de una sola dimensión y el tamaño de este arreglo (el número de calificaciones). El arreglo sencillo corresponde a las tres calificaciones del alumno en cuestión. La función acumula mediante un ciclo todos los valores para posteriormente dividirlo entre el total de calificaciones (la variable pruebas).

/*Promedio */
float promedio(int calif[], int pruebas)
{
int i, total=0;
for (i=0; i<=pruebas-1; i++)
total += calif[i];
return total/pruebas;
}

En la función main (programa principal) se declararán dos variables enteras, una representando el arreglo de calificaciones, donde tendremos 5 renglones (de alumnos) y 3 columnas (de calificaciones), que se inicializará en la misma declaración. La otra variable entera ialumno, servirá para recorrer el arreglo en todas sus filas, es decir, alumno por alumno. Una vez hecho esto deben mandar llamarse las funciones creadas de la forma correcta para obtener los resultados deseados.

main()
{
int ialumno, alumCalif[ALUMNOS][EXAMENES] = {{77,74,84},{96,85,96},
70,69,88},{58,86,90},{75,91,72}};
clrscr();
printf("El arreglo es: \n");
imprimeArreglo(alumCalif, ALUMNOS, EXAMENES);
printf("\n\nCalificación más baja: %d\n", minima(alumCalif,(ALUMNOS,EXAMENES));
printf("Calificación más alta: %d\n", maxima(alumCalif,ALUMNOS,EXAMENES));
for (ialumno=0; ialumno<=ALUMNOS - 1; ialumno ++)
printf("\nEl promedio para el alumno %d es: %.2f\n", ialumno,
promedio(alumCalif[ialumno], EXAMENES));
getch ( );
return 0;
}

No hay que olvidar que las definiciones de las funciones deben ubicarse después de main y que antes de main debe existir un prototipo. De este modo el programa completo queda de la siguiente manera:

/* Ejemplo de arreglos bidimensionales */
#include (stdio.h)
#include (conio.h)
#define ALUMNOS 5
#define EXAMENES 3
int minima(int [][EXAMENES], int, int);
int maxima(int [][EXAMENES], int, int);
float promedio(int [], int);
void imprimeArreglo( int [][EXAMENES], int, int);

main( )
{
int ialumno,
alumCalif[ALUMNOS][EXAMENES] = {{77,74,84},{96,85,96},
{70,69,88},{58,86,90},{75,91,72}};
clrscr();
printf("El arreglo es: \n");
imprimeArreglo(alumCalif, ALUMNOS, EXAMENES);
printf("\n\nCalificación más baja: %d\n", minima(alumCalif, ALUMNOS,EXAMENES));
printf("Calificación más alta: %d\n", maxima(alumCalif, ALUMNOS,EXAMENES));
for (ialumno=0; ialumno<=ALUMNOS - 1; ialumno ++)
printf("\nEl promedio para el alumno %d es: %.2f\n", ialumno,
promedio(alumCalif[ialumno], EXAMENES));
return 0;
}

/*imprime el arreglo */
void imprimeArreglo(int calificaciones[][EXAMENES], int estudiante, int pruebas)
{
int i,j;
printf("\n%-20s%-30s\n"," ", "CALIFICACIONES");
printf("%-20s%6s%6s%6s", "ALUMNO","[0]","[1]","[2]","[3]");
for (i=0; i<= estudiante-1; i++)
{
printf("\nAlumno %d ", i);
for(j=0; j<=pruebas-1; j++)
printf("%6d", calificaciones[i][j]);
}
}

/*Calificación mínima */
int minima (int calificaciones[][EXAMENES], int alum, int prueba)
{
int i, j, minimo = 100;
for (i=0;i<=alum-1; i++)
for (j=0; j<=prueba-1; j++)
if (calificaciones[i][j] < minimo)
minimo = calificaciones[i][j];
return minimo;
}

/*Calificación máxima */
int maxima (int calificaciones[][EXAMENES], int alum, int prueba)
{
int i, j, maximo = 0;
for (i=0;i<=alum-1; i++)
for (j=0; j<=prueba-1; j++)
if (calificaciones[i][j] > maximo)
maximo = calificaciones[i][j];
return maximo;
}

/*Promedio */
float promedio(int calif[], int pruebas)
{
int i, total=0;
for (i=0; i<=pruebas-1; i++)
total += calif[i];
return total/pruebas;
}