El lenguaje C
Indice:
Copyright:
No tiene Copyright, porque el trabajo que he hecho ha sido para beneficio propio, y espero que le sirva a otros tanto como me ha servido a mí, así que si quieres usar, modificar copiar el documento no tienes porqué pedirme permiso, aunque siempre es de agradecer conocer si le ha servido de algo esto a alguien.
1.1. Estructura de los programas
La estructura de un programa sencillo en C es la siguiente:
declaración de importaciones
definición de constantes
definición de tipos
void main (void)
{
declaración de variables
instrucciones ejecutables
}
La declaración de importaciones se utiliza para comunicar al compilador qué acciones y funciones de las mencionadas en el programa no se encuentran implementados en este. Estos procedimientos externos al programa se hallan disponibles en ciertas estructuras llamadas módulos que tienen asociado un fichero con extensión.h que contiene la lista de procedimientos importables. Para incluirlos es suficiente escribir una instrucción del tipo
#include <nombre_módulo.h>
Ejemplos de módulos empleados con frecuencia son stdio (que contiene los procedimientos de entrada salida) y math (que contiene funciones matemáticas).
Las definiciones de constantes asignan a un identificador una cadena de caracteres. Un proceso previo a la compilación substituirá el identificador por la cadena de caracteres cada vez que lo encuentre. Su formato es
#define identificador cadena
y al igual que con la instrucción #include, no hace falta poner el terminador; al final de la instrucción.
La declaración de tipos permite definir tipos de datos que serán utilizados en todo el programa.
Ejemplo 1.1.a C no posee ninguna implementación primitiva del tipo booleano. Entonces, para declarar variables de este tipo, hace falta definirlo previamente. La manera de hacerlo es añadir entre las declaraciones de tipos la siguiente:
typedef enum {FALSE=0, TRUE=1} booleano;
Que define el tipo booleano y asigna a sus elementos FALSE y TRUE los valores 0 y 1 respectivamente.
Los identificadores utilizados en C como nombres de variables son cadenas de hasta 32 caracteres alfanuméricos y pueden contener el guión '_' para unir palabras. Además, la cadena ha de comenzar por una letra. Tradicionalmente se utilizan letras minúsculas y se reserva el uso de mayúsculas para nombres de constantes.
La manera de representar las variables en el interior de computador utilizada por C solo prevé la representación de un conjunto finito de datos. Esto no genera problemas con la codificación de caracteres, pero recorta considerablemente el alcance del conjunto de enteros. En este caso, se genera un tamaño máximo de los enteros manejables por el computador, pero dentro del rango definido por este tamaño la aritmética entera es exacta.
Las palabras clave de C para declarar caracteres o enteros son, respectivamente, char e int y los datos definidos mediante estas declaraciones tienen la característica de representarse internamente de tal manera que las operaciones realizadas posteriormente sean muy rápidas. Por otro lado, debido a la existencia de problemas que exigen tratar enteros que el doble de memoria en su representación. Estos datos, que pueden ser definidos mediante la palabra clave long int, comportan sin embargo un tiempo de ejecución mas lento para su aritmética. Se puede resumir las características de los tipos elementales de datos mencionados en la tabla siguiente:
Tipo en C |
Número de bits |
Rango |
char int long int |
8 16 32 |
0 a 255 -32768 a 32767 -2147483648 a 2147483647 |
De esta manera, el código C de un algoritmo cualquiera será
typedef enum {FALSE=0, TRUE=1} booleano;
void main (void)
{ char c;
int larg, num_palabras;
booleano error;
...
}
Los operadores aritméticos en C son
+ |
Suma |
|
* |
Producto |
|
- |
diferencia (o cambio de signo) |
|
/ |
división entera |
|
% |
módulo (resto de la división entera) |
a nivel de bit
& and a nivel de bit
| or a nivel de bit
los booleanos
&& and
| | or
! not
y los operadores relacionales a valores booleanos
== igual
!= diferente
< menor que
> mayor que
<= menor o igual
>= mayor o igual
El almacenamiento de caracteres en el interior del computador se hace en "palabras" de 8 bits. Esto limita el cardinal del conjunto de caracteres a 256. De estos 256, los primeros 128 son los mismos en todas partes y siguen el orden dado por la American Society for Computer Interchange of information (ASCII). Los 128 caracteres restantes varían en función de factores culturales y geográficos. La función que asigna un entero a cada carácter recibe el nombre de código ASCII.
Una característica de la parte estándar del conjunto de caracteres (los 128 primeros) es que contiene las letras mayúsculas, las minúsculas y los dígitos, y que cada uno de estos tres subconjuntos esta ordenado con su orden natural. Así, la siguiente expresión booleana decide si el carácter contenido en la variable c es una letra mayúscula
‘A’<=c && c<=’Z’
Para pasar de un carácter a su código ASCII o viceversa, C dispone de los operadores (int) y (char). El primero situado delante de una variable de tipo char retorna el código ASCII del contenido de la variable. Es recomendable, no obstante, no apoyarse en exceso en el uso de estos operadores. Posiblemente el caso de utilidad más evidente es la transformación de una variable c de tipo char (que contiene un dígito) a una expresión entera que se evalúa en el valor de este dígito mediante
(int)c-(int)’0’
La asignación en C presenta el aspecto
variable = expresión;
donde la expresión ha de tomar un valor del tipo de la variable. Es conveniente dejar un espacio entre el signo = y la expresión. En caso de no hacerlo, C puede generar efectos no deseados para determinados tipos de expresiones, como por ejemplo las que comienzan con -.
En el caso de expresiones compuestas de una constante de tipo carácter, esta ha de aparecer entre comillas simples. Por ejemplo, para asignar el carácter 4 a c_1 y el carácter v a c_2, hace falta escribir
c_1 = ‘4’;
c_2 = ‘v’;
Las asignaciones
c_1 = 4;
c_2 = v;
en cambio, asignarían el entero 4 a c_1 (cosa incorrecta ya que c_1 es de tipo carácter) o el contenido de la variable v a c_2 (que difiere de lo que se quiere hacer).
Para cierto tipo de asignaciones utilizadas frecuentemente, C posee formas de escritura abreviadas. La más extendida es
variable ++;
que incrementa en una unidad el valor de la variable.
Veremos ahora como leer o escribir valores de variables en un caso particular. Por una parte, las variables serán de los tipos elementales en C (int o char) y, por otra, solo consideraremos lecturas y escrituras por el canal llamado estándar, o sea, lecturas desde el teclado y escrituras en pantalla.
Para leer variables desde el teclado, la instrucción pertinente es scanf y su formato es
scanf("formato_1 formato_2... formato_k", &variable_1, &variable_2,... ,&variable_k)
donde formato_1... formato_k representan los tipos de los datos a leer según la correspondencia siguiente:
Formato |
Tipo |
%d %ld %c |
int long int char |
Los valores leídos han de estar separados por espacios o cambios de línea. La parte de la instrucción entre comillas recibe el nombre de control y sirve para avisar a C sobre como ha de interpretar los valores leídos.
Para leer caracteres hay un grupo de instrucciones más sintéticas y ventajosas. Se trata de las instrucciones getch y getche que con el formato
variable = getch ();
variable = getche ();
Leen un carácter desde el teclado (sin necesidad de pulsar la tecla retorno) y la almacenan en variable.
La diferencia entre ambas es que getch no escribe el carácter leído en la pantalla mientras que getche sí lo hace.
Análogamente, para escribir un mensaje en la pantalla, utilizaremos printf. Su formato se asemeja al de la instrucción de lectura con una pequeña diferencia: se pueden intercalar texto e instrucciones de formateado del mensaje a imprimir. A título de ejemplo, para intercalar un cambio de línea en un cierto punto del mensaje, se ha de escribir el comando \n. Así, la instrucción
printf("El valor buscado para %d iteraciones \n es %d", n, suma);
producirá, para los valores (pongamos por caso) 35 para n y 2375 para suma el mensaje
El valor buscado para 35 iteraciones
es 2375
Para imprimir un carácter, se puede utilizar también la instrucción putchar() que imprime su argumento en la pantalla.
Las instrucciones scanf, printf y putchar no forman parte del núcleo de C. Para hacer uso de ellas hace falta importarlas del módulo stdio con la instrucción
#include <stdio.h>
Análogamente, las instrucciones getch y getche se deben importar del módulo conio.
La implementación en C de la lectura y escritura de secuencias se puede hacer de diversas maneras según el soporte físico de la secuencia. De momento solo se considerará secuencias de entrada/salida estándar, o sea, todas las secuencias serán leídas desde el teclado y escritas en pantalla.
Este hecho comporta la limitación de poder trabajar con una única secuencia. Comporta también la particularidad de no declarar las secuencias como variables locales de nuestros programas; en efecto vemos que los programas acceden a los elementos de una secuencia uno por uno y nunca almacenan la secuencia entera.
Para leer secuencias en C se utiliza la instrucción scanf. En todo caso, es conveniente insertar antes de la instrucción que imprima un mensaje por pantalla pidiendo al usuario que introduzca la secuencia y la termine con la marca deseada.
En el caso de secuencias de caracteres es conveniente utilizar en lugar de scanf la instrucción getchar con el formato
variable = getchar();
Esta instrucción lee un carácter desde el teclado y lo almacena en una memoria intermedia (que en C recibe el nombre de stdin) interrumpiendo temporalmente la ejecución del programa. A partir de este momento el computador lee caracteres desde el teclado y los almacena en la memoria intermedia hasta leer un retorno de carro. En este momento la ejecución del programa se reanuda y las sucesivas instrucciones de lectura se realizan desde la memoria intermedia (donde se encuentra almacenada la secuencia de caracteres hasta el retorno de carro). Este protocolo de almacenar provisionalmente en la memoria intermedia para luego proporcionar los caracteres al programa se itera entonces hasta que este ultimo finalice su ejecución, y tiene la ventaja de que permite al usuario borrar caracteres de su secuencia si lo desea (por ejemplo para corregir un error) y estos borrados se efectúan en la memoria intermedia.
Análogamente, para la escritura de secuencias, utilizaremos el printf (o el putchar si se trata de una secuencia de caracteres) para imprimir la cadena de caracteres.
Algunos sistemas operativos pueden redireccionar el canal estándar hacia un fichero. Esto permite escribir una secuencia en un fichero o leer desde un fichero la entrada para un programa (cosa que resulta muy útil a la hora de depurar).
En UNIX o DOS, el formato es
nombre_programa <fichero_entrada> fichero_salida
Que hará que el programa nombre_programa se ejecute leyendo la entrada del fichero fichero_entrada y escribiendo la salida en el fichero fichero_salida.
Los ficheros de entrada han de ser de texto y, si son ficheros de enteros, los elementos han de estar separados por espacios, tabuladores o cambios de línea. Por la misma razón, si deseamos generar una secuencia de enteros y almacenarla en un fichero, hace falta escribir un espacio después del %d en la instrucción scanf porque si no lo hacemos, al editar el fichero resultante veremos una hilera ininterrumpida de dígitos.
Para añadir comentarios a un programa C es suficiente con encerrar el comentario entre los símbolos /* y */. También se pueden realizar anteponiendo // a la línea del comentario.
/* De esta manera el compilador de C ignorará */
// estas líneas al leerlas
2.1. Secuenciación
La manera de secuenciar instrucciones en C consiste en escribirlas consecutivamente (notemos que el punto y coma en C es un terminador de instrucciones y no un separador). Por otra parte, para aumentar la legibilidad de los programas, es hábito escribir instrucciones diferentes en líneas diferentes y respetar el uso habitual de la identación que ya hemos utilizado en los algoritmos.
La estructura de alternación en C se realiza mediante la instrucción if—else—if, con el formato siguiente:
if (condicion_1) {
secuencia_de_instrucciones_1
}
else if (condicion_2) {
secuencia_de_instrucciones_2
}...
else if (condicion_k) {
secuencia_de_instrucciones_k }
Observaciones:
( i ) En caso de tener una sola instrucción en lugar de una secuencia, no hace falta cerrarla entre llaves.
( ii ) Si condicion_k es exactamente la negación de todas las condiciones precedentes, podemos no escribir la parte if (condicion_k), ya que C interpreta el else como lo que significa en inglés, o sea, en caso contrario.
( iii ) Además, si secuencia_de_instrucciones_k es la secuencia vacía, podemos no escribir toda la parte
else if (condicion_k) {
secuencia_de_instrucciones_k
}
Hay un caso para el cual C provee una instrucción alternativa especifica. Cuando en la estructura anterior todas las protecciones afirman la igualdad de una variable determinada con constantes predefinidas, se puede utilizar la instrucción switch que presenta el formato siguiente:
switch (variable) {
case constante_1: {
secuencia_de_instrucciones_1
} break;
case constate_2: {
secuencia_de_instrucciones_2
} break;
...
case constante_k: {
secuencia_de_instrucciones_k
} break;
default: {
secuencia_de_instrucciones_por_defecto
}
}
Al igual que con el if, si la secuencia_de_instrucciones_por_defecto es la secuencia vacía, se pueden omitir el default y la secuencia misma.
En C hay tres maneras de implementar iteraciones. La más básica es la sentencia de iteración while:
while (condición) {
secuencia_de_instrucciones
}
Una variación del while es la instrucción do, que se diferencia de la primera en que verifica la condición después de ejecutar la secuencia_de_instrucciones y, por lo tanto, la ejecuta como mínimo una vez. Su sintaxis es
do {
secuencia_de_instrucciones
} while (condición)
Finalmente, la instrucción
for (variable = valor_inicial; variable<=valor_final; variable = variable + paso) {
secuencia_de_instrucciones
}
donde paso indica el incremento (o decremento) de la variable.
3.1. Tablas o vectores
La declaración de vectores en C se hace mediante el siguiente formato:
tipo nombre_vector [dim]
Donde dim ha de ser un número entero y denota la dimensión o longitud del vector. Así, las instrucciones siguientes:
int datos[10]:
char nombre[12], apellido[12];
Declaran un vector de enteros de 10 elementos y dos vectores de caracteres de 12 elementos cada uno.
Una vez declarado un vector de n elementos, se puede leer y escribir en sus componentes teniendo en cuenta que éstas se numeran de la 0 a la n-1. Por ejemplo, para referenciar la primera letra de apellido se ha de escribir apellido[0] y para referenciar el último elemento de datos se ha de escribir datos[9].
Para declarar variables indexadas con mas de un índice, el formato es
Tipo nombre_variable[dim_1][dim_2]...[dim_k]
Usualmente se llama vector una variable con un índice y matriz una variable con dos índices. Además, el primer índice se llama fila, el segundo columna y el tercero página. A partir del cuarto índice no se utilizan nombres específicos.
Es frecuente emplear vectores para la manipulación de cadenas de caracteres, como nombres de personas, de calles, etc. En estos casos, hemos de declarar la variable con una longitud capaz de alojar el nombre más largo de entre los valores posibles. Así, para almacenar nombres de personas, podemos declarar el vector
char nombre[15]
si se sabe que 15 es una cota para el tamaño de los nombres que aparecerán durante la ejecución. Ahora bien, resulta muy incómodo tener que rellenar con espacios el resto de las letras hasta la 15ª cada vez que se lee un nombre desde el teclado. Para facilitar el tratamiento de estos vectores de caracteres con longitud variable, C tiene un tipo predefinido que resulta muy conveniente: el tipo string (cadena de caracteres). Su declaración es la normal de un vector de caracteres. Lo que cambia es su lectura y escritura. En el momento de leer una cadena de caracteres, el computador los leerá uno a uno hasta encontrar un cambio de línea. Cuando esto suceda, añadirá al final de la secuencia leída un carácter nulo (el carácter con código ASCII igual a 0). Esta marca le permitiría saber posteriormente donde se encuentra el fin de la cadena. Huelga decir que si sabemos que la longitud máxima de las cadenas a tratar es k, hace falta declarar el vector de dimensión k+1 para poder alojar también esta marca. Así, en el ejemplo anterior se debería declarar
char nombre[16]
La lectura de una cadena de caracteres se realiza poniendo en el control el formato %s y quitando del nombre de la variable el prefijo & en la instrucción scanf. Existe otra manera de leer una cadena desde el teclado, usando la instrucción gets con el formato
gets (nombre_cadena);
que lee todos los caracteres tecleados hasta pulsar la tecla retorno. Para la escritura, también hace falta utilizar el formato %s en la parte de control del printf.
C también posee una gran variedad de funciones para manipular cadenas. Las más corrientes son
strcpy (cad_1, cad_2);
strcat (cad_1, cad_2);
res_comparacion = strcmp (cad_1, cad_2);
longitud = strlen (cad);
La primera copia el contenido de cad_2 sobre cad_1. La segunda añade el contenido de cad_2 al final de cad_1. La tercera compara lexicográficamente los contenidos de las cadenas que recibe como entradas y retorna 0 si son la misma. Si cad_1 es mayor que cad_2, entonces retorna un número positivo y, si es menor, uno negativo. Finalmente strlen(cad) retorna la longitud de cad. Para utilizar estas instrucciones hace falta importarlas del módulo string.
Finalmente, cabe destacar que las cadenas constantes se encierran entre comillas dobles ". Por ejemplo, en
strcpy (cad, "hola");
se asocia la cadena hola a la variable cad.
4.1. Enumeración
La definición de tipos por extensión o enumeración sirve para definir tipos de datos que toman valores en un conjunto finito y tiene una sintaxis muy simple.
typedef enum {valor_1, valor_2, ..., valor_k} nombre_tipo
con esta definición, además, los k valores quedan ordenados con el orden en el que han sido declarados. Si se mira la definición del tipo booleano se puede ver que ha sido definida de esta manera. El hecho de poder hacer estas identificaciones puede resultar útil en muchos casos. Por ejemplo:
typedef enum {lunes=0, martes=1, miercoles=2, jueves=3,viernes=4} dia_laborable;
Para definir tipos nuevos mediante el constructor tabla se ha de utilizar la palabra reservada typedef, esta vez con el formato
typedef tipo_conocido tipo_nuevo[dim_1]...[dim_k];
donde dim_k es la longitud del k-ésimo dominio.
El formato del constructor tupla en C es el siguiente
typedef struct {
tipo_1 nombre_1;
...
tipo_k nombre_k; } tipo_nombre_nuevo;
donde, naturalmente, los tipos de la declaración anterior pueden ser cualesquiera de entre los tipos primitivos o definidos previamente y los nombres pueden ser de variables indexadas. Para acceder a los campos de una variable con un tipo definido mediante el constructor tupla, también se utiliza el punto. Así, si cliente es una variable del tipo persona y este tipo está definido como:
typedef struct {
char nombre[15];
char apellido[15];
int edad;
char ciudad[15]; } persona;
las expresiones siguientes:
cliente.nombre
cliente.edad
cliente.ciudad[0]
denotan respectivamente, el nombre, la edad y la primera letra de la ciudad de cliente.
5.1. Acciones y funciones
La manera de traducir en C la diferencia entre acciones y funciones pasa por la introducción de un nuevo tipo elemental de información llamado void. Esta palabra, que en inglés significa vacío, designa un tipo sin contenido, o sea que un elemento de tipo void no tendrá valor alguno y, por lo tanto, no ocupará memoria. Visto esto, se puede pasar a describir el formato general de los procedimientos en C. Este formato es
tipo nombre (lista_parámetros_formales);
{
cuerpo del procedimiento
}
donde tipo puede ser void, int, char o cualquier tipo definido por extensión (como por ejemplo booleano) y designa el tipo de valor retornado por el procedimiento. Así, en el primer caso, el procedimiento es una acción, ya que no retorna valor alguno, y en los otros casos es una función entera, de caracteres, etc. La primera línea del esquema anterior recibe el nombre de cabecera del procedimiento.
Cuando el procedimiento es una función, la instrucción pertinente para indicar al programa el valor que ha de retornar es
return expresión;
Cabe destacar que C no permite retornar valores de cualquier tipo, como por ejemplo vectores. Hay otro tipo de valor que una función puede retornar en C, llamado puntero, que permite un ‘apaño’ para retornar los tipos en principio no permitidos.
El cuerpo del procedimiento consta de una declaración de variables (que serán locales, o sea, visibles únicamente desde el mismo procedimiento) y de una secuencia de instrucciones ejecutables. De hecho, el cuerpo de un procedimiento tiene la misma estructura que un programa cualquiera.
Para la trasferencia de información entre procedimientos, C utiliza esencialmente dos mecanismos. En el caso de un parámetro de entrada, el computador genera otra variable, el nombre de la cual viene dado por el parámetro formal que aparece en la cabecera del procedimiento y deposita en ella una copia del valor (en el momento en que se efectúa la llamada) del parámetro real. Esto es costoso en memoria, ya que se ha de ocupar otra vez el espacio correspondiente al parámetro real, y en tiempo ya que hace falta escribir la copia, pero es seguro ya que el valor del parámetro no se puede alterar. De los parámetros transmitidos de esta manera se dice que son pasados por valor.
Por otra parte, en el caso de un parámetro de entrada/salida o de salida, el computador transmite al procedimiento invocado la dirección de memoria donde se almacena la variable pasada como parámetro. De esta manera, el procedimiento invocado podrá acceder a esta variable y, por lo tanto, modificar su contenido. Hay que añadir que, como las direcciones de memoria ocupan solo una palabra en el computador, su transmisión es muy eficiente tanto respecto al espacio como al tiempo. De los parámetros transmitidos de esta manera se dice que son pasados por dirección.
Para utilizar este mecanismo, C posee dos operadores básicos: & y *. El primero, aplicado a una variable (no indexada), denota su dirección de memoria. El segundo, aplicado a una dirección, denota el contenido de ésta. La instrucción que invoca tiene la forma
nombre_función (lista_de_parámetros)
donde en lista_de_parámetros se escriben los nombres de todas las variables transmitidas escribiendo & delante de las que se pasan por dirección. Por otra parte, en la cabecera del procedimiento invocado, la lista de parámetros es una secuencia de declaraciones de la forma
tipo nombre_parámetro
donde, si la variable se pasa por dirección, se ha de escribir * delante de su nombre. Como siempre, se pueden agrupar muchas variables del mismo tipo bajo una única declaración de tipo. Además, los nombres de las variables declaradas en la cabecera del procedimiento no están forzados a coincidir con los de las variables o expresiones transmitidas por el procedimiento que invoca; solo el orden ha de ser el mismo. Para leer o escribir en una variable recibida por dirección hace falta escribir siempre * delante del nombre del parámetro formal, ya que el nombre declarado en la cabecera del procedimiento denota justamente la dirección de la variable recibida.
Así, por ejemplo, si un procedimiento utiliza las variables enteras num_1 y num_2 y la variable booleana error, e invoca la acción modifica pasándole num_1 y error por dirección y num_2 por valor, el aspecto del programa C correspondiente será:
...
{
int num_1, num_2;
booleano error;
...
modifica(&num_1, num_2, &error);
...
}
void modifica(int *a, int b, booleano *c)
{
int d;
...
if (*c) *a=d;
else *c=TRUE;
acc1(a);
acc1(&b);
acc2(b+3);
acc2(*a);
...
}
void acc1(int *x)
{
...
}
void acc2(int x)
{
...
}
Como la acción modifica recibe la dirección de la variable a como primer argumento, al invocar acc1 (que recibe su argumento por dirección) con esta variable, le pasa exactamente la dirección recibida. En cambio, cuando invoca acc2 (que recibe su argumento por valor) con esta variable, le ha de pasar su valor y éste es obtenido mediante el uso de *. Exactamente lo contrario pasa con el segundo argumento.
Para el caso de variables definidas mediante tuplas, C posee un operador que simplifica la escritura de programas. Si x es la dirección de memoria de una tupla y campo es un campo de x, se tiene la equivalencia
x->campo ~ (*x).campo
y, si aniversario tiene los campos día, mes y año, al escribir, por ejemplo,
x=&(cliente.aniversario);
y mediante
a=x->dia;
b=x->mes;
c=x->año;
se asigna a las variables a, b y c respectivamente, el día, mes y año del aniversario de cliente.
En C hay una relación muy estrecha entre vectores y direcciones de memoria, que se podría resumir diciendo que el nombre de un vector denota la dirección de memoria de su primer elemento. De hecho, si se tiene, por ejemplo el vector
int a[10];
para C vale la igualdad siguiente:
a ~ &a[0]
Esto repercute en el paso de parámetros cuando se trata de variables indexadas, ya que no se ve como se puede realizar éste por valor. En efecto, C transmite siempre por dirección las variables indexadas, ya que al transmitir su nombre lo que hace es pasar su dirección. Así, en la instrucción que invoca, se escribe siempre el nombre de la variable sin más y en la cabecera del procedimiento invocado, el tipo y el nombre de la variable con sus dimensiones. Por ejemplo, si la acción modifica ha de recibir una matriz 10 por 10 de enteros, el aspecto del programa que la contiene será:
...
{
int mat[10][10];
...
modifica(mat);
...
}
void modifica(int m[10][10])
{...
m[2][7]=45;
...}
Las secuencias no se declaran como variables locales en los programas C, cuando se manipulan mediante la entrada/salida estándar. Consecuentemente, tampoco se pasarán como parámetros entre procedimientos. Como se está limitado a trabajar con una única secuencia, las instrucciones de lectura (o escritura) solo pueden hacer referencia a ésta y ello no genera ambigüedades.
En el momento de implementar en C un algoritmo que contiene acciones y funciones diversas, se crea un fichero donde se escribe el código de estos procedimientos en cualquier orden. El algoritmo principal tendrá la cabecera
void main(void)
y, si bien no ha de ser necesariamente el primero de los procedimientos en el fichero, se acostumbra a situarlo en este lugar. La única observación pertinente es que C exige poner una copia (seguida de un punto y coma) de las cabeceras de todos los otros procedimientos antes de comenzar el procedimiento principal. De esta manera, el aspecto general del código C de un algoritmo que utilice la acción modifica, la función entera calcula y la función booleana condición habrá de ser una cosa como:
declaracion_de_importaciones
definiciones_de_constantes
typedef int matriz[10][10];
void modifica(int *a, matriz mat, booleano error);
int calcula(int num, div);
booleano condición(int a, int b, matriz m);
void main(void)
{ cuerpo_del_procedimiento }
void modifica(int *a, matriz mat, booleano error);
{ cuerpo_del_procedimiento }
int calcula(int num, div);
{ cuerpo_del_procedimiento }
booleano condición(int a, int b, matriz m);
{ cuerpo_del_procedimiento }
#include<stdio.h>
#define MAX 15
typedef enum{FALSE=0, TRUE=1} booleano;
typedef char letras[MAX];
typedef struct {
letras cont;
int long;
} palabra;
void leer_primera_palabra(char *c, palabra *pal);
void leer_palabra(char *c, palabra *pal);
booleano ultima_palabra(palabra pal);
booleano palabras_iguales(palabra pal1, pal2);
void saltar_blancos(char *c);
void leer_letras(char *c, palabra *pal);
booleano comparar_texto(letras t1, letras t2, int long);
void main(void)
{
palabra primera, otra;
char c;
int num_rep;
printf("Escribe un texto acabado en punto \n");
leer_primera_palabra(&c,&primera);
num_rep=0;
leer_palabra(&c,&otra);
while (¡ultima_palabra(otra)){
if (palabras_iguales(primera,otra)) num_rep++;
leer_palabra(&c,&otra);
}
printf("La primera palabra del texto aparece %d veces \n", num_rep);
}
void leer_primera_palabra(char *c, palabra *pal);
{
*c=getchar();
saltar_blancos( c );
leer_letras(c,pal);
}
void leer_palabra(char *c, palabra *pal);
{
saltar_blancos( c );
leer_letras(c,pal);
}
booleano ultima_palabra(palabra pal);
{
return pal.long==0;
}
booleano palabras_iguales(palabra pal1, pal2);
{
booleano iguales;
if(pal1.long==pal2.long)
iguales=comparar_letras(pal1.cont,pal2.cont,pal1.long);
else iguales==FALSE;
return iguales;
}
void saltar_blancos(char *c);
{
while (*c==’ ‘) *c=getchar();
}
void leer_letras(char *c, palabra *pal);
{
(*pal).long=0;
while(*c!=’ ‘ && *c!=’.’) {
(*pal).long++;
(*pal).cont[(*pal).long]=*c;
*c=getchar();
}
}
booleano comparar_texto(letras t1, letras t2, int long);
{
int i;
i=0;
while(t1[i]==t2[i] && i<long-1) i++;
return t1[i]==t2[i];
}
La palabra clave de C para declara variables de tipo real es float. Para obtener mayor precisión en los cálculos, C ofrece el tipo double que, al igual que long int, utiliza el doble de memoria para su representación. Sin embargo, esta mejora en exactitud implica un mayor tiempo de ejecución. Se pueden resumir las características de estos tipos elementales en la siguiente tabla, donde en la columna de la derecha se usa la notación exponencial cuando se escribe aEb para representar el número a*10^b.
Tipos en C |
Número de bits |
Rango |
float double |
32 64 |
3.4E-38 a 3.4E+38 1.7E-308 a 1.7E+308 |
Las operaciones de lectura y escritura se efectúan con scanf y printf. Los códigos de formato para datos del tipo float son los que se muestran en la siguiente tabla:
Código |
Formato de la escritura |
%e %f %g |
Notación exponencial Punto decimal flotante La más corta de las anteriores |
Para la lectura se pueden utilizar indistintamente %e, %f o %g. Aquí también, el modificador l en los formatos %le, %lf o %lg sirve para indicar que se trata de datos del tipo double.
Los operadores relacionales para variables del tipo float o double son los mismos que para los tipos tratados en los capítulos anteriores, y los aritméticos los mismos que los utilizados para los enteros, con la excepción del operador % que en este contexto no tiene significado.
Ejemplo de la función para evaluar polinomios con la regla de Horner. Se supone que MAX es una constante declarada en algún lugar del programa que acota al grado de los polinomios a evaluar.
float horner(int n, float a[MAX], x)
{
int i;
float p;
p=0;
for (i=n;i>0;i=i-1) p=p*x+a[i];
return p;
}
Una característica de C es la posibilidad de escribir expresiones que mezclen diferentes tipos de datos. En el caso de datos numéricos, el resultado de la expresión será del tipo más general de los elementos que la integran. La siguiente cadena
int à long int à float à double
expresa la relación que existe entre los tipos numéricos de C: un tipo en la cadena es más general que sus antecesores en la misma.
C permite cambiar el tipo de una variable o expresión, usando un mecanismo que recibe el nombre inglés de casting. Su formato es
(tipo) expresión
y su efecto es forzar a expresión a ser del tipo especificado.
Por ejemplo si num es una variable entera, la instrucción
(float) num/2
provocará que el valor de num sea representado como un número real, y por tanto, la división por dos sea la división real. En cambio, la instrucción
(float) (num/2)
evaluará la división entera num/2 y transformará el resultado entero en un número real.
Se va a explicar como codificar el paso de acciones y funciones como parámetros, sin profundizar en el mecanismo en sí. En C se escribe la siguiente declaración algorítmica:
tipo
tipo_acción = acción (lista de los tipos de los parámetros)
tipo_función = función (lista de los tipos de los parámetros): tiporesultado
ftipo
como
typedef void (*tipoacción) (lista de los tipos de los parámetros);
typedef tiporesultado (*tipofunción) (lista de los tipos de los parámetros);
con lo que se consigue que tipoacción y tipofunción sean nuevos identificadores de tipo. Entonces el tipo del ejemplo se definirá como
typedef float (*funciónreal) (float);
y se podrá declarar variables como en
funciónreal f;
Ahora si se quiere que el valor de la variable f fuese la función tangente, cuyo prototipo es
float tangente (float);
Se procesaría la asignación
f=tangente;
Se observa que como f se ha declarado como una dirección de memoria, si se desea conocer el valor de f en el punto x hay que evaluar la expresión (*f)(x).
Por último si la función f es requerida como parámetro se traducirá
función integrar (f: funciónreal; a,b,error:real):real
como
float integrar (funcionreal f, float a, float b, float error);
Los procedimientos de las bibliotecas de C tiene la particularidad de que no pueden ser asignados a variables ni pasados como parámetros directamente. Sin embargo, esto no crea ningún problema. Si se desea utilizar una función disponible en la biblioteca, se crea una nueva función en el programa que se limite a evaluar la primera y se trabaja del modo habitual con la última. Se llama a este tipo de objetos funciones máscara. Para terminar este apartado, se escribe el algoritmo bisección como una función C.
float biseccion (funcionreal f, float a, float b, float eps)
{
float pint;
while (b-a>eps) {
pint=(a+b)/2.;
if ((*f)(pint)*(*f)(b)<=0) a=pint;
else b=pint;
}
return a;
}
El lenguaje C no permite todas las posibilidades de modularización vistas en las secciones anteriores, ya que si bien permite definir módulos y exportar acciones y funciones, no es posible la exportación de tipos ocultando su implementación.
El concepto de módulo en C se corresponde con el de fichero. O sea, que la implementación de un programa en diversos módulos se realiza distribuyendo código C en diversos ficheros.
Todas las acciones y funciones de un fichero son exportables, excepto aquellas que contienen la palabra reservada static es su declaración.
Para crear un módulo se edita un fichero nombre_módulo.h (la h viene del inglés header, cabecera), con las definiciones de todos los elementos que se quiere hacer públicos o exportables, y en otro fichero de nombre nombre_módulo.c se implementa todas las funciones y acciones, tanto públicas como privadas (estas últimas precedidas de la palabra static). En este segundo fichero, se han de incluir las definiciones del primero, y esto se hace incluyendo la orden #include de la forma
#include "nombre_módulo.h"
sentencia que también se ha de escribir en todos los módulos que importen alguno de sus elementos.
Para ilustrar esto se implemente el módulo conversiones. En primer lugar el fichero conversiones.h:
#define MAXDIG 16
typedef int base10;
typedef int tipodigito[MAXDIG];
typedef struct {
int longitud;
tipodigito digito;
} basedif10;
typedef union {
base10 valor10;
basedif10 valordif10;
} representacion;
typedef struct {
int base;
representacion repres;
} numero;
void cambiar_base(numero num, int base, numero *res);
Se ha definido el tipo representación con el constructor union, que permite almacenar datos de distintos tipos en una misma variable siempre y cuando solo se almacene un valor cada vez. Esta restricción es satisfecha en este caso ya que se sabe que solo uno de los dos campos del tipo base estará ocupado. Ahora se verá el contenido del fichero conversiones.c:
#include "conversiones.h"
static void base_a_decimal(numero num, numero *res);
static void decimal_a_base(numero num, int base, numero *res);
static void base_a_decimal(numero num, numero *res) {
representacion *n, *r;
int i;
n=&(num.repres);
r=&((*res).repres);
(*res).base=10;
r->valor10=0;
i=n->valordif10.longitud-1;
while (i!=-1) {
r->valor10=r->valor10*num.base+n->valordif10.digito[i];
i--; }
}
static void decimal_a_base(numero num, int base, numero *res) {
int a, largo;
representacion *n, *r;
n=&(num.repres);
r=&((*res).repres);
a=n->valor10;
largo=0;
r->valordif10.digito[largo]=a % base;
while (a>=base) {
a=a/base;
largo++;
r->valordif10.digito[largo]=a%base;
};
r->valordif10.longitud=largo+1;
(*res).base=base;
}
void cambiar_base(numero num, int base, numero *res) {
numero auxnum;
if (base==10) base_a_decimal(num,res);
else if (num.base==10) decimal_a_base(num,base,res);
else {
base_a_decimal(num,&auxnum);
decimal_a_base(auxnum,base,res);
}
}
Se observa que los procedimientos exportados se declaran en el programa con la inclusión de sus cabeceras en el fichero conversiones.h. así, el compilador de C puede verificar la corrección sintáctica del módulo principal en disponer del fichero conversiones.obj que de hecho, solo necesitará en el enlazado.
También es necesario que el programador del módulo principal conozca la especificación de las acciones y funciones importables. Los medios empleados por el programador del módulo para publicarlas pueden ser diversos (manual impreso, fichero de ayuda, etc.) y es irrelevante para nosotros.
Una situación usual en la creación de módulos funcionales es la ignorancia, por parte del programador, de las dimensiones de los vectores que el procedimiento a implementar ha de recibir como parámetros. Por ejemplo, en la función que evalúa un polinomio con la regla de horner, el polinomio es pasado como un vector float a[MAX], donde el valor de MAX ha sido declarado como una constante en algún lugar del programa. Ahora bien, si se quiere implementar la misma función independientemente del programa que la invoca, el valor de la constante MAX será desconocido y, por lo tanto, el parámetro formal float a[MAX] no tendrá sentido.
Una manera de resolver esta situación hace uso del hecho de que, al recibir un parámetro por dirección, el procedimiento recibe, en realidad, la dirección de memoria donde se encuentra el parámetro, y todas las consultas y modificaciones que se le hacen se efectúan directamente sobre esa región de memoria. Se puede entonces, pasar la dirección de la primera componente del vector junto con un nuevo parámetro de entrada de tipo entero, que contendrá la dimensión del vector en el procedimiento que invoca al llamado. En el ejemplo, el grado del polinomio cumple esta última función, y el aspecto general de la función horner será:
float horner(int n, float *a, float x) {
int i;
float p;
p=0;
for (i=n;i>=0;i=i-1) p=p*x+a[i];
return p;
}
En el caso de matrices o, más generalmente, de tablas con más de un índice, la situación es análoga. Solo se debe tener en cuenta que C considera una matriz como un vector de vectores. Por lo tanto, si se ha declarado float a[15][20] y se quiere acceder al elemento a[3][6], como su dirección de memoria es a+3*20+6, será suficiente escribir *(a+3*20+6).
De manera más general, si se tiene una tabla a[d1][d2]..[dn] la dirección de memoria del elemento a[p1][p2]..[pn] será a+p1*(d2*..*dn)+p2*(d3*..*dn)+..+pn y una manera de evitar la escritura de esta expresión cada vez que nos haga falta consiste en ponerle un alias mediante la definición
#define ind(p1,..pn,d2,..,dn) (p1*(d2*..*dn)+p2*(d3*..*dn)+..+pn)
que permite utilizar la primera expresión en lugar de la segunda en cualquier lugar del programa que contiene esta definición.
A título de ejemplo, se presenta ahora el código C de una función de uso general que determina si una matriz es simétrica. La función que se implementa ahora recibe la dirección de una matriz cuadrada nxn junto con su dimensión n.
#include <stdio.h>
#define ind(fil, col, n_col) (fil*n_col+col)
typedef enum{FALSO=0, CIERTO=1} booleano;
booleano simetrica(int *mat, int n)
{
int n,fil,col,dim,pos;
booleano sim;
dim=0;
sim=CIERTO;
while (dim!=n-1 && sim) {
pos=0;
while (pos!=dim+1 && *(mat+ind(dim+1,pos,n))==*(mat+ind(pos,dim+1,n)) {
pos++;
}
sim=*(mat+ind(dim+1,pos,n))==*(mat+ind(pos,dim+1,n));
dim++;
}
return sim;
}
Para implementar en C algoritmos que hacen usos de ficheros, hace falta declarar identificadores utilizando la palabra reservada FILE. De hecho, si f1 y f2 son los identificadores lógicos utilizados para trabajar con dos ficheros en un programa hará falta declararlos en este con el formato
FILE *f1, *f2;
Nótese que con esta declaración, f1 y f2 son, de hecho, direcciones de memoria que indican el lugar de la memoria central donde se almacenan las características del fichero (nombre, estado, posición actual del cabezal, etc.).
Antes de escribir los procedimientos disponibles en C para gestionar ficheros, nótese que hay una clase de éstos que reciben un tratamiento especial por parte del computador: los ficheros de texto, en los cuales cada registro almacena únicamente un carácter. Los ficheros de texto pueden ser abiertos desde el sistema operativo y pueden ser editados, modificados, etc.
Ahora se describen como escribir en C las operaciones públicas para gestionar ficheros de acceso secuencial:
f=fopen("nombre", "rb"); à abrir fichero binario para lectura
f=fopen("nombre", "wb"); à crear fichero binario para escritura
fread(&x, sizeof(x), 1, f); à leer del fichero en la posicion del cursor y guardar en x
fwrite(&x,sizeof(x),1,f); à escribir en la posicion del cursor la variable x
feof (f); à final de fichero
fclose(f); à cerrar fichero
unlink("nombre"); à borrar fichero "nombre"
Las instrucciones precedentes son para manejar ficheros binarios. Para ficheros de texto las instrucciones son las mismas a excepción de las de apertura del fichero, para las cuales "rb" o "wb" se han de sustituir por "r" o "w", respectivamente.
La función sizeof() es ejecutada por C en tiempo de compilación y sus apariciones en el código son sustituidas por su valor, que es el tamaño del tipo de dato que toma como argumento. Es de esta manera que, durante la ejecución, el programa sabe, por ejemplo, cuantos bits ha de leer del fichero en cada operación de lectura.
La cadena de caracteres "nombre" identifica al fichero desde el sistema operativo. Así, si creamos un fichero de nombre AUX.CHT, este será almacenado en el directorio desde el cual se ejecute el programa que crea el fichero. Si se quiere almacenar el fichero en otro lugar, se tendrá que especificar el camino en la misma cadena "nombre". Por ejemplo, trabajando en un PC bajo el sistema operativo DOS, se quiere crear el fichero AUX.CHT en el directorio TRABAJO que está en el disco duro identificado con D: entonces, se ha de escribir
f=fopen("D:\TRABAJO\AUX.CHT", "wb");
A titulo de ejemplo, se ve ahora el código C del algoritmo fusion_condicionada. Se supone la existencia de un módulo elemento del cual se importará el tipo t_elemento, la constante MAX y las funciones booleanas menor y prop. Además, se supone que el tipo nombre utilizado para almacenar nombres de ficheros son cadenas de caracteres. Ello permitirá utilizarlos directamente en las instrucciones de apertura de ficheros. El fichero fus_cond.h queda entonces
#include <stdio.h>
#include "elemento.h"
#include "nombres.h"
#define N 100
typedef nombre tabla_nombres[N];
void fusion_condicionada(tabla_nombres arch, nombre final, int n);
y el correspondiente fus_cond.c
typedef t_elemento tabla_elem[N];
typedef FILE *tabla_fich[N];
static void abrir_tabla_ficheros(tabla_nombres arch, int n, tabla_fich fARCH,
tabla_elem v, int *num);
static void fusionar_ficheros(FILE *fFinal, tabla_fich fARCH, tabla_elem v,
int *num, int n);
static void cerrar_tabla_ficheros(tabla_fich fARCH, int n);
static void avanzar_fich(int i, tabla_fich fARCH, tabla_elem v, int *num);
static int minimo(tabla_elem v, int n);
void fusion_condicionada(tabla_nombres arch, nombre final, int n)
{
tabla_fich fARCH;
FILE *fFinal;
int num;
tabla_elem v;
fFinal=fopen(final, "wb");
abrir_tabla_ficheros(arch, n, fARCH, v, &num);
fusionar_ficheros(fFinal, fARCH, v, &num, n);
cerrar_tabla_ficheros(fARCH, n);
fclose(fFinal);
}
static void abrir_tabla_ficheros(tabla_nombres arch, int n, tabla_fich fARCH,
tabla_elem v, int *num)
{
int i;
*num=0;
for (i=0; i<n; i++) {
fARCH[i]=fopen(arch[i], "wb");
avanzar_fich(i, fARCH, v, num);
}
}
static void fusionar_ficheros(FILE *fFinal, tabla_fich fARCH, tabla_elem v,
int *num, int n);
{
int i;
while (*num<n){
i=minimo(v,n);
if (prop(v[i])) fwrite(&v[i],sizeof(v[i]),1,fFinal);
avanzar_fich(i,fARCH, v, num);
}
}
static void cerrar_tabla_ficheros(tabla_fich fARCH, int n);
{
int i;
for (i=0; i<n; i++) fclose(fARCH[i]);
}
static void avanzar_fich(int i, tabla_fich fARCH, tabla_elem v, int *num);
{
fread(&v[i], sizeof(v[i]), 1, fARCH[i]);
if (feof(fARCH[i])) {
v[i]=MAX;
*num++;
}
}
static int minimo(tabla_elem v, int n);
{
int min, i;
min=0;
for (i=1; i<n; i++)
if (menor(v[i], v[min])) min=i;
return min
}
Nótese la declaración de tipo typedef FILE *tabla_fich[N], con la que se define un vector de N elementos, cada uno de los cuales es una dirección de memoria que contiene un identificador de fichero, e implica que si fARCH es una variable de este tipo, la expresión fARCH[i] haga referencia a la i-ésima de esas direcciones.
Para gestionar ficheros de acceso directo, la situación es muy similar, ya que se dispone de las instrucciones siguientes:
f=fopen("nombre", "r+b"); à abrir fichero de acceso directo
fseek(f, sizeof(x)*(p-1), SEEK_SET);
fread(&x, sizeof(x), 1, f);
fseek(f, sizeof(x)*(p-1), SEEK_SET);
fwrite(&x, sizeof(x), 1, f);
fclose(f); à cerrar fichero
unlink("nombre"); à borrar fichero
Como antes, las instrucciones precedentes son para manejar ficheros binarios. Para ficheros de texto, las instrucciones son las mismas a excepción de la de apertura (y creación) del fichero, donde "r+b" y "w+b" deben sustituirse por "r+t" y "w+t", respectivamente.
Las instrucciones de lectura y escritura se descomponen en dos partes. En la primera, el cabezal de lectura/escritura se desplaza a lo largo del fichero buscando la posición p, y en la segunda realiza la operación propiamente de lectura o escritura. En la instrucción fseek, el parámetro SEEK_SET indica que el desplazamiento ha de comenzar desde el origen del fichero y el segundo argumento que la longitud de este desplazamiento es de p-1 veces el tamaño sizeof(x) (cosa que, efectivamente, sitúa el cabezal en el origen del fichero del p-ésimo registro). C permite también comenzar el desplazamiento desde la posición actual del cabezal, o bien desde el final del fichero, mediante los parámetros SEEK_CUR y SEEK_END, respectivamente, que se han de pasar como tercer argumento. En estos casos, el segundo argumento tendrá la forma sizeof(x)*k con un k adecuadamente calculado para leer o escribir en el registro deseado.
void pivotar(FILE *f, int i, int n)
{
int j, k;
float x, y, z, p, aux;
fseek(f, sizeof(p)*pos(i, i, n), SEEK_SET);
fread(&p, sizeof(p), 1, f);
for (j=i+1; j<=n; j++) {
fseek(f, sizeof(x)*pos(j, i, n), SEEK_SET);
fread(&x, sizeof(x), 1, f);
if (x!=0) {
x=x/p;
for (k=1; k<=n; k++) {
fseek(f, sizeof(y)*pos(j, i, n), SEEK_SET);
fread(&y, sizeof(y), 1, f);
fseek(f, sizeof(z)*pos(i, k, n), SEEK_SET);
fread(&z, sizeof(z), 1, f);
fseek(f, sizeof(y)*pos(j, k, n), SEEK_SET);
aux=y-z*x;
fwrite(&aux, sizeof(y),1,f)
}
}
}
}