Capitulo 1: Introduccion General

Comencemos con una introduccion rapida a C. Nuestro objetivo es mostrar los elementos esenciales del lenguaje en programas reales, pero sin perdernos en detalles, reglas o excepciones. Por el momento, no intentamos ser completos ni precisos (exceptuando en los ejemplos, que si lo son). Deseamos llevarlo tan rapido com o sea posible al punto en donde pueda escribir programas utiles, y para hacerlo tenemos que concentrarnos en las bases: variables y constantes, arit­metica, control de flujo, funciones y los rudimentos de entrada y salida. Hemos dejado intencionalmente fuera de este capitulo las caracteristicas de C que son importantes para escribir programas mas grandes. Esas caracteristicas incluyen apuntadores, estructuras, la mayor parte del rico conjunto de operadores de C, varias proposiciones para control de flujo y la biblioteca estandar.

Este enfoque tiene sus inconvenientes. Lo mas notorio es que aqui no se en­cuentra la descripcion completa de ninguna caracteristica particular del lenguaje, y la introduccion, por su brevedad, puede tambien resultar confusa. Y debido a que los ejemplos no utilizan la potencia completa de C, no son tan concisos y elegan­tes como podrian serlo. Hemos tratado de aminorar esos efectos, pero tenga cui­dado. Otro inconveniente es que los capitulos posteriores necesariamente repetiran algo de lo expuesto en este. Esperamos que la repeticion, mas que molestar, ayude.

En cualquier caso, los programadores con experiencia deben ser capaces de extrapolar del material que se encuentra en este capitulo a sus propias necesidades de programacion. Los principiantes deben complementarlo escribiendo pequenos programas semejantes a los aqui expuestos. Ambos grupos pueden utilizar este capitulo como un marco de referencia sobre el cual asociar las descripciones mas detalladas que comienzan en el capitulo 2.

1.1 Comencemos

La unica forma de aprender un nuevo lenguaje de programacion es escribien­do programas con el. El primer programa por escribir es el mismo para todos los lenguajes:

Imprime las palabras

Viva Peron!

Este es el gran obstaculo; para librarlo debe tener la habilidad de crear el texto del programa de alguna manera, compilarlo con exito, cargarlo, ejecutarlo y des­cubrir a donde fue la salida. Con el dominio de estos detalles mecanicos, todo lo demas es relativamente facil.

En C, el programa para escribir "Viva Peron!" es

#include <stdio.h>

main()
(
    printf("Viva Peron!\n");
}

La forma de ejecutar este programa dependera del sistema que se este utilizan­do. Como un ejemplo especifico, en el sistema operativo UNIX se debe crear el programa en un archivo cuyo nombre termine con ".c" , como vivaperon.c , y despues compilarlo con la orden

cc vivaperon.c

Si no se ha cometido algun error, como la omision de un caracter o escribir algo en forma incorrecta, la compilacion se hara sin emitir mensaje alguno, y creara un archivo ejecutable llamado a.out. Si se ejecuta a.out escribiendo la orden

a.out

se presentara:

Viva Peron!

En otros sistemas, las reglas seran diferentes, consultelo con un experto.

Ahora algunas explicaciones acerca del programa en si. Un programa en C, cualquiera que sea su tamano, consta de funciones y variables. Una funcion con­tiene proposiciones que especifican las operaciones de calculo que se van a reali­zar, y las variables almacenan los valores utilizados durante los calculos. Las funciones de C son semejantes a las subrutinas y funciones de Fortran o a los procedimientos y funciones de Pascal. Nuestro ejemplo es una funcion llamada main. Normalmente se tiene la libertad de dar cualquier nombre que se desee, pero “main” es especial — el programa comienza a ejecutarse al principio de main. Esto significa que todo programa debe tener un main en algun sitio.

Por lo comun main llamara a otras funciones que ayuden a realizar su traba­jo, algunas que usted ya escribio, y otras de bibliotecas escritas previamente. La primera linea del programa.

#include <stdio.h>

indica al compilador que debe incluir informacion acerca de la biblioteca estan­dar de entrada/salida; esta linea aparece al principio de muchos archivos de codigo fuente de lenguaje C. La biblioteca estandar esta descrita en el capitulo 7 y en el apendice B.

Un metodo para comunicar datos entre las funciones es que la funcion que llama proporcione una lista de valores, llamados argumentos, a la funcion que esta invocando. Los parentesis que estan despues del nombre de la funcion encierran a la lista de argumentos. En este ejemplo, main esta definido para ser una funcion que no espera argumentos, lo cual esta indicado por la lista vacia ().

El primer programa en C

#include <stdio.h>          /* Incluye informacion acerca de la biblioteca estandar */

main()                      /* Define una funcion llamada main que no recibe valores de argumentos */
{                           /* Las proposiciones de main estan encerradas entre llaves */
  printf("Viva Peron!\n");  /* main llama a la funcion de biblioteca printf para escribir esta secuencia de caracteres; \n representa el caracter nueva linea. */
}

Las proposiciones de una funcion estan encerradas entre llaves {...}. La fun­cion main solo contiene una proposicion, printf ("Viva Peron!\n");. Una funcion se invoca al nombrarla, seguida de una lista de argumentos entre pa­rentesis; de esta manera se esta llamando a la funcion printf con el argumento "Viva Peron!\n". printf es una funcion de biblioteca que presenta la salida, en este caso la cadena de caracteres que se encuentra entre comillas. A una secuencia de caracteres entre comillas, como "Viva Peron!\n", se le llama cadena de caracteres o constante de cadena. Por el momento, nuestro unico uso de cadenas de caracteres sera como argumentos para printf y otras funciones.

La secuencia \n en la cadena representa el caracter nueva linea en la notacion de C, y hace avanzar la impresion al margen izquierdo de la siguiente linea. Si se omite el \n (un experimento que vale la pena hacer), encontrara que no hay avance de linea despues de la impresion. Se debe utilizar \n para incluir un caracter de nue­va linea en el argumento de printf; si se intenta algo como

printf("Viva Peron!
");

...el compilador de C producira un mensaje de error.

printf nunca proporciona una nueva linea automaticamente, de manera que se pueden utilizar varias llamadas para construir una linea de salida en etapas.

Nuestro primer programa tambien pudo haber sido escrito de la siguiente manera:

#include <stdio.h>
main()
{
    printf ("Viva");
    printf ("Peron!");
    printf ("\n");
}

...produciendose una salida identica.

Notese que \n representa un solo caracter. Una secuencia de escape como \n proporciona un mecanismo general y extensible para representar caracteres invisi­bles o dificiles de escribir. Entre otros que C proporciona estan \t para tabula­cion, \b para retroceso, \" para comillas, y \\ para la barra diagonal invertida. Hay una lista completa en la seccion 2.3.

  • Ejercicio 1-1. Ejecute el programa "vivaperon" en su sistema. Experimente con la omision de partes del programa, para ver que mensajes de error se obtienen. □
  • Ejercicio 1-2. Experimente el descubrir que pasa cuando la cadena del argumento de printf contiene \c, en donde c es algun caracter no puesto en lista anterior­mente. □

1.2 Variables y Expresiones Aritmeticas

El siguiente programa utiliza la formula °C =(5/9) x (°F-32) para imprimir la siguiente tabla de temperaturas Fahrenheit y sus equivalentes centigrados o Celsius:

0   -17
20  -6
40  4
60  15
80  26
100 37
120 38
140 60
160 71
180 82
200 93
220 104
240 115
260 126
280 137
300 148

En si el programa aun consiste de la definicion de una unica funcion llamada main. Es mas largo que el que imprime Viva peron!, pero no es complicado. Introduce varias ideas nuevas, incluyendo comentarios, declaraciones, variables, expresiones aritmeticas, ciclos y salida con formato.

#include < std io .h >
/* imprime Tabla Fahrenheit-Celsius para fahr = 0, 20, ..., 300 */
main()
{
    int fahr, celsius;
    int inferior, superior, paso;

    inferior = 0;      /* limite inferior de la tabla de temperaturas */
    superior = 300;    /* limite superior */
    paso = 20;         /* tamano del incremento */

    fahr = inferior;
    while (fahr < = superior) {
    celsius = 5 * (fahr-32) / 9;
    printf("%d\t%d\n", fahr, celsius);
    fahr = fahr + paso;
    }
}

Las dos lineas

/* imprime la tabla Fahrenheit-Celsius
    para fahr = 0, 20, ..., 300 */

...son un comentario, que en este caso explica brevemente lo que hace el programa. Cualesquier caracteres entre /* y */ son ignorados por el compilador, y pueden ser utilizados libremente para hacer a un programa mas facil de entender. Los comentarios pueden aparecer en cualquier lugar donde puede colocarse un espacio en blanco, un tabulador o nueva linea.

En C, se deben declarar todas las variables antes de su uso, generalmente al principio de la funcion y antes de cualquier proposicion ejecutable. Una declara­cion notifica las propiedades de una variable; consta de un nombre de tipo y una lista de variables, como

int fahr, celsius;
int inferior, superior, paso;

El tipo int significa que las variables de la lista son enteros, en contraste con float, que significa punto flotante, esto es, numeros que pueden tener una parte fraccionaria. El rango tanto de int como de float depende de la maquina que se esta utili­zando; los int de 16 bits, que estan comprendidos entre el -32768 y +32767, son comunes, como lo son los int de 32 bits. Un numero float tipicamente es de 32 bits, por lo menos con seis digitos significativos y una magnitud generalmente entre 10 a la -38 y 10 a la +38.

Ademas de int y float, C proporciona varios tipos de datos basicos, incluyendo:

char caracter —un solo byte
short entero corto
long entero largo
double punto flotante de doble precision

Los tamanos de estos objetos tambien dependen de la maquina. Tambien existen arreglos, estructuras y uniones de estos tipos basicos, apuntadores a ellos y funciones que regresan valores con esos tipos, todo lo cual se vera en el momento oportuno.

Los calculos en el programa de conversion de temperaturas principian con las proposiciones de asignacion.

inferior = 0 ;
superior = 300;
paso = 20 ;

...que asignan a las variables sus valores iniciales. Las proposiciones individuales se terminan con punto y coma ;.

Cada linea de la tabla se calcula de la misma manera por lo que se utiliza una iteracion que se repite una vez por cada linea de salida; este es el proposito del ciclo while:

while (fahr <= superior) {
    ...
}

El ciclo while funciona de la siguiente manera: se prueba la condicion entre pa­rentesis. De ser verdadera (si fahr es menor o igual que superior), el cuerpo del ciclo (las tres proposiciones entre llaves) se ejecuta. Luego la condicion se prueba nue­vamente, y si es verdadera, el cuerpo se ejecuta de nuevo. Cuando la prueba resul­ta falsa (fahr excede a superior) la iteracion termina, y la ejecucion continua en la proposicion que sigue al ciclo. No existe ninguna otra proposicion en este progra­ma, de modo que termina.

El cuerpo de un while puede tener una o mas proposiciones encerradas entre llaves, como en el convertidor de temperaturas, o una sola proposicion sin llaves, como en:

while (i < j)
    i = 2 + i;

En cualquier caso, siempre se sangra la proposicion controlada por el while con una tabulacion (lo que se presenta aqui a cuatro espacios) para poder apreciar de un vistazo cuales proposiciones estan circunscriptas dentro del ciclo. El sangrado enfatiza la estructura logica del programa. Aunque a los compiladores de C no les importa la apariencia del programa, un sangrado y espaciamiento adecuados son muy im­portantes para hacer programas faciles de leer. Recomendamos escribir una sola proposicion por linea y utilizar espacios en blanco alrededor de los operadores para dar claridad al agrupamiento. La posicion de las llaves es menos importante, aunque los programadores del pueblo sostienen credos pasionales al respecto. Se eligio uno de los varios esti­los populares. Escoja un estilo que le satisfaga y sea consistente en su uso.

La mayor parte del trabajo se realiza en el cuerpo del ciclo. La temperatura Celsius se calcula y se asigna a la variable celsius por la proposicion.

celsius = 5 * (fahr—32) / 9;

La razon de multiplicar por 5 y despues dividir entre 9 en lugar de solamente multiplicar por 5/9 es que en C - como en muchos otros lenguajes - la division de enteros trunca el resultado: cualquier parte fraccionaria se descarta. Puesto que 5 y 9 son enteros, 5/9 seria truncado a cero y asi todas las temperaturas Celsius se reportarian como cero.

Este ejemplo tambien muestra un poco mas acerca de como funciona printf. En realidad, printf es una funcion de proposito general para dar formato de sali­da, que se describira con detalle en el capitulo 7. Su primer argumento es una ca­dena de caracteres que seran impresos, con cada % indicando en donde uno de los otros (segundo, tercero, ...) argumentos va a ser sustituido, y en que forma sera impreso. Por ejemplo, %d especifica un argumento entero, de modo que la proposicion

printf("%d\t%d\n", fahr, celsius);

hace que los valores de los dos enteros fahr y celsius sean escritos, con una tabulacion (\t) entre ellos.

Cada construccion % en el primer argumento de printf esta asociada con el correspondiente segundo argumento, tercero, etc., y deben corresponder apro­piadamente en numero y tipo, o se tendran soluciones incorrectas.

Con relacion a esto, printf no es parte del lenguaje C; no existe propiamente una entrada o salida definida en C. printf es solo una util funcion de la biblioteca estandar de funciones que esta accesible normalmente a los programas en C. Sin embargo, el comportamiento de printf esta definido en el estandar ANSI, por lo que sus propiedades deben ser las mismas en cualquier compilador o biblioteca que se apegue a el.

Para concentrarnos en C, no hablaremos mucho acerca de la entrada y la sali­da hasta el capitulo 7. En particular, pospondremos el tema de la entrada con for­mato hasta entonces. Si se tiene que darle entrada a numeros, lease la discusion de la funcion scanf en la seccion 7.4. La funcion scanf es como printf, exceptuan­do que lee de la entrada en lugar de escribir a la salida.

Existen un par de problemas con el programa de conversion de temperaturas.

El mas simple es que la salida no es muy estetica debido a que los numeros no estan justificados hacia su derecha. Esto es facil de corregir; si aumentamos a cada %d de la proposicion printf una amplitud, los numeros impresos seran jus­tificados hacia su derecha dentro de sus campos. Por ejemplo, podria decirse:

printf("%3d %6d\n", fahr, celsius);

para escribir el primer numero de cada linea en un campo de tres digitos de an­cho, y el segundo en un campo de seis digitos, como esto:

0    -17
20    -6
40     4
60    15
80    26
100   37
...

El problema mas grave es que debido a que se ha utilizado aritmetica de en­teros, las temperaturas Celsius no son muy precisas; por ejemplo, 0ºF es en rea­lidad aproximadamente —17.8°C, no —17ºC. Para obtener soluciones mas preci­sas, se debe utilizar aritmetica de punto flotante en lugar de entera. Esto requiere de algunos cambios en el programa. Aqui esta una segunda version:

#include <stdio.h>

/* imprime la tabla Fahrenheit-Celsius
   para fahr = 0, 20, ..., 300; version de coma flotante */
main()
{
    float fahr, celsius;
    float inferior, superior, paso;

    inferior = 0;     /* limite inferior de escala de temperatura */
    superior = 300;   /* limite superior */
    paso = 20;        /* tamano de paso */

    fahr = inferior;
    while (fahr <= superior) {
        celsius = (5.0/9.0) * (fahr-32.0);
        printf("%3.0f %6.1f\n", fahr, celsius);
        fahr = fahr + paso;
    }
}

Esto es muy semejante a lo anterior, excepto que fahr y celsius estan declara­dos como float, y la formula de conversion esta escrita en una forma mas natural. No pudimos utilizar 5/9 en la version anterior debido a que la division entera lo truncaria a cero. Sin embargo, un punto decimal en una constante indica que esta es de punto flotante, por lo que 5.0/9.0 no se trunca debido a que es una relacion de dos valores de punto flotante.

Si un operador aritmetico tiene operandos enteros, se ejecuta una operacion entera. Si un operador numerico tiene un operando de punto flotante y otro ente­ro, este ultimo sera convertido a punto flotante antes de hacer la operacion. Si se hubiera escrito (fahr-32), el 32 seria convertido automaticamente a punto flotante. Escribir constantes de punto flotante con puntos decimales explicitos, aun cuando tengan valores enteros, destaca su naturaleza de punto flotante para los lectores humanos.

Las reglas detalladas de cuando los enteros se convierten a punto flotante se encuentran en el capitulo 2. Por ahora, notese que la asignacion

fahr = inferior;

y la prueba

while (fahr <= superior)

tambien trabajan en la forma natural (el int se convierte a float antes de efec­tuarse la operacion).

La especificacion de conversion %3.0f del printf indica que se escribira un numero de punto flotante (en este caso fahr) por lo menos con tres caracteres de ancho, sin punto decimal y sin digitos fraccionarios; %6.1f describe a otro numero (celsius) que se escribira en una amplitud de por lo menos 6 caracteres, con 1 digito despues del punto decimal. La salida tendra el siguiente aspecto:

 0  -17.8
20   -6.7
40    4.4
...

La amplitud y la precision pueden omitirse de una especificacion: %6f indica que el numero es por lo menos de seis caracteres de ancho; %.2f indica dos caracteres despues del punto decimal, pero el ancho no esta restringido; y %f unicamente indica escribir el numero como punto flotante.

%d Escribe como entero decimal
%6d escribe como entero decimal, por lo menos con 6 caracteres de amplitud.
%f escribe como punto flotante
%6f escribe como punto flotante, por lo menos con 6 caracteres de amplitud
%.2f escribe como punto flotante, con 2 caracteres despues del punto decimal.
%6.2f escribe como punto flotante, por lo menos con 6 caracteres, y con dos caracteres despues del punto decimal.

Entre otros, printf tambien reconoce %o para octal, %x para hexadecimal, %c para caracter, %s para cadena de caracteres y %% para denotar el caracter % en si.

  • Ejercicio 1-3. Modifique el programa de conversion de temperaturas de modo que escriba un encabezado sobre la tabla. □

  • Ejercicio 1-4. Escriba un programa que imprima la tabla correspondiente Celsius a Fahrenheit. □

1.3 La proposicion for

Existen suficientes formas distintas de escribir un programa para una tarea en particular. Intentemos una variacion del programa de conversion detem peraturas.

#include <stdio.h>

/* imprime la tabla Fahrenheit-Celsius */
main()
{
    int fahr;

    for (fahr = 0; fahr <= 300; fahr = fahr + 20)
        printf("%3d %6.1f\n", fahr, (5.0/9.0)*(fahr-32));
}

Este produce los mismos resultados, pero ciertamente se ve diferente. Un cambio importante es la eliminacion de la mayoria de las variables; solo permanece fahr y la hemos convertido en int. Los limites inferior y superior y el tamano del paso solo aparecen como constantes dentro de la proposicion for, que es una nueva construccion, y la expresion que calcula la temperatura Celsius ahora aparece como el tercer argumento de printf en vez de una proposicion de asignacion sepa­rada.

Este ultimo cambio ejemplifica una regla general — en cualquier contexto en el que se permita utilizar el valor de una variable de algun tipo, es posible usar una expresion mas complicada de ese tipo. Puesto que el tercer argumento de printf debe ser un valor de punto flotante para coincidir con %6.1f, cualquier ex­presion de punto flotante puede ocurrir aqui.

La proposicion for es un ciclo, una forma generalizada del while. Si se compara con el while anterior, su operacion debe ser clara. Dentro de los parentesis existen tres secciones, separadas por punto y coma ;. La primera, la inicializacion

fahr = 0

se ejecuta una vez, antes de entrar propiamente al ciclo. La segunda seccion es la condicion o prueba que controla el ciclo:

fahr <= 300

Esta condicion se evalua; si es verdadera, el cuerpo del ciclo (en este caso un sim­ple printf) se ejecuta. Despues el incremento de avance

fahr = fahr + 20

se ejecuta y la condicion se vuelve a evaluar. El ciclo termina si la condicion se hace falsa. Tal como con el while, el cuerpo del ciclo puede ser una proposicion sencilla o un grupo de proposiciones encerradas entre llaves. La inicializacion, la condicion y el incremento pueden ser cualquier expresion.

La seleccion entre while y for es arbitraria, y se basa en aquello que parezca mas claro. El for es por lo general apropiado para ciclos en los que la inicializa­cion y el incremento son proposiciones sencillas y logicamente relacionadas, pues­to que es mas compacto que el while y mantiene reunidas en un lugar a las propo­siciones que controlan al ciclo.

  • Ejercicio 1-5. Modifique el programa de conversion de temperaturas de manera que escriba la tabla en orden inverso, esto es, desde 300 grados hasta 0. □

1.4 Constantes simbolicas

Una observacion final antes de dejar definitivamente el tema de la conversion de temperaturas. Es una mala practica poner “numeros magicos” tales como 300 y 20 en un programa, ya que le proporcionan muy poca informacion a quien tenga que leer el programa, y son dificiles de modificar de forma sistematica. Una mane­ra de tratar a esos numeros magicos es darles nombres significativos. Una linea #define define un nombre simbolico o constante simbolica como una cadena de caracteres particularmente especial:

#define nombre  texto de reemplazo

A partir de esto, cualquier ocurrencia de nombre (que no este entre comillas ni como parte de otro nombre) se sustituira por el texto de reemplazo correspon­diente. El nombre tiene la misma forma que un nombre de variable: una secuen­cia de letras y digitos que comienza con una letra. El texto de reemplazo puede ser cualquier secuencia de caracteres; no esta limitado a numeros.

#include <stdio.h>
#define INFERIOR   0     /* limite inferior de la tabla »/
#define SUPERIOR    300  /* limite superior */
#define PASO        20   /* tamano del paso de incremento */

/* imprime la tabla Fahrenheit-Celsius */
main()
{
    int fahr;

    for (fahr = INFERIOR; fahr <= SUPERIOR; fahr = fahr + PASO)
        printf("%3d %6.1f\n", fahr, (5.0/9.0)*(fahr-32));
}

Las cantidades INFERIOR, SUPERIOR y PASO son constantes simbolicas, no variables, por lo que no aparecen entre las declaraciones. Por convencion, los nombres de constantes simbolicas, se escriben con letras mayusculas, de forma tal que puedan distinguirse facilmente de los nombres de variables (escritos con minusculas). Notese que no hay punto y coma al final de una linea #define.

1.5 Entrada y salida de caracteres

Ahora vamos a considerar una familia de programas relacionados para el procesamiento de datos de tipo caracter. Se encontrara que muchos programas solo son versiones ampliadas de los prototipos que se tratan aqui.

El modelo de entrada y salida manejado por la biblioteca estandar es muy simple. La entrada y salida de texto, sin importar donde fue originada o hacia donde se dirige, se tratan como flujos ("streams") de caracteres. Un flujo de texto es una secuencia de caracteres divididos entre lineas, cada una de las cuales consta de cero o mas caracteres seguidos de un caracter nueva linea. La biblioteca es responsable de hacer que cada secuencia de entrada o salida este de acuerdo con este modelo; el programador de C que utiliza la biblioteca no necesita preocuparse de como estan representadas las lineas fuera del programa.

La biblioteca estandar proporciona varias funciones para leer o escribir de a un caracter a la vez, de las cuales getchar y putchar son las mas simples. Cada vez que es invocada, getchar lee el siguiente caracter de entrada de una secuencia de tex­to y lo devuelve como su valor. Esto es, despues de

c = getchar()

la variable c contiene el siguiente caracter de entrada. Los caracteres provienen normalmente del teclado; la entrada de archivos se trata en el capitulo 7.

La funcion putchar escribe un caracter cada vez que se invoca:

putchar(c);

escribe el contenido de la variable entera c como un caracter, generalmente en la pantalla; Las llamadas a putchar y a printf pueden estar alternadas; la salida aparecera en el orden en que se realicen las llamadas.

1.5.1 Copia de archivos

Con getchar y putchar se puede escribir una cantidad sorprendente de codigo util sin saber nada mas acerca de entrada y salida. El ejemplo mas sencillo es un programa que copia la entrada en la salida, un caracter a la vez:

lee un caracter
while (caracter no es indicador de fin de archivo)
    manda a la salida el caracter recien leido
    lee un caracter

Al convertir esto en C se obtiene

#include <stdio.h>
/* copia la entrada a la salida; la. version */
main()
{
    int c;

    c = getchar();
    while (c != EOF) {
        putchar(c);
        c = getchar();
    }
}

El operador de relacion != significa "no igual a".

Lo que aparece como un caracter en el teclado o en la pantalla es, por supues­to, como cualquier otra cosa, almacenado internamente como un patron de bits. El tipo char tiene la funcion especifica de almacenar ese tipo de dato, pero tambien puede ser usado cualquier tipo de entero. Usamos int por una sutil pero importante razon.

El problema es distinguir el fin de la entrada de los datos validos. La solucion es que getchar devuelve un valor distintivo cuando no hay mas a la entrada, un valor que no puede ser confundido con ningun otro caracter. Este valor se llama EOF, por "end-of-file" ("fin de archivo"). Se debe declarar c con un tipo que sea lo suficientemente grande para almacenar cualquier valor que le regrese getchar. No se puede utilizar char puesto que c debe ser suficientemente grande como para mantener a EOF ademas de cualquier otro caracter. Por lo tanto, se emplea int.

EOF es un entero definido en <stdio.h>, pero el valor numerico especifico no importa mientras que no sea el mismo que ningun valor tipo char. Utilizando la constante simbolica, hemos asegurado que nada en el programa depende del valor numerico especifico.

El programa para copiar podria escribirse de modo mas conciso por progra­madores experimentados de C. En lenguaje C, cualquier asignacion, tal como

c = getchar();

es una expresion y tiene un valor: el del lado izquierdo luego de la asignacion. Es­to significa que una asignacion puede aparecer como parte de una expresion mas larga. Si la asignacion de un caracter a c se coloca dentro de la seccion de prueba de un ciclo while, el programa que copia puede escribirse de la siguiente manera:

#include <stdio.h>
/* copia la entrada a la salida; 2a. version */
main()
{
    int c;

    while ((c = getchar()) != EOF)
        putchar(c);
}

El while obtiene un caracter, lo asigna a c, y entonces prueba si el caracter fue la senal de fin de archivo. De no serlo, el cuerpo del while se ejecuta, escribiendo el caracter; luego se repite el while. Luego, cuando se alcanza el final de la entra­da, el while termina y tambien lo hace main.

Esta version centraliza la entrada — ahora hay solo una referencia a getchar — y reduce el programa. El programa resultante es mas compacto y mas facil de leer una vez que se domina el truco. Usted vera seguido este estilo. (Sin embargo, es posible descarriarse y crear codigo impenetrable, una tendencia que tratare­mos de reprimir.)

Los parentesis que estan alrededor de la asignacion dentro de la condicion son necesarios. La precedencia de != es mas alta que la de =, lo que significa que en ausencia de parentesis la prueba de relacion != se realizaria antes de la asigna­cion =. De esta manera, la proposicion

c = getchar() != EOF

es equivalente a

c = (getchar() != EOF)

Esto tiene el efecto indeseable de hacer que c sea 0 o 1, dependiendo de si la lla­mada de getchar encontro fin de archivo. (En el capitulo 2 se trata este tema con mas detalle).

  • Ejercicio 1-6. Verifique que la expresion getchar() != EOF es 0 o 1. □
  • Ejercicio 1-7. Escriba un programa que imprima el valor de EOF. □

1.5.2 Conteo de caracteres

El siguiente programa cuenta caracteres y es semejante al programa que copia.

#include <stdio.h>
/* cuenta los caracteres de la entrada; la. version */
main()
{
    long nc;

    nc = 0;
    while (getchar() != EOF)
        ++nc;
    printf("%ld\n", nc);
}

La proposicion

++nc;

presenta un nuevo operador, ++, que significa incrementa en uno. Es posible escribir nc = nc + 1, pero ++nc es mas conciso y muchas veces mas eficiente. Hay un operador correspondiente -- para disminuir en 1. Los operadores ++ y -- pueden ser tanto operadores prefijos (++nc) como postfijos (nc++); esas dos formas tienen diferentes valores dentro de las expresiones, como se de­mostrara en el capitulo 2, pero ambos ++nc y nc++ incrementan a nc. Por el momento adoptaremos la forma de prefijo.

El programa para contar caracteres acumula su cuenta en una variable long en lugar de una int. Los enteros long son por lo menos de 32 bits. Aunque en algunas maquinas int y long son del mismo tamano, en otras un int es de 16 bits, con un valor maximo de 32767, y tomaria relativamente poca lectura a la entrada para desbordar un contador int. La especificacion de conversion %ld indica a printf que el argumento correspondiente es un entero long.

Seria posible tener la capacidad de trabajar con numeros mayores empleando un double (float de doble precision). Tambien se utilizara una proposicion for en lugar de un while, para demostrar otra forma de escribir el ciclo.

#include <stdio.h>
/* cuenta los caracteres de la entrada; 2a. version */
main()
{
    double nc;
    for (nc = 0; gechar() != EOF; ++nc)
        ;
    printf("%.0f\n", nc);
}

printf utiliza %f tanto para float como para double; %.0f suprime la impresion del punto decimal y de la parte fraccionaria, que es cero.

El cuerpo de este ciclo for esta vacio, debido a que todo el trabajo se realiza en las secciones de prueba e incremento. Pero las reglas gramaticales de C requie­ren que una proposicion for tenga un cuerpo. El ; aislado se llama proposicion nula, y esta aqui para satisfacer este requisito. Lo colocamos en una linea aparte para que sea visible.

Antes de abandonar el programa para contar caracteres, observese que si la entrada no contiene caracteres, la prueba del while o del for no tiene exito desde la primera llamada getchar, y el programa produce cero, el resultado correcto. Esto es importante. Uno de los aspectos agradables acerca del while y del for es que hacen la prueba al inicio del ciclo, antes de proceder con el cuerpo. Si no hay nada que hacer, nada se hace, aun si ello significa no pasar a traves del cuerpo del ciclo. Los programas deben actuar en forma inteligente cuando se les da una entrada de longitud cero. Las proposiciones while y for ayudan a asegurar que los programas realizan cosas razonables con condiciones de frontera.

1.5.3 Conteo de lineas

El siguiente programa cuenta lineas a la entrada. Como se menciono anterior­mente, la biblioteca estandar asegura que una secuencia de texto de entrada pa­rezca una secuencia de lineas, cada una terminada por un caracter nueva linea.

Por lo tanto, contar lineas es solamente contar caracteres nueva linea:

#include <stdio.h>
/* cuenta las lineas de la entrada */
main()
{
    int c, nl;

    nl = 0;
    while ((c = getchar()) != EOF)
        if (c == '\n')
            ++nl;
    printf("%d\n", nl);
}

El cuerpo del while consiste ahora en un if, el cual a su vez controla el incre­mento ++n1. La proposicion if prueba la condicion que se encuentra entre pa­rentesis y, si la condicion es verdadera, ejecuta la proposicion (o grupo de proposiciones entre llaves) que le sigue. Hemos sangrado nuevamente para mostrar lo que controla cada elemento.

El doble signo de igualdad == es la notacion de C para expresar "igual a" (como el = simple de Pascal o el .EQ. de Fortran). Este simbolo se emplea para distinguir la prueba de igualdad del = simple que utiliza C para la asignacion. Un mensaje de alerta: los principiantes de C ocasionalmente escriben = cuando en realidad deben usar == . Como se vera en el capitulo 2, el resultado es por lo general una expresion legal, de modo que no se obtendra ninguna advertencia.

Un caracter escrito entre apostrofos '...' representa un valor entero igual al valor numerico del caracter en el conjunto de caracteres de la maquina. Esto se llama una constante de caracter, aunque solo es otra forma de escribir un pequeno entero. Asi, por ejemplo 'A' es una constante de caracter; en el conjunto ASCII de caracteres su valor es 65 (esta es la representacion interna del caracter A). Por su­puesto 'A' es preferible que 65: su significado es obvio, y es independiente de un conjunto de caracteres en particular.

Las secuencias de escape que se utilizan en constantes de cadena tambien son le­gales en constantes de caracter; asi, '\n' significa el valor del caracter nueva linea, el cual es 10 del codigo ASCII. Se debe notar cuidadosamente que '\n' es un caracter simple, y en expresiones es solo un entero; por otro lado, '\n' es una constante de cadena que contiene solo un caracter. En el capitulo 2 se trata el tema de cadenas versus caracteres.

  • Ejercicio 1-8. Escriba un programa que cuente espacios en blanco, tabuladores y nuevas lineas. □
  • Ejercicio 1-9. Escriba un programa que copie su entrada a la salida, reemplazando cada cadena de uno o mas blancos por un solo blanco. □
  • Ejercicio 1-10. Escriba un programa que copie su entrada a la salida, reemplazan­do cada tabulacion por \t , cada retroceso por \b y cada diagonal invertida por \\. Esto hace que las tabulaciones y los espacios sean visibles sin confusiones. □

1.5.4 Conteo de palabras

El cuarto en nuestra serie de programas utiles cuenta las lineas, palabras y ca­racteres, usando la definicion de que una palabra es cualquier secuencia de carac­teres que no contiene espacio en blanco ni tabulacion ni nueva linea. Esta es una version reducida del programa wc de UNIX.

#include <stdio.h>

#define DENTRO  1 /* en una palabra */
#define FUERA   0 /* fuera de una palabra */

/* cuenta lineas, palabras, y caracteres de la entrada */
main()
{
    int c, nl, nw, nc, state;

    state = OUT;
    nl = nw = nc = 0;
    while ((c = getchar()) != EOF) {
        ++nc;
        if (c == '\n')
            ++nl;
        if (c == ' ' || c == '\n' || c = '\t')
            state = FUERA;
        else if (state == FUERA) {
            state = IN;
            ++nw;
        }
    }
    printf("%d %d %d\n", nl, nw, nc);
}

Cada vez que el programa encuentra el primer caracter de una palabra, conta­biliza una palabra mas. La variable state registra si actualmente el programa esta o no sobre una palabra; al iniciar es “no esta sobre una palabra”, por lo que se asigna el valor DENTRO. Es preferible usar las constantes simbolicas DENTRO y FUERA que los valores literales 1 y 0, porque hacen el programa mas legible. En un programa tan pequeno como este, la diferencia es minima, pero en programas mas grandes el incremento en claridad bien vale el esfuerzo extra que se haya realizado para escribir de esta manera desde el principio. Tambien se descubrira que es mas facil hacer cambios extensivos en programas donde los numeros magicos aparecen solo como constantes simbolicas.

La linea

n1 = nw = nc = 0;

inicializa a las tres variables en cero. Este no es un caso especial sino una conse­cuencia del hecho de que una asignacion es una expresion con un valor, y que las asignaciones se asocian de derecha a izquierda. Es como si se hubiese escrito

n1 = (nw = (nc = 0));

El operador || significa "OR", por lo que la linea

if (c == ' ' || c == '\n' || c = '\t')

dice "si c es un caracter en blanco o c es nueva linea, o c es un tabulador". (Recuerde que la secuencia de escape \t es una representacion visible del caracter tabulador). Existe un correspondiente operador && para AND; su precedencia es mas alta que la de ||. Las expresiones conectadas por && o || se evaluan de izquierda a derecha, y se garantiza que la evaluacion terminara tan pronto como se conozca la verdad o falsedad. Si c es un caracter en blanco, no hay necesidad de probar si es una nue­va linea o un tabulador, de modo que esas pruebas no se hacen. Esto no es de particular importancia en este caso, pero es significativo en situaciones mas com­plicadas, como se vera mas adelante.

El ejemplo muestra tambien un else, el cual especifica una accion alternativa si la condicion de una proposicion if es falsa. La forma general es

if (expresion)
    proposicion1
else
    proposicion2

Una y solo una de las dos proposiciones asociadas con un if-else se realiza. Si la expresion es verdadera, se ejecuta proposicion1 si no lo es, se ejecuta propo­sicion2. Cada proposicion puede ser una proposicion sencilla o varias entre lla­ves. En el programa para contar palabras, la que esta despues del else es un if que controla dos proposiciones entre llaves.

  • Ejercicio 1-11. ¿Como probaria el programa para contar palabras? ¿Que clase de entrada es la mas conveniente para descubrir errores si estos existen? □
  • Ejercicio 1-12. Escriba un programa que imprima su entrada una palabra por linea. □

1.6 Arreglos

Escribamos un programa para contar el numero de ocurrencias de cada digito, de caracteres espaciadores (caracter en blancos, tabuladores, nueva linea), y de todos los otros caracteres. Esto es artificioso, pero nos permite ilustrar varios aspectos de C en un programa.

Existen doce categorias de entrada, por lo que es conveniente utilizar un arre­glo para mantener el numero de ocurrencias de cada digito, en lugar de tener diez variables individuales. Esta es una version del programa:

#include <stdio.h>

/* cuenta digitos, espacios blancos, y otros */
main()
{
    int c, i, nwhite, nother;
    int ndigit[10];

    nwhite = nother = 0;
    for (i = 0; i < 10; ++i)
        ndigit[i] = 0;

    while ((c = getchar()) != EOF)
        if (c >= '0' && c <= '9')
            ++ndigit[c-'0'];
        else if (c == ' ' || c == '\n' || c == '\t')
            ++nwhite;
        else
            ++nother;
    printf("digitos =");
    for (i = 0; i < 10; ++i)
        printf(" %d", ndigit[i]);
    printf(", espacio en blanco = %d, otro = %d\n",
        nwhite, nother);
}

La salida de este programa al ejecutarlo sobre si mismo es

digitos = 9 3 0 0 0 0 0 0 0 1, espacios en blancos = 123, otros = 345

La declaracion

int ndigit [ 1 0 ];

declara ndigit como un arreglo de 10 enteros. En C, los subindices de arreglos comienzan en cero, por lo que los elementos son ndigit[0], ndigit[1], ndigit[9]. Esto se refleja en los ciclos for que inicializan e imprimen el arreglo.

Un subindice puede ser cualquier expresion entera, lo que incluye a variables enteras como i, y constantes enteras.

Este programa en particular se basa en las propiedades de la representacion de los digitos como caracteres. Por ejemplo, la prueba

if (c >= '0' && c <= '9')

determina si el caracter en c es un digito. Si lo es, el valor numerico del digito es

c - '0'

Esto solo funciona si '0', '1', ..., '9' tienen valores consecutivos ascendentes. Por fortuna, esto es asi en todos los conjuntos de caracteres.

Por definicion, los char son solo pequenos enteros, por lo que las variables y las constantes char son identicas a las int en expresiones aritmeticas. Esto es natural y conveniente; por ejemplo, c - '0' es una expresion entera con un valor entre 0 y 9, correspondiente a los caracteres '’0’' a ’'9’' almacenados en c, por lo que es un subindice valido para el arreglo ndigit.

La decision de si un caracter es digito, espacio en blanco u otra cosa se realiza con la secuencia

if (c >= '0' && c <= '9')
    ++ndigit[c-'0'];
else if (c == ' ' || c == '\n' || c == '\t')
    ++nwhite;
else
    ++nother;

El patron

if (condicion 1)
    proposicion 1
else if (condicion 2)
    proposicion2
        ...
        ...
else
    proposicion_n,

se encuentra frecuentemente en programas como una forma de expresar una deci­sion multiple. Las condiciones se evaluan en orden desde el principio hasta que se satisface alguna condicion; en ese punto se ejecuta la proposicion correspon­diente, y la construccion completa termina. (Cualquier proposicion puede constar de varias proposiciones entre llaves.) Si no se satisface ninguna de las condiciones, se ejecuta la proposicion que esta despues del else final (si es que esta existe). Cuando se omiten el else y la proposicion finales, tal como se hizo en el programa para con­tar palabras, no se lleva a cabo ninguna accion. Puede haber cualquier numero de grupos de

else if (condicion)
    proposicion

entre el if inicial y el else final.

Se recomienda, por estilo, escribir esta construccion tal como se ha mostrado; si cada if estuviese sangrado despues del else anterior, una larga secuencia de de­cisiones podria rebasar el margen derecho de la pagina.

La proposicion switch, que se tratara en el capitulo 3, proporciona otra forma de escribir una decision multiple, que es particularmente apropiada cuando la condicion es determinar si alguna expresion entera o de caracter corresponde con algun miembro de un conjunto de constantes. Para contrastar, se presentara una version de este programa, usando switch, en la seccion 3.4.

  • Ejercicio 1-13. Escriba un programa que imprima el histograma de las longitudes de las palabras de su entrada. Es facil dibujar el histograma con las barras horizontales; la orientacion vertical es un reto mas interesante. □
  • Ejercicio 1-14. Escriba un programa que imprima el histograma de las frecuen­cias con que se presentan diferentes caracteres leidos a la entrada. □

1.7 Funciones

En lenguaje C, una funcion es el equivalente a una subrutina o funcion en Fortran, o a un procedimiento o funcion en Pascal. Una funcion proporciona una forma conveniente de encapsular algunos calculos, que se pueden emplear despues sin preocuparse de su implantacion. Con funciones disenadas adecuada­mente, es posible ignorar como se realiza un trabajo; basta con saber que hace. El lenguaje C hace que el uso de funciones sea facil, conveniente y eficien­te; es comun ver una funcion corta definida y empleada una sola vez, unicamente porque eso esclarece alguna parte del codigo.

Hasta ahora solo se han utilizado funciones como printf, getchar y putchar, que nos han sido proporcionadas; ya es el momento de escribir unas pocas no­sotros mismos. Dado que C no posee un operador de exponenciacion como el ** de Fortran, ilustremos el mecanismo de la definicion de una funcion al escribir la funcion power(m,n), que eleva un entero m a una potencia entera y positiva n. Esto es, el valor de power(2,5) es 32. Esta funcion no es una rutina de exponen­ciacion practica, puesto que solo maneja potencias positivas de enteros pequenos, pero es suficiente para ilustracion (la biblioteca estandar contiene una funcion pow(x,y) que calcula x^y).

A continuacion se presenta la funcion power y un programa main para utili­zarla, de modo que se vea la estructura completa de una vez.

#include <stdio.h>

int power(int m, int n);

/* prueba la funcion power */
main()
{
    int i;

    for (i = 0; i < 10; ++i)
        printf("%d %d %d\n", i, power(2,i), power(-3,i));
    return 0;
}

/* power: eleva la base a la n-esima potencia; n >= 0 */
int power(int base, int n)
{
    int i, p;

    p = 1;
    for (i = 1; i <= n; ++i)
        p = p * base;
    return p;
}

Una definicion de funcion tiene la forma siguiente:

tipo-de-retorno nombre-de-funcion (declaracion de parametros, si los hay)
{
    declaraciones
    proposiciones
}

Las definiciones de funcion pueden aparecer en cualquier orden y en uno o varios archivos fuente, pero una funcion no puede separarse en archivos diferentes. Si el programa fuente aparece en varios archivos, tal vez se tengan que especificar mas cosas al compilar y cargarlo que si estuviera en uno solo, pero eso es cosa del sistema operativo, no un atributo del lenguaje. Por ahora supondremos que ambas funciones estan en el mismo archivo y cualquier cosa que se haya aprendi­do acerca de como ejecutar programas en C, aun funcionaran.

La funcion power es invocada dos veces por main, en la linea

printf("%d %d %d\n", i, power(2,i), power(-3,i));

Cada llamada pasa dos argumentos a power, que cada vez regresa un entero, al que se pone formato y se imprime. En una expresion, power(2,i) es un entero tal como lo son 2 e i. (No todas las funciones producen un valor entero; lo que se ve­ra en el capitulo 4).

La primera linea de la funcion power,

int power(int base, int n)

declara los tipos y nombres de los parametros, asi como el tipo de resultado que la funcion devuelve. Los nombres que emplea power para sus parametros son lo­cales a la funcion y son invisibles a cualquier otra funcion: otras rutinas pueden utilizar los mismos nombres sin que exista problema alguno. Esto tambien es cier­to para las variables i y p: la i de power no tiene nada que ver con la i de main.

Generalmente usaremos parametro para una variable nombrada en la lista en­tre parentesis de la definicion de una funcion, y argumento para el valor emplea­do al hacer la llamada de la funcion. Los terminos argumento formal y argumen­to real se emplean en ocasiones para hacer la misma distincion.

El valor que calcula power se regresa a main por medio de la proposicion return, a la cual le puede seguir cualquier expresion:

return expresion;

Una funcion no necesita regresar un valor; una proposicion return sin expresion hace que el control regrese al programa, pero no devuelve algun valor de utilidad, como se haria al “caer al final” de una funcion al alcanzar el caracter } de llave cerrada que opera de terminacion. Ademas, la funcion que llama puede ignorar el valor que regresa una funcion.

Probablemente haya notado que hay una proposicion return al final de main. Puesto que main es una funcion como cualquier otra, tambien puede regresar un valor a quien la invoca, que es en efecto el medio ambiente en el que el programa se ejecuta. Tipicamente, un valor de regreso cero implica una terminacion nor­mal; los valores diferentes de cero indican condiciones de terminacion no comu­nes o erroneas. En busca de la simplicidad, se han omitido hasta ahora las proposiciones return de las funciones main, pero se incluiran mas adelante, como un recordatorio de que los programas deben regresar su estado final a su medio ambiente.

La declaracion

int power(int m, int n);

precisamente antes de main, indica que power es una funcion que espera dos ar­gumentos int y regresa un int. Esta declaracion, a la cual se le llama funcion prototipo, debe coincidir con la definicion y uso de power. Es un error el que la defi­nicion de una funcion o cualquier uso que de ella se haga no corresponda con su prototipo.

Los nombres de los parametros no necesitan coincidir; de hecho, son opcionales en el prototipo de una funcion, de modo que para el prototipo se pudo haber escrito

int power(int, int);

No obstante, unos nombres bien seleccionados son una buena documentacion, por lo que se emplearan frecuentemente.

Una nota historica: La mayor modificacion entre ANSI C y las versiones ante­riores es como estan declaradas y definidas las funciones. En la definicion original de C, la funcion power se pudo haber escrito de la siguiente manera:

/* power: eleva la base a n-esima potencia; n >= 0 */
/*        (version en estilo antiguo) */
power(base, n)
int base, n;
{
    int i, p;

    p = 1 ;
    for (i = 1 ; i < = n; + + i)
    p — p * base;
    return p;
}

Los parametros se nombran entre los parentesis y sus tipos se declaran antes del caracter { de llave abierta; los parametros que no se declaran se toman como int. {El cuerpo de la funcion es igual a la anterior).

La declaracion de power al inicio del programa pudo haberse visto como sigue:

int power();

No se permitio ninguna lista de parametros, de modo que el compilador no pudo revisar con facilidad que power fuera llamada correctamente. De hecho, puesto que por omision se podia suponer que power regresaba un entero int, toda la decla­racion podria haberse omitido.

La nueva sintaxis de los prototipos de funciones permite que sea mucho mas facil para el compilador detectar errores en el numero o tipo de argumentos. El viejo estilo de declaracion y definicion aun funciona en ANSI C, al menos por un periodo de transicion, pero se recomienda ampliamente utilizar la nueva forma si se tiene un compilador que le de soporte.

  • Ejercicio 1-15. Escriba de nuevo el programa de conversion de temperatura de la seccion 1.2, de modo que utilice una funcion para la conversion.

1.8 Argumentos -- llamadas por valor

Hay un aspecto de las funciones de C que puede parecer poco familiar a los programadores acostumbrados a otros lenguajes, particularmente Fortran. En C, todos los argumentos de una funcion se pasan “por valor” . Esto significa que la funcion que se invoca recibe los valores de sus argumentos en variables tempo­rales y no en las originales. Esto conduce a algunas propiedades diferentes a las que se ven en lenguajes con “llamadas por referencia” como Fortran o con para­metros var en Pascal, en donde la rutina que se invoca tiene acceso al argumento original, no a una copia local.

La diferencia principal es que en C la funcion que se invoca no puede alterar directamente una variable de la funcion que hace la llamada; solo puede modifi­car su copia privada y temporal.

Sin embargo, la llamada por valor es una ventaja, no una desventaja. Por lo general, esto conduce a elaborar programas mas compactos con pocas variables extranas, puesto que los parametros se tratan en la funcion invocada como va­riables locales convenientemente inicializadas. Por ejemplo, he aqui una version de power que utiliza esta propiedad.

/* power: eleva la base a la n-esima potencia; n > = 0 ; version 2 */
int power(int base, int n)
{
    int p;

    for (p = 1; n > 0; --n)
        p = p * base;
    return p;
}

El parametro n se utiliza como una variable temporal, y se decrementa (un ciclo for que se ejecuta hacia atras) hasta que llega a cero; ya no es necesaria la variable i. Cualquier cosa que se le haga a n dentro de power no tiene efecto sobre el argu­mento con el que se llamo originalmente power.

Cuando sea necesario, es posible hacer que una funcion modifique una va­riable dentro de una rutina invocada. La funcion que llama debe proporcionar la direccion de la variable que sera cambiada (tecnicamente un apuntador a la va­riable), y la funcion que se invoca debe declarar que el parametro sea un apunta­dor y tenga acceso a la variable indirectamente a traves de el. Los apuntadores se trataran en el capitulo 5.

La historia es diferente con los arreglos. Cuando el nombre de un arreglo se emplea como argumento, el valor que se pasa a la funcion es la localizacion o la direccion del principio del arreglo — no hay copia de los elementos del arreglo. Al colocarle subindices a este valor, la funcion puede tener acceso y alterar cual­quier elemento del arreglo. Este es el tema de la siguiente seccion.

1.9 Arreglos de caracteres

El tipo de arreglo mas comun en C es el de caracteres. Para ilustrar el uso de arreglos de caracteres y funciones que los manipulan, escriba un programa que lea un conjunto de lineas de texto e imprima la de mayor longitud. El pseudocodigo es bastante simple:

while (hay otra linea)
    if (es mas larga que la anterior mas larga)
        guardala
        guarda su longitud
imprime la linea mas larga

Este pseudocodigo deja en claro que el programa se divide naturalmente en par­tes. Una trae una nueva linea, o trae la prueba y el resto controla el proceso.

Puesto que la division de las partes es muy fina, lo correcto sera escribirlas de ese modo. Asi pues, escribamos primero un a funcion getline para extraer la siguiente linea de la entrada. Trataremos de hacer a la funcion util en otros con­textos. Al menos, getline tiene que regresar una senal acerca de la posibilidad de un fin de archivo; un diseno de mas utilidad debera retornar la longitud de la li­nea, o cero si se encuentra el fin de archivo. Cero es un regreso de fin de archivo aceptable debido a que nunca es una longitud de linea valida. Cada linea de texto tiene al menos un caracter; incluso una linea que solo contenga un caracter nueva linea, tiene longitud 1.

Cuando se encuentre una linea que es mayor que la anteriormente mas larga, se debe guardar en algun lugar. Esto sugiere una segunda funcion copy, para co­piar la nueva linea a un lugar seguro.

Finalmente, se necesita un programa principal para controlar getline y copy.

El resultado es el siguiente:

#include <stdio.h>
#define MAXLINE 1000    /* tamano maximo de la linea de entrada */

int getline(char line[], int maxline);
void copy(char to[], char from[]);

/* imprime la linea de entrada mas larga */
main()
{
    int len;               /* longitud actual de la linea */
    int max;               /* maxima longitud vista hasta el momento */
    char line[MAXLINE];    /* linea de entrada actual */
    char longest[MAXLINE]; /* la linea mas larga se guarda aqui */

    max = 0 ;
    while ((len = getline(line, MAXLINE)) > 0)
        if (len > max) {
          max = len;
          copy(longest, line);
        }
    if (max > 0 )    /* hubo una linea */
        printf("%s", longest);
    return 0 ;
}

/* getline: lee una linea en s, regresa su longitud */
int getline(char s[], int lim)
{
    int c, i;

    for (i=0; i < lim-1 && (c=getchar())!=EOF && c!='\n'; ++i)
        s[i] = c;
    if (c == '\n') {
        s[i] = c;
        ++i;
    }
    s[i] = '\0';
    return i;
    }
    s[i] = '\0';
    return i;
}
/* copy: copia 'from' en 'to'; supone que to es suficientemente grande */
void copy(char to[], char from[])
{
    int i;

    i = 0;
    while ((to[i] = from[i]) != '\0')
        ++i;
}

Las funciones getline y copy estan declaradas al principio del programa, que se supone esta contenido en un archivo. main y getline se comunican a traves de un par de argumentos y un valor de retorno. En getline los argumentos se declaran por la linea

int getline(char s[], int lim);

que especifica que el primer argumento, s, es un arreglo, y el segundo, lim, es un entero. El proposito de proporcionar el tamano de un arreglo es fijar espacio de almacenamiento contiguo. La longitud del arreglo s no es necesaria en getline, puesto que su tamano se fija en main. En getline se utiliza return para regresar un valor a quien lo llama, tal como hizo la funcion power. Esta linea tambien declara que getline regresa un int; puesto que int es el valor de retorno por omi­sion, puede suprimirse.

Algunas funciones regresan un valor util; otras, como copy, se emplean uni­camente por su efecto y no regresan un valor. El tipo de retorno de copy es void, el cual establece explicitamente que ningun valor se regresa.

En getline se coloca el caracter \0 (caracter nulo, cuyo valor es cero) al final del arreglo que esta creando, para marcar el fin de la cadena de caracteres. Esta con­vencion tambien se utiliza por el lenguaje C; cuando una constante de caracter como

"hola\n"

aparece en un programa en C, se almacena como un arreglo que contiene los caracteres de la cadena y termina con un \0 para marcar el fin.

h o l a \n \0

La especificacion de formato %s dentro de printf espera que el argumento co­rrespondiente sea una cadena representada de este modo; copy' tambien se basa en el hecho de que su argumento de entrada se termina con \0, y copia este ca­racter dentro del argumento de salida.

Todo esto implica que \0 no es parte de un texto normal.

Es util mencionar de paso que aun un programa tan pequeno como este pre­senta algunos problemas de diseno. Por ejemplo, ¿que debe hacer main si en­cuentra una linea que es mayor que su limite? getline trabaja en forma segura, en ese caso detiene la recopilacion cuando el arreglo esta lleno, aunque no en­cuentre el caracter nueva linea. Probando la longitud y el ultimo caracter devuel­to, main puede determinar si la linea fue demasiado larga, y entonces realiza el tratamiento que se desee. Por brevedad, hemos ignorado el asunto.

No existe forma para un usuario de getline de saber con anticipacion cuan larga podra ser una linea de entrada, por lo que getline revisa un posible desbor­damiento ("overflow"). Por otro lado, el usuario de copy ya conoce (o lo puede averiguar) cual es el tamano de la cadena, por lo que decidimos no agregar comprobacion de errores en ella.

  • Ejercicio 1-16. Corrija la rutina principal del programa de la linea mas larga de modo que imprima correctamente la longitud de lineas de entrada arbitrariamen­te largas, y tanto texto como sea posible. □
  • Ejercicio 1-17. Escriba un programa que imprima todas las lineas de entrada que sean mayores de 80 caracteres. □
  • Ejercicio 1-18. Escriba un programa que elimine los blancos y los tabuladores que esten al final de cada linea de en trad a, y que borre completamente las lineas en blanco. □
  • Ejercicio 1-19. Escriba una funcion reverse(s) que invierta la cadena de caracte­res s. Usela para escribir un programa que invierta su entrada, linea a linea. □

1.10 Variables externas y alcance

Las variables que estan en main, tal como line, longest, etc., son privadas o locales a ella. Debido a que son declaradas dentro de main, ninguna otra fun­cion puede tener acceso directo a ellas. Lo mismo tambien es valido para va­riables de otras funciones; por ejemplo, la variable i en getline no tiene relacion con la i que esta en copy. Cada variable local de una funcion comienza a existir solo cuando se llama a la funcion, y desaparece cuando la funcion termina. Esto es por lo que tales variables son conocidas como variables automaticas, siguiendo la terminologia de otros lenguajes. Aqui se utilizara en adelante el termino automatico para hacer referencia a esas variables locales. (En el capitulo 4 se discute la categoria de almacenamiento static ("estatica"), en la que las variables locales si conser­van sus valores entre llamadas.)

Puesto que las variables locales aparecen y desaparecen con la invocacion de funciones, no retienen sus valores entre dos llamadas sucesivas, y deben ser inicializadas explicitamente en cada entrada. De no hacerlo, contendran “basura” .

Como una alternativa a las variables automaticas, es posible definir variables que son externas a todas las funciones, esto es, variables a las que toda funcion puede tener acceso por su nombre. (Este mecanismo es parecido al COMMON de Fortran o a las variables de Pascal declaradas en el bloque mas exterior). Debi­do a que es posible tener acceso global a las variables externas, estas pueden ser usadas en lugar de listas de argumentos para comunicar datos entre funciones. Ademas, puesto que las variables externas se mantienen permanentemente en existencia, en lugar de aparecer y desaparecer cuando se llaman y terminan las funciones, mantienen sus valores aun despues de que regresa la funcion que los fijo.

Una variable externa debe definirse, exactamente una vez, fuera de cualquier funcion; esto fija un espacio de almacenamiento para ella. La variable tambien debe declararse en cada funcion que desee tener acceso a ella; esto establece el tipo de la variable. La declaracion debe ser una proposicion extern explicita, o bien puede estar implicita en el contexto. Para concretar la discusion, reescribamos el programa de la linea mas larga con line, longest y max como variables externas. Esto requiere cambiar las llamadas, declaraciones y cuerpos de las tres funciones.

#include <stdio.h>

#define MAXLINE 1000      /* maximo tamano de una linea de entrada */

int max;                  /* maxima longitud vista hasta el momento */
char line [MAXLINE];      /* linea de entrada actual */
char longest [MAXLINE];   /* la linea mas larga se guarda aqui */

int getline(void);
void copy(void);

/* imprime la linea de entrada mas larga; version especializada */
main()
{
    int len;
    extern int max;
    extern char longest[];

    max = 0;
    while ((len = getline()) > 0)
        if (len > max) {
            max = len;
            copy();
        }
    if (max > 0) /* hubo una linea */
        printf("%s", longest);
    return 0;
}

/* getline: version especializada */
int getline(void)
{
    int c, i;
    extern char line[];

    for (i = 0; i < MAXLINE - 1
        && (c=getchar)) != EOF && c != '\n'; ++i)
            line[i] = c;
    if (c == '\n') {
        line[i] = c;
        ++i;
}
    line[i] = '\0';
    return i;
}

/* copy: version especializada */
void copy(void)
{
    int i;
    extern char line[], longest[];

    i = 0;
    while ((longest[i] = line[i]) != '\0')
        ++i;
}

Las variables externas de main, getline y copy estan definidas en las primeras lineas del ejemplo anterior, lo que establece su tipo y causa que se les asigne espa­cio de almacenamiento. Desde el punto de vista sintactico, las definiciones exter­nas son exactamente como las definiciones de variables locales, pero puesto que ocurren fuera de las funciones, las variables son externas. Antes de que una fun­cion pueda usar una variable externa, se debe hacer saber el nombre de la variable a la funcion. Una forma de hacer esto es escribir una declaracion extern dentro de la funcion; la declaracion es la misma que antes, excepto por la palabra reservada extern.

Bajo ciertas circunstancias, la declaracion extern puede omitirse. Si la defini­cion de una variable externa ocurre dentro del archivo fuente antes de su uso por una funcion en particular, entonces es necesario usar una declaracion extern dentro de la funcion. La declaracion extern en main, getline y copy es - por tanto - redundante. De hecho, una practica comun consiste en colocar las definiciones de todas las variables externas al principio del archivo fuente y luego omitir todas las declaraciones extern.

Si el programa esta conformado por varios archivos de codigo fuente y una variable se define en archi­vo1 y tambien se recurre a ella en archivo2 y archivo3, entonces es necesario realizar las declaraciones extern en archivo2 y archivo3 para conectar las ocurrencias de la variable. La practica comun es reunir dichas declaraciones de variables y funciones extern en un archivo sepa­rado - historicamente denominado header.h (el sufijo .h se usa por convencion para nombres de header) - los cuales son incluido por medio de #include al principio de cada archivo fuente. Las funciones de la biblioteca estandar, por ejemplo, estan declaradas en headers como <stdio.h>. Este tema se trata ampliamente en el capitulo 4, y la biblioteca en el capitulo 7 y en el apendice B.

Puesto que las versiones especializadas de getline y copy no tienen argumen­tos, la logica sugeriria que sus prototipos al principio del archivo deben ser getline() y copy(). Pero para mantener compatibilidad con programas de C anteriores, el estan­dar considera una lista vacia como una declaracion del estilo antiguo, y suspende toda revision de listas de argumentos; para una lista explicitamente vacia debe emplearse la palabra void. Esto se discutira en el capitulo 4.

Se debe notar que en esta seccion empleamos cuidadosamente las palabras definicion y decla­racion cuando nos referimos a variables externas. La palabra “de­finicion” se refiere al lugar donde se crea la variable o se le asigna un lugar de almacenamiento; “declaracion” se refiere al lugar donde se establece la naturale­za de la variable pero no se le asigna espacio.

A proposito, existe una tendencia a convertir todo en variables extern, debido a que aparentemente simplifica las comunicaciones — las listas de argumentos son cortas y las variables siempre estan alli, cuando se las necesita. Pero las variables externas existen siempre, aun cuando no hacen falta. Descansar en la dependencia de las variables externas resulta peligroso, puesto que lleva a programas cuyas cone­xiones entre datos no son absolutamente obvias — las variables pueden alterarse de manera inadvertida e inesperada, y dichos programas son dificiles de modificar. La segunda version del programa de la linea mayor es inferior a la primera, en parte por las anteriores razones y en parte porque destruye la generalidad de dos utiles funciones, introduciendo en ellas los nombres de las variables que manipula.

Hasta este punto hemos descrito lo que podria llamarse los fundamentos con­vencionales de C. Con estos fundamentos, le sera posible escribir programas utiles de tamano considerable, y probablemente seria una buena idea hacer una pausa sufi­cientemente grande para realizarlos. Estos ejercicios sugieren programas de complejidad algo mayor que los presentados anteriormente en este capitulo.

  • Ejercicio 1-20. Escriba un programa detab que reemplace tabuladores de la en­trada con el numero apropiado de caracteres en blancos para espaciar hasta el siguiente paro de tabulacion. Considere un conjunto fijo de paros de tabulacion, digamos cada n columnas. ¿Debe ser n una variable o un parametro simbolico? □
  • Ejercicio 1-21. Escriba un programa entab que reemplace cadenas de blancos por el minimo numero de tabuladores y blancos para obtener el mismo espaciado. Considere los paros de tabulacion de igual manera que para detab. Cuando un tabulador o un simple espacio en blanco fuese suficiente para alcanzar un paro de tabulacion, ¿a cual se le debe dar preferencia? □
  • Ejercicio 1-22. Escriba un programa para "dividir" lineas grandes de entrada en dos o mas lineas mas cortas despues del ultimo caracter no blanco que ocurra an­tes de la n-esima columna de entrada. Asegurese de que su programa se comporte apropiadamente con lineas muy largas, y de que no existan caracteres en blancos o tabuladores antes de la columna especificada. □
  • Ejercicio 1-23. Escriba un programa para eliminar todos los comentarios de un programa en C. No olvide manejar apropiadamente las cadenas entre comillas y las constantes de caracter. Los comentarios de C no se anidan. □
  • Ejercicio 1-24. Escriba un programa para revisar los errores de sintaxis rudimen­tarios de un programa en C, como parentesis, llaves y corchetes no alineados. No olvide las comillas ni los apostrofos, las secuencias de escape y los comentarios. (Este programa es dificil si se hace completamente general). □

Continuar: Capitulo 2